Lambda 表达式和匿名函数是 C# 开发中的基本功能，为我们提供了简洁灵活的代码构造。在这篇文章中，我将讨论 lambda 表达式的基础知识，包括它们的语法、使用场景、最新的 C# 12 功能和其他一些高级功能。Lambda 表达式基础知识表达式 lambda表达式 lambda 遵循一个简单的模式：（input-parameters） => expression。例如，（x） => x * x 将输入值 x 平方。这些 lambda 通常用于各种方案，例如 LINQ 查询、使用 Task.Run（） 进行任务计划以及异步编程。LINQ 查询// List of integers List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; // Using a lambda expression in a LINQ query to filter even numbers var evenNumbers = numbers.Where(x => x % 2 == 0); // Output the result foreach (var number in evenNumbers) { Console.WriteLine(number); }Lambda 表达式用于从列表中筛选偶数。(x => x % 2 == 0)任务调度Task.Run()// Using Task.Run with an expression lambda to execute a task asynchronously Task.Run(() => { // Perform some time-consuming operation for (int i = 0; i < 5; i++) { Console.WriteLine($"Task is running: {i}"); Thread.Sleep(1000); // Simulate some work } });在 的任务调度中，表达式 lambda 用于定义异步运行的代码块。Task.Run()语句 lambdas相比之下，语句 lambda 将它们的身体包裹在大括号内：(input-parameters) => { <sequence-of-statements> }它们为更复杂的逻辑和执行多个语句提供了一个平台。与表达式 lambda 不同，它们在处理控制流和变量范围方面提供了更大的灵活性。例如，请考虑以下事项：Action<string> greet = name => { string greeting = $"Hello {name}!"; Console.WriteLine(greeting); }; greet("Niraj");在这里，lambda greet 接受一个字符串参数，使用该参数构造一个问候消息，然后将其打印到控制台。Lambda 表达式的输入参数单一输入参数Lambda 表达式可以具有单个或多个输入参数。在处理单个输入参数时，语法简洁明了，并为声明提供了可选的括号，从而实现了灵活性和可读性。可以声明单个参数，也可以不带括号：或 。虽然括号是可选的，但它们有助于提高代码的清晰度和组织性。x => x x(x) => x x多个输入参数多个参数之间用逗号分隔：。- 显式和隐式类型推断提高了代码的清晰度。(x, y) => x + yFunc<int, int, bool> testForEquality = (x, y) => x == y;这行简单的代码声明了一个名为 的委托，该委托表示一个函数，该函数采用两个整数参数并返回一个布尔值。该表达式定义了一个匿名函数，如果第一个参数等于第二个参数，则返回该函数，否则返回。testForEquality(x, y) => x == ytruexyfalse委托类型和转换Lambda 表达式可以转换为 Action 和 Func 等委托类型，从而有助于创建动态和简洁的代码结构。// Lambda expression converted to Action delegate Action<int> printNumber = x => Console.WriteLine(x); printNumber(10); // Output: 10 // Lambda expression converted to Func delegate Func<int, int, int> addNumbers = (x, y) => x + y; int result = addNumbers(5, 3); // result = 8Action 委托用于具有 void 返回类型的方法，而 Func 委托则用于返回非 void 值的方法。例如，该表达式举例说明了名为 的 Func 委托的创建，该委托计算其整数参数的平方。Func<int, int> square = x => x * x;squareAction 和 Func 委托类型示例：// Action delegate example (void return type) Action<string> greet = name => Console.WriteLine($"Hello, {name}!"); greet("John"); // Output: Hello, John! // Func delegate example (non-void return type) Func<int, int> square = x => x * x; int squaredValue = square(5); // squaredValue = 25示例演示：// Asynchronous programming with lambda expressions Task.Run(() => { // Simulate a time-consuming operation Thread.Sleep(2000); Console.WriteLine("Asynchronous task completed!"); }); // LINQ query using lambda expressions List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; var evenNumbers = numbers.Where(x => x % 2 == 0); foreach (var number in evenNumbers) { Console.WriteLine(number); }Lambda 表达式和元组要定义元组，只需将其组件的逗号分隔列表括在括号内即可。例如，考虑下面的示例，其中具有三个组件的元组用于将数字序列传达到 lambda 表达式。然后，lambda 将每个值加倍，并返回一个包含相应结果的元组。Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3); var numbers = (2, 3, 4); var doubledNumbers = doubleThem(numbers); Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}"); // Output: // The set (2, 3, 4) doubled: (4, 6, 8)此代码块演示了一个名为 lambda 表达式的用法 ，该表达式将一个元组 作为输入并返回另一个元组。lambda 表达式使用参数将输入元组的每个分量乘以 2 。然后，它构造并返回一个具有双倍值的新元组。在此示例中， 带有值_的元组_将传递给 lambda 表达式，而生成的加倍元组将存储在 中。doubleThem(int, int, int)(int, int, int)nsnumbers(2, 3, 4)doubledNumbers默认情况下，元组字段被命名为 Item1、Item2 等。但是，可以定义具有命名组件的元组以提高清晰度，如后续示例中所示。Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3); var numbers = (2, 3, 4); var doubledNumbers = doubleThem(numbers); Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");此块详细说明了 C# 中元组组件的命名约定。它定义了另一个 lambda 表达式 ，该表达式将具有_命名组件的元组_作为输入。与前面的示例类似，lambda 表达式使用参数将输入元组的每个命名分量加倍 2 。然后，它构造并返回一个具有双倍值的新元组。doubleThem(int n1, int n2, int n3)ns错误处理和最佳实践使用 Lambda 表达式进行错误处理使用 lambda 表达式时，请务必了解错误处理中的潜在陷阱。常见问题包括 null 引用异常和不正确的参数类型。// Example of error handling in a lambda expression Func<int, int> divideByZeroSafe = divisor => { try { return 10 / divisor; } catch (DivideByZeroException ex) { // Handle divide by zero error Console.WriteLine($"Error: {ex.Message}"); return 0; } }; // Invoke the lambda expression int result = divideByZeroSafe(0); // Output: Error: Attempted to divide by zero.在此示例中，lambda 表达式尝试执行除法运算，但通过捕获它并返回默认值来处理 。divideByZeroSafeDivideByZeroException编写 Lambda 表达式的最佳实践请考虑以下准则，保持 lambda 表达式简洁明了且富有表现力。避免过于复杂或令人费解的语法。通过最大限度地减少不必要的分配和计算开销来优化性能。优先考虑可读性和代码可维护性。使用 Lambda 表达式的优点简洁：与传统方法相比，Lambda 表达式允许紧凑和简洁的代码。可读性：它们更清楚地表达了程序员的意图，使代码更易于理解。灵活性： Lambda 表达式可以作为数据传递，从而在程序中实现动态行为。改进的 API 设计：Lambda 表达式有助于创建直观且功能强大的 API。函数式编程： 它们支持函数式编程范式，如高阶函数和函数组合。并发和异步编程：Lambda 表达式简化了定义异步操作的语法，有助于开发并发和可扩展的应用程序。C# 12 的新功能Lambda 表达式现在可以具有默认参数，从而增强了其灵活性。var IncrementBy = (int source, int increment = 1) => source + increment; Console.WriteLine(IncrementBy(5)); // 6 Console.WriteLine(IncrementBy(5, 2)); // 7我们还可以声明带有数组作为参数的 lambda 表达式。paramsvar arraySum = (params int[] nums) => nums.Sum(); int[] array1 = { 2, 3, 4, 5 }; Console.WriteLine(arraySum(2, 3, 4, 5));lambda 表达式和匿名函数就像方便的快捷方式，使我们的代码更短、更易于理解。我鼓励你继续探索它们在不同情况下是如何工作的。
.NET中至少有6种定时器，每一种定时器都有它的用途和特点。根据定时器的应用场景，可以分为UI相关的定时器和UI无关的定时器。本文将简单介绍这6种定时器的基本用法和特点。UI定时器.NET中的UI定时器主要是WinForm、WPF以及WebForm中的定时器。分别为：System.Windows.Forms.TimerSystem.Windows.Threading.DispatcherTimerSystem.Web.UI.Timer通常情况下，WinForm、WPF中的定时器是在UI线程上执行回调函数，因此可以直接访问UI元素。由于WinForm、WPF支持单线程单元模型（Single-Thread Apartment，STA），定时器间隔事件是在UI线程上触发，因此，不用担心线程安全问题。System.Web.UI.Timer是通过Javascript定时器和服务端异步回调实现，也是单线程的。请注意，这里说的是通常情况，后边介绍System.Windows.Threading.DispatcherTimer时会提到在非UI线程创建DispatcherTimer时也无法直接访问UI元素。System.Windows.Forms.TimerSystem.Windows.Forms.Timer针对WinForm应用进行了优化，是只能在WinForm上使用的定时器。这个定时器是针对单线程环境设计的，是在UI线程上处理定时任务。它要求用户代码有可用的UI消息泵，定时任务须在UI线程上运行，或者跨线程通过Invoke或者BeginInvoke封送(marshal)到UI线程上运行。其优点是使用简单，只需通过给Interval属性赋值来设置时间间隔，并注册Tick事件处理定时任务。其缺点是精度不高，精度为55毫秒，也就是Interval赋值小于55时，也是55毫秒触发一次定时任务。public partial class TimerFrom : Form { private System.Windows.Forms.Timer digitalClock; private void TimerFrom_Load(object sender, EventArgs e) { digitalClock = new System.Windows.Forms.Timer();//创建定时器 digitalClock.Tick += new EventHandler(HandleTime);//注册定时任务事件 digitalClock.Interval = 1000;//设置时间间隔 digitalClock.Enabled = true; digitalClock.Start(); //开启定时器 } public void HandleTime(Object myObject, EventArgs myEventArgs) { labelClock.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); } private void frmTimerDemo_FormClosed(object sender, FormClosedEventArgs e) { digitalClock.Stop();//停止定时器 digitalClock.Dispose(); } }System.Windows.Threading.DispatcherTimerSystem.Windows.Threading.DispatcherTimer是WPF中的定时器，它是基于Dispatcher对象的(并不是基于UI线程的)。DispatcherTimer的定时任务是像其他操作一样放在Dispatcher队列上，其执行操作时间依赖于队列中其他任务及其优先级，因此，DispatcherTimer不保证在时间间隔发生时准确执行，只保证不会在时间间隔发生前执行。Dispatcher为特定线程维护工作项（操作）的优先级队列，在线程上创建Dispatcher对象时，它成为唯一可以关联该线程的Dispatcher对象，WPF中，DispatcherObject只能被与之关联的Dispatcher对象访问，也就是非UI线程中无法直接访问UI元素（WPF中的UI元素都是派生自DispatcherObject）此外，DispatcherTimer不像System.Windows.Forms.Timer那样只在UI线程上创建才能触发Tick事件，它在非UI线程下创建也可以触发Tick事件，此时访问UI元素也需要通过Invoke或者BeginInvoke封送(marshal)到UI线程上运行。其优点也是简单易用，适合在UI线程上执行任务或触发事件，缺点是精度不准确，可能存在延迟。private void Dt_Tick(object sender, EventArgs e) { Dispatcher.BeginInvoke((Action)delegate () { text1.Text = DateTime.Now.ToString(); }); Console.WriteLine(DateTime.Now.ToString()); } private void Button_Click(object sender, RoutedEventArgs e) { Task.Run(() =>{ DispatcherTimer dt = new DispatcherTimer(); dt.Tick += Dt_Tick; dt.Interval = TimeSpan.FromSeconds(1); dt.Start(); Dispatcher.Run(); }); }上述代码中，DispatcherTimer是非UI线程中创建，定时任务中访问UI元素text1，需要通过Invoke或者BeginInvoke封送(marshal)到UI线程上运行，而Console.WriteLine则可以直接运行。System.Web.UI.TimerSystem.Web.UI.Timer是仅适用于.NET Framework的ASP.NET组件。通过Javascript定时器和服务端异步回调实现。每次触发定时器时，只能执行一个异步回调方法，而其他的异步回调方法需要等待前一个异步回调方法执行完毕后才能执行。这样可以保证在任意时刻只有一个异步回调方法在执行，避免了多线程并发执行的问题。UI无关定时器从 .NET 6开始，UI无关定时器有三个：System.Threading.TimerSystem.Timers.TimerSystem.Threading.PeriodicTimer（.NET 6+）System.Threading.TimerSystem.Threading.Timer是最基础轻量的定时器，它将定期在线程池线程上执行单个回调方法。在创建定时器对象时必须指定回调方法，并且后续不能修改，同时也可以指定定时器回调开始执行的时间以及时间间隔。定时器创建后可以通过Change方法修改回调开始执行的时间以及时间间隔。该定时器的优点是轻量，精度相对较高，与Windows操作系统时钟精度一致，大约15毫秒。但因为是基于线程池的，所以在任务执行时间较长或者线程池过载时，会出现延迟。其缺点是使用不太方便，定时器创建后无法修改回调方法。var stateTimer = new var autoEvent = new AutoResetEvent(false); Timer(CheckStatus, autoEvent, 1000,250); private int invokeCount=0; public void CheckStatus(Object stateInfo) { AutoResetEvent autoEvent = (AutoResetEvent)stateInfo; Console.WriteLine("{0} Checking status {1,2}.",DateTime.Now.ToString("h:mm:ss.fff"),(++invokeCount).ToString()); if(invokeCount == 10) { invokeCount = 0; autoEvent.Set(); } }System.Timers.TimerSystem.Timers.Timer在内部使用System.Threading.Timer，并公开了更多的属性，如AutoReset, Enabled或SynchronizingObject，这些属性允许配置回调的执行方式。此外，Tick事件允许注册多个处理程序。因此，一个定时器可以触发多个处理程序。还可以在计时器启动后更改处理程序。与System.Threading.Timer相似，其优点也是精度相对较高，与Windows操作系统时钟精度一致，大约15毫秒。因为默认（或者SynchronizingObject=null时）是基于线程池的，所以在任务执行时间较长或者线程池过载时，会出现延迟。但使用要更简便一些。public partial class TimerFrom : Form { private System.Timers.Timer timer; private void TimerFrom_Load(object sender, EventArgs e) { // 支持注册多个处理程序 timer.Elapsed += (sender, e) => { label1.Text = DateTime.Now.ToLongTimeString(); }; timer.Elapsed += (sender, e) => { Console.WriteLine(DateTime.Now.ToLongTimeString()); }; //自定义回调执行的方式（指定对象所在的线程），SynchronizingObject=null时在线程池上执行 timer.SynchronizingObject = this; timer.AutoReset = true; timer.Start(); } }本例中将SynchronizingObject属性设置为Form对象，因此Elapsed的处理程序在UI线程上执行，可以直接修改label1.Text，如果SynchronizingObject属性为null，处理程序则是在线程池线程上执行，修改label1.Text时需要通过Invoke或者BeginInvoke封送(marshal)到UI线程上运行。System.Threading.PeriodicTimer#System.Threading.PeriodicTimer是 .NET 6中引入的定时器。它能方便地使用异步方式，它没有Tick事件，而是提供WaitForNextTickAsync方法处理定时任务。通常是使用While循环结合CancellationToken一起使用。和CancellationToken一起用的时候需要注意，如果CancellationToken被取消的时候会抛出一个OperationCanceledException需要考虑自己处理异常。相比之前的定时器来说，有下面几个特点：[1]没有callback 来绑定事件;不会发生重入，只允许有一个消费者，不允许同一个PeriodicTimer在不同的地方同时WaitForNextTickAsync，不需要自己做排他锁来实现不能重入；异步化。之前的 timer 的 callback 都是同步的，使用新 timer 可以使用异步方法，避免了编写 Sync over Async 代码；Dispose 之后，实例就无法使用，并且 WaitForNextTickAsync 始终返回 false。var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); using (var timer = new PeriodicTimer(TimeSpan.FromSeconds(1))) { try { while (await timer.WaitForNextTickAsync(cts.Token)) { await Task.Delay(3000); Console.WriteLine($"ThreadId is {Thread.CurrentThread.ManagedThreadId} --- Time is {DateTime.Now:HH:mm:ss}"); } } catch (OperationCanceledException) { Console.WriteLine("Operation cancelled"); } } C# 复制 全屏小结我们在开发过程中遇到的坑往往不是技术本身的坑，而是我们滥用没有掌握的技术导致的，在有多种技术方案可选的时候，通常只关注技术的优点，忽略了技术适用场景及其局限性。.NET中几种定时器各自都有其适用场景和不足，但都不支持高精度计时。了解这些有助于我们在开发过程中选择合适定时器，避免遇到问题后被动地替换解决方案。https://xie.infoq.cn/article/6aa23b6850abddf717a6c9fc9 ↩︎作者：czwy出处：https://www.cnblogs.com/czwy/p/17862702.html
在C#中，object和dynamic类型都提供了一种处理数据的灵活方式，但它们的用途和行为在某些关键方面存在显著差异。了解这些差异有助于更合理地使用这两种类型，并优化代码的性能和可维护性。object 类型定义：object是.NET中所有类型的基类。任何类型的数据都可以赋值给object类型的变量。类型安全：使用object类型时，编译时类型检查是类型安全的。但在运行时，如果需要将object类型转换回原始类型，就需要显式的类型转换或使用as、is操作符，这可能引发运行时错误。性能：频繁地将值类型装箱（赋值给object类型）和拆箱（转换回原始值类型）会影响性能，因为这些操作需要在托管堆上分配和回收内存。dynamic 类型定义：dynamic类型在编译时跳过类型检查，在运行时解析类型信息。这使得编写与COM对象、动态语言（如Python）互操作的代码或使用反射等场景下的代码更为简便。类型安全：dynamic类型不是类型安全的，因为所有关于类型的错误只能在运行时被捕获。这增加了运行时错误的风险，但提供了更大的灵活性。性能：使用dynamic类型可能会导致比object类型更大的性能开销，因为它需要在运行时解析类型信息。这个开销在每次动态操作时都会发生，尤其是在循环或高频调用的场景中更为明显。关键区别编译时类型检查object: 使用object类型时，编译器会进行类型检查。任何类型的数据都可以赋值给object类型的变量，这是通过装箱操作实现的（对于值类型）。从object类型转换回原始类型需要显式的拆箱或类型转换，这在编译时就确定了。如果类型转换不正确，会在编译时捕获到错误。dynamic: 使用dynamic类型时，编译器不会进行类型检查。相反，所有关于该变量的操作都会推迟到运行时解析。这意味着，如果对dynamic类型变量的操作无效，错误只会在运行时被发现。性能object: object类型的性能开销主要源于需要装箱和拆箱操作，尤其是对于值类型。这些操作需要在运行时进行，可能会影响性能，但不涉及运行时类型解析。dynamic: dynamic类型的性能开销来自于运行时的类型解析。每次对dynamic类型的变量进行操作时，都需要动态地检查对象可以执行哪些操作，这比静态类型检查更耗时。使用场景object: 由于其类型安全的特性，object类型适用于需要处理不同数据类型但又希望保持类型安全的场景。例如，作为泛型集合中的元素类型，或在需要在不同类型之间进行显式转换的场景中使用。dynamic: dynamic类型适用于需要高度灵活性的场景，如与动态语言的互操作（例如，Python或JavaScript），或者当访问COM对象时。dynamic类型减少了需要编写的样板代码量，并允许更灵活地处理没有静态类型信息的数据。错误处理object: 类型转换错误可以在编译时被发现，因此使用object类型时，代码通常更安全，更易于调试。dynamic: 由于类型检查被推迟到运行时，因此使用dynamic类型可能会导致运行时错误，这些错误在编译时是不可见的。这使得dynamic类型的代码可能更难调试。使用与优化建议优先考虑类型安全：除非确实需要dynamic类型提供的灵活性，否则应优先使用静态类型或object类型。静态类型在编译时提供错误检查，有助于提高代码的稳定性和可维护性。避免不必要的装箱拆箱：如果频繁处理值类型数据，考虑使用泛型或值类型集合来避免装箱拆箱带来的性能损失。谨慎使用dynamic：在需要处理动态类型数据或与动态语言互操作时，dynamic类型非常有用。但在其他情况下，使用dynamic可能会使代码难以理解和维护，并且增加运行时错误的风险。仅在确实需要动态特性时使用dynamic。性能关键路径避免使用dynamic：在性能敏感的代码路径中，避免使用dynamic类型。考虑使用其他方式，如反射加缓存、委托、表达式树等，来实现所需的动态行为，同时优化性能。
在C#中，System.Runtime.InteropServices.Marshal类是一个非常重要的类，它提供了一组方法和属性，用于在托管代码与非托管代码之间进行数据的互操作（Interop）。这包括分配和释放非托管内存、复制数据、以及处理非托管函数指针等功能。简而言之，Marshal类是托管代码（.NET 应用程序）和非托管代码（如C++编写的动态链接库DLL）之间的桥梁。Marshal类的一些主要用途包括：内存分配和释放：AllocHGlobal和AllocCoTaskMem方法用于在非托管内存中分配内存。FreeHGlobal和FreeCoTaskMem方法用于释放那些通过AllocHGlobal或AllocCoTaskMem分配的内存块。数据复制：Copy系列方法允许将数据从托管数组复制到非托管内存，或者从非托管内存复制到托管数组中。PtrToStructure和StructureToPtr方法用于将数据从非托管内存块复制到托管结构体，或将托管结构体的数据复制到非托管内存。处理非托管函数指针：GetDelegateForFunctionPointer方法可以将非托管函数指针转换为相应的托管委托。GetFunctionPointerForDelegate方法可以获得与托管委托相对应的非托管函数指针。字符串处理：StringToHGlobalAnsi、StringToHGlobalUni等方法用于将托管字符串转换为非托管内存中的字符串。PtrToStringAnsi、PtrToStringUni等方法用于将非托管内存中的字符串转换为托管字符串。获取非托管类型的大小：SizeOf方法用于获取非托管类型或托管结构体的大小，这在分配非托管内存时非常有用。示例：复制数据到非托管内存以下示例演示了如何使用Marshal类将一个整数数组复制到非托管内存，并最终释放该内存：using System; using System.Runtime.InteropServices; class Program { static void Main() { int[] numbers = { 1, 2, 3, 4, 5 }; int size = Marshal.SizeOf(typeof(int)) * numbers.Length; // 在非托管内存中分配空间 IntPtr ptr = Marshal.AllocHGlobal(size); try { // 将托管数组复制到非托管内存 Marshal.Copy(numbers, 0, ptr, numbers.Length); // 从非托管内存读取数据... } finally { // 释放非托管内存 Marshal.FreeHGlobal(ptr); } } }Marshal类是.NET应用程序中实现托管与非托管代码互操作的关键类，它提供了丰富的API来处理非托管资源，是处理非托管代码的强大工具。
C# 中的垃圾回收（Garbage Collection, GC）机制是自动管理应用程序内存的一个重要组成部分，它自动跟踪和回收不再使用的对象所占用的内存。这一机制使得开发人员不必手动分配和释放内存，降低了内存泄漏的风险，提高了开发效率。以下是C#垃圾回收机制的几个关键方面：堆内存管理C#程序中的对象（即引用类型实例）存储在托管堆（managed heap）上。当创建一个对象时，.NET运行时会为其在堆上分配内存。而值类型，如果作为局部变量，通常存储在栈上；如果作为类或结构体的字段，则同样存储在堆上作为对象的一部分。代际回收.NET的垃圾回收器采用代际回收策略，将堆划分为三个代（Generation 0、1、2），每个代对应不同的回收频率和成本效益比：Generation 0 (Gen 0): 新创建的对象首先被分配在这里。Gen 0的回收最频繁，因为它包含了短生命周期的对象。每次Gen 0回收都会检查并清理不再引用的对象。Generation 1 (Gen 1): 经过一次Gen 0回收仍然存活的对象会被提升到Gen 1。Gen 1的回收频率低于Gen 0。Generation 2 (Gen 2): 经过Gen 1回收仍存活的对象最终进入Gen 2。Gen 2的对象被认为是长寿对象，回收频率最低。垃圾回收触发时机垃圾回收的触发条件包括但不限于：当托管堆空间不足时。当系统资源紧张时。显式调用 GC.Collect() 方法（虽然不推荐，因为GC会根据需要自动执行）。回收过程标记阶段：GC首先执行标记操作，识别出所有可达的对象，即从根对象（如静态变量、线程栈上的局部变量）开始，遍历引用链，标记所有可达的对象。压缩阶段（在某些回收周期中）：回收结束后，为了减少堆碎片，GC可能会执行压缩操作，移动存活的对象，整理堆空间，使其连续。清理阶段：未被标记的对象所占用的内存被回收，释放给堆使用。非确定性延迟垃圾回收的过程是不可预测的，这意味着它可能在任何时刻发生，导致应用程序暂停执行（停顿），这对于实时或高性能应用可能是个问题。.NET提供了低延迟的GC模式（如服务器GC模式）来减轻这个问题。
在C#中，值类型和引用类型在内存管理，特别是垃圾回收（Garbage Collection, GC）方面表现出不同的行为。值类型值类型（如int、float、struct等）存储在栈（stack）上，或者如果它们是类或结构体的成员，则存储在该对象的堆（heap）内存中。值类型的本质特点是它们直接包含数据，而非指向数据的引用。因此，值类型的生命周期通常与其声明的作用域紧密相关：栈上分配: 在局部作用域内声明的小型值类型变量通常直接分配在栈上。栈内存由操作系统自动管理，超出作用域后，变量占用的内存会自动释放，无需GC参与。堆上分配: 如果值类型是类的字段或数组的元素，它们会被分配在堆上。尽管如此，因为它们不是通过引用访问，所以仍然是值类型。堆上的值类型数据会随着包含它的对象一起被垃圾回收，但垃圾回收机制并不直接管理这些值类型的生命周期，而是管理包含它们的对象。引用类型引用类型（如class、interface、数组、委托等）存储在堆上，并且变量本身（即引用）存储在栈上或其它堆对象中。引用类型的关键在于变量存储的是数据的地址，而非数据本身：垃圾回收管理: 垃圾回收器负责追踪和回收不再被使用的堆内存。当一个引用类型的对象不再有根可达（即没有任何变量引用它）时，GC会在某个不确定的时刻回收其占用的内存，以减少内存泄露并回收资源。代际回收: .NET的垃圾回收器将堆分为多个代，新创建的对象位于第0代，经历多次未被回收的对象会逐渐晋升到更高代。不同代别的对象有不同的回收策略，这有助于提高垃圾回收的效率。总结值类型的内存管理更加直接，尤其是栈上分配的值类型，其内存自动随作用域结束而释放，不需要垃圾回收机制介入。引用类型的内存管理依赖于垃圾回收器，GC负责监控堆内存，找出不再使用的对象，并回收它们占用的资源，这一过程相对间接且引入了一定的运行时开销。了解这些差异有助于开发者在设计程序时做出更合理的类型选择，以优化性能和内存使用。
在C#中，struct 是一种值类型，它用于定义轻量级的对象，这些对象通常用于存储少量数据。与引用类型（如类 class）不同，结构体是直接存储在栈上的，或者作为其他类型的一个字段时，存储在包含类型的实例中。这意味着，当你传递或赋值一个结构体时，会复制整个结构体的内容，而不是仅仅复制一个指向对象的引用。这在某些场景下可以提高效率，并避免了引用类型可能导致的副作用。结构体的特点：值类型：结构体是值类型，意味着它们在内存中直接存储其数据，当作为参数传递或赋值时，会创建一份拷贝。默认构造函数：结构体有默认的无参构造函数，用于初始化所有字段为它们的默认值（例如，数字为0，引用类型为null）。可以有构造函数：尽管结构体默认有一个无参构造函数，你也可以定义自己的构造函数，但是需要注意的是，结构体不能有默认的无参数构造函数以外的默认构造函数（即没有参数列表的构造函数），且构造函数必须初始化所有的字段。可以有方法、属性、索引器、事件：与类相似，结构体也可以包含方法、属性、索引器和事件，但通常推荐仅在必要时才这么做，因为结构体主要用于轻量级的数据封装。继承性：结构体不能从另一个结构体或类继承，也不能作为基类被其他类或结构体继承。所有结构体都隐式继承自 System.ValueType，后者又继承自 System.Object。密封性：结构体是密封的，意味着不能被其他类型继承。性能考虑：由于结构体是值类型，所以在栈上分配时速度很快，适合于那些频繁创建和销毁的小型数据结构。但是，如果结构体过大或者包含大型对象（如数组、大字符串等），则可能不如引用类型高效，因为每次复制都会涉及大量数据。装箱和拆箱：当结构体用作对象类型时（例如，存储在Object类型的变量中或作为泛型参数），会发生装箱操作，将结构体转换为对象引用。相反的过程称为拆箱。装箱和拆箱操作会带来性能开销。使用场景：轻量级数据结构：当需要存储一些简单的数据组合，比如坐标、颜色、日期时间等，且不需要复杂的继承或多态行为时，使用结构体更为合适。性能敏感的场景：在对性能要求极高的循环或频繁操作中，使用结构体可以减少垃圾回收的压力。在C#中，struct 和 class 都用于定义对象，但它们之间存在几个关键的区别，主要涉及内存管理、继承、默认值、赋值行为等方面。以下是两者之间的一些核心差异：1. 内存管理struct（结构体）：结构体是值类型，它们通常直接存储在栈上或作为其他类型的一部分存储在堆上。当结构体作为方法的参数传递或被赋值给另一个变量时，会创建结构体内容的一份拷贝。这意味着修改一个结构体变量不会影响到原始数据。class（类）：类是引用类型，它们存储在堆上，而变量通常只包含对对象的引用（一个指针）。当类的对象被传递或赋值时，实际上只是复制了对象的引用，而不是对象本身。因此，多个变量可能指向同一个对象，修改其中一个变量的内容会影响到其他所有指向同一对象的变量。2. 默认值struct：结构体的每个字段都会被初始化为其默认值（如 int 类型默认为0，bool 类型为false，引用类型为null等），即使没有显式初始化。class：类的实例如果没有通过构造函数初始化，其字段将保持未定义状态，且不能直接访问未实例化的类对象。3. 继承struct：结构体不能从另一个结构体或类继承，也不可以被其他结构体或类继承。所有结构体隐式继承自 System.ValueType，而 System.ValueType 继承自 System.Object。class：类可以继承自其他类（除了密封类），并且可以作为基类被其他类继承，支持多层继承和接口的实现。4. 构造函数struct：虽然结构体可以有构造函数，但它们只能有带有参数的构造函数。结构体没有默认的无参构造函数，除非你显式定义一个。此外，调用结构体的构造函数通常是通过初始化语法完成的。class：类既可以有无参数构造函数也可以有带参数的构造函数，并且如果不显式定义任何构造函数，编译器会自动提供一个默认的无参构造函数。5. 性能考量struct：由于结构体是值类型，对于小型数据结构来说，它们的分配和处理通常更快，尤其是在栈上分配时。但在大型结构体或含有大型对象成员的情况下，频繁的
数据绑定在强绑定方式中，需要给控件定义他的Name属性。 在界面后台的cs文件中使用强绑定方式。//BtnContent是ViewModel中的属性，btnOpenFile是界面中的控件，并指定控件需要绑定的依赖属性`` ``this``.OneWayBind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content); 在Reactive UI框架中，提供了单向绑定和双向绑定两种绑定类型，上述代码中的OneWayBind是属于ViewModel->View的单向绑定，另外还有一个API Bind则是双向绑定。this.Bind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content);之所以官方推荐这样的绑定方式，是因为框架中提供了一个管理ViewModel生命周期的API WhenActivated，解决了Xaml弱绑定方式带来的内存泄露的可能性。在WhenActivated API的函数回调中进行绑定属性和Command，可以同步跟踪View和对应绑定属性的生命周期，避免发生内存泄露。 this.WhenActivated(dispos => { this.OneWayBind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content).DisposeWith(dispos); });WhenActivated 会在View被激活时同步调用注册的回调函数，注意，在OneWayBind后面新增了一个API调用DisposeWith,他可以确保当界面被销毁时，对应的viewModel及其绑定的属性和命令也会被销毁。类似的，绑定Commond this.WhenActivated(dispos => { this.OneWayBind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content).DisposeWith(dispos); this.BindCommand(ViewModel, viewModel => viewModel.OpenPage, view => view.openButton) .DisposeWith(disposableRegistration); }); 这样的强绑定相比于Xaml中的弱绑定，会有以下的优势：1.提供了ViewModel的生命周期管理，避免内存泄露。2.控件和后台属性的对应关系更为直观，提高代码的可阅读性。当然也有一定的缺陷，会增加代码量，并且增加View和ViewModel的耦合性。定义属性和命令在MVVMLight中定义一个带通知的属性和Commond： private string content ; public string Content { get { return content; } set { content = value; RaisePropertyChanged(() => Content); } } private RelayCommand openFileCommand = null; public RelayCommand OpenFileCommand { get { return openFileCommand = openFileCommand ?? new RelayCommand(OpenFile); } }在ReactiveUI中也通成功了类似RaisePropertyChanged和RelayCommand功能的API，RaiseAndSetIfChanged和ReactiveCommand。 private string content; public string Content { get { return content; } set { this.RaiseAndSetIfChanged(ref content,value); } } private ReactiveCommand<Unit, Unit> openFileCommand; public ReactiveCommand<Unit, Unit> OpenFileCommand { get { return openFileCommand = openFileCommand ?? ReactiveCommand.Create(() => { }); } }其中ReactiveCommand的两个Unit，前一个是传入参数，后一个是返回值。ReactiveCommand的定义与MVVMLight大同小异。但是在ReactiveUI中，还有更简单方便的定义可通知的属性，使用标记[Reactive]。 [Reactive] public string Content { get; set; }以上代码等价于 private string content; public string Content { get { return content; } set { this.RaiseAndSetIfChanged(ref content,value); } }动态数据集合在.Net中，带通知功能的数据集合一般使用ObservableCollection，但是这个类存在一个限制，不支持多线程操作元素，只能在主线程中增加或者删除元素。所以在多线程操作ObservableCollection的时候，一般都需要通过Dispatcher或者线程上下文来推送操作到UI线程。针对这个问题，ReativeUI框架提供了更优雅的操作方式，SourceList，SourceCache, ObservableCollectionExtended，都是线程安全的集合，需要和ReadOnlyObservableCollection一起搭配使用，用于创建可绑定的线程安全的数据集合。//这是用于View绑定的数据集合 private readonly ReadOnlyObservableCollection<string> _disks; public ReadOnlyObservableCollection<string> Disks => _disks; //这里的ObservableCollectionExtended和SourceList作用相同，都是与_disks强关联并创 //建副本集合，在操作数据的时候，不直接操作_disks或者Disks，而是对DisksSource或 //DisksSource2进行操作，会自动的同步到_disk集合并更新到绑定的UI，而Disks用于界面绑定。 public ObservableCollectionExtended<string> DisksSource; public SourceList<string> DisksSource2; //以下代码是将DiskSource和DiskSource2与_disk建立强关联关系的两种方式 DisksSource = new(); DisksSource.ToObservableChangeSet() .Bind(out _disks) .Subscribe(); DisksSource2 = new SourceList<string>(); DisksSource2.Connect().Bind(out _disks).Subscribe();函数式组合声明以一个读取磁盘文件夹信息的小功能为例。一般都需要定义一个ObservableCollection的Model集合，在子线程中需要通过Dispatcher操作集合。 public ObservableCollection<FolderModel> FolderModels { get; set; } private async Task LoadFolderInfoWithSelectedDiskChanged(string diskName) { await Task.Run(() => { var files = Directory.GetDirectories(diskName); foreach (var fileName in files) { FolderModel folderModel = new FolderModel(); DirectoryInfo directoryInfo = new DirectoryInfo(fileName); folderModel.FolderName = directoryInfo.Name; folderModel.CreateTime = directoryInfo.CreationTime; _dispatcher.Invoke(() => { FolderModels.Add(folderModel); }); } }); }而在ReactiveUI 框架中，不需要Dispatcher这个东西，而是需要通过一个辅助类ObservableAsPropertyHelper。ObservableAsPropertyHelper 是一个简化 IObservable 和 ViewModel 上的属性之间的互操作的类，为一个普通属性/字段和一个IObservable对象之间建立观察者模式的联系。以上代码可以修改成： //当前选中的磁盘符号，是一个IObservable对象 [Reactive] public string SelectedDisk { get; set; } //使用ObservableAsPropertyHelper包装 private readonly ObservableAsPropertyHelper<IEnumerable<FolderModel>> _folderModels; //FolderModels可用于强绑定 public IEnumerable<FolderModel> FolderModels => _folderModels.Value; //将_folderModels和SelectedDisk建立观察者和被观察者联系，构建函数组合式声明，当SelectedDisk改变时， //会自动触发所注册的事件并自动给指定的属性FolderModels赋值。 _folderModels = this.WhenAnyValue(s => s.SelectedDisk) .Where(s => !string.IsNullOrWhiteSpace(s)) .SelectMany(LoadFolderInfoWithSelectedDiskChanged) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, nameof(FolderModels));//将计算后得到的结果赋值到指定的属性中 private async Task<IEnumerable<FolderModel>> LoadFolderInfoWithSelectedDiskChanged(string diskName) { List<FolderModel> folderModels = new List<FolderModel>(); var files = Directory.GetDirectories(diskName); foreach (var fileName in files) { FolderModel folderModel = new FolderModel(); DirectoryInfo directoryInfo = new DirectoryInfo(fileName); folderModel.FolderName = directoryInfo.Name; folderModel.CreateTime = directoryInfo.CreationTime; folderModels.Add(folderModel); } //这个方法中不需要操作FolderModels 只需要把结果返回即可 await Task.CompletedTask; return folderModels; }其中ObservableAsPropertyHelper包装的对象是可以任何对象，而LoadFolderInfoWithSelectedDiskChanged方法必须要带有结果返回的异步方法，这样就构成了函数式声明的异步数据流。
C# 版本 12发布时间：2023 年 11 月C# 12 中增加了以下功能：主构造函数 - 可以创建任意 class 或 struct 类型的主构造函数。集合表达式 - 一种用于指定集合表达式的新语法，包括 spread 元素 (..e)，可展开任何集合。内联数组 - 使用内联数组，你可以创建固定大小的 struct 类型数组。Lambda 表达式中的可选参数 - 可以为 Lambda 表达式的参数定义默认值。ref readonly 参数 - ref readonly 参数可以让可能使用 ref 参数或 in 参数的 API 更清晰。创建任何类型的别名 - 可以使用 using 别名指令创建任何类型的别名，而不仅仅是命名类型。试验性属性 - 指示试验性功能。拦截器 - 已作为预览功能发布。总体而言，C# 12 提供的新功能可让你更高效地编写 C# 代码。 你已经知道的语法可以在更多地方使用。 其他语法可实现相关概念的一致性。C# 版本 11发布时间：2022 年 11 月C# 11 中增加了以下功能：原始字符串字面量泛型数学支持泛型属性UTF-8 字符串字面量字符串内插表达式中的换行符列表模式文件本地类型必需的成员自动默认结构常量 string 上的模式匹配 Span扩展的 nameof 范围数值 IntPtrref 字段和 scoped ref改进了方法组向委托的转换警告波 7C# 11 引入了泛型数学以及支持该目标的几个功能。 可以为所有数字类型编写一次数值算法。 有更多功能可简化 struct 类型处理，例如所需成员和自动默认结构。 使用原始字符串文本、字符串内插中的换行符和 UTF-8 字符串文本可以更轻松地处理字符串。 文件本地类型等功能使源生成器更简单。 最后，列表模式添加了对模式匹配的更多支持。C# 10 版发布时间：2021 年 11 月C# 10 向 C# 语言添加了以下功能和增强功能：记录结构结构类型的改进内插字符串处理程序global using 指令文件范围的命名空间声明扩展属性模式对 Lambda 表达式的改进可使用 const 内插字符串记录类型可密封 ToString()改进型明确赋值在同一析构中可同时进行赋值和声明可在方法上使用 AsyncMethodBuilder 属性CallerArgumentExpression 属性增强的 #line pragma预览模式下提供了更多功能。 为了使用这些功能，需要在项目中将 ` 设置为 Preview`：本文后面的泛型属性。接口中的静态抽象成员。C# 10 继续致力于删除不必要的模式、将数据与算法分离以及提高 .NET 运行时的性能等主题。许多功能意味着可以通过键入更少的代码来表达相同的概念。 记录结构合并了许多记录类所使用的相同方法。 结构和匿名类型支持 with 表达式。 全局 using 指令和文件范围的命名空间声明意味着可以更清楚地表达依赖项和命名空间组织。 进行 Lambda 改进后，就可以在使用 Lambda 表达式时更容易地声明它们。 新的属性模式和析构改进可创建更简洁的代码。新的内插字符串处理程序和 AsyncMethodBuilder 行为可提高性能。 在 .NET 运行时中应用了这些语言功能来实现 .NET 6 中的性能改进。C# 10 还标志着每年 .NET 发布节奏的更多转变。 因为不是每项功能都可以在一年内完成，因此你可以尝试 C# 10 中的几项“预览”功能。 泛型属性和接口中的静态抽象成员均可使用，但这些预览功能在最终发布之前可能会发生更改。C# 9 版发布时间：2020 年 11 月C# 9 随 .NET 5 一起发布。 它是面向 .NET 5 版本的任何程序集的默认语言版本。 它包含以下新功能和增强功能：记录仅限 Init 的资源库顶级语句模式匹配增强：关系模式和逻辑模式性能和互操作性本机大小的整数函数指针禁止发出 localsinit 标志模块初始值设定项分部方法的新功能调整和完成功能目标类型的 new 表达式static 匿名函数目标类型的条件表达式协变返回类型扩展 GetEnumerator 支持 foreach 循环Lambda 弃元参数本地函数的属性C# 9 继续以前版本中的三大主题：删除不必要的模式、将数据与算法分离，以及在更多位置提供更多模式。顶级语句意味着主程序将更易于读取。 减少了不必要的模式：命名空间、Program 类和 static void Main() 都是不必要的。records 的引入为遵循值语义的引用类型提供了简洁的语法，以实现相等性。 使用这些类型来定义那些通常定义最小行为的数据容器。 仅限 init 的资源库提供了在记录中进行非破坏性修改的功能（with 表达式）。 C# 9 还添加了协变返回类型，以便派生记录可以重写虚拟方法，并返回从基方法的返回类型派生的类型。模式匹配功能以多种方式进行了扩展。 数值类型现在支持范围模式。 可以使用 and、or 和 not 模式组合模式。 可以通过添加括号来阐明更复杂的模式：C# 9 包括新的模式匹配改进：类型模式匹配一个与特定类型匹配的对象带圆括号的模式强制或强调模式组合的优先级联合 and 模式要求两个模式都匹配析取 or 模式要求任一模式匹配否定 not 模式要求模式不匹配关系模式要求输入小于、大于、小于等于或大于等于某个给定常数这些模式丰富了模式的语法。 请考虑下列示例：C#public static bool IsLetter(this char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';使用可选的括号来明确 and 的优先级高于 or：C#public static bool IsLetterOrSeparator(this char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';最常见的用途之一是用于 NULL 检查的新语法：C#if (e is not null) { // ... }后面模式中的任何一种都可在允许使用模式的任何上下文中使用：is 模式表达式、switch 表达式、嵌套模式以及 switch 语句的 case 标签的模式。另一组功能支持 C# 中的高性能计算：nint 和 nuint 类型对目标 CPU 上的本机大小整数类型进行建模。函数指针提供类似委托的功能，同时避免创建委托对象所需的分配。localsinit 指令可以省略以保存指令。性能和互操作性另一组改进支持代码生成器添加功能的场景：模块初始化表达式是程序集加载时运行时调用的方法。分部方法支持新的可访问修饰符和非 void 返回类型。 在这些情况下，必须提供一个实现。调整和完成功能C# 9 添加了许多其他小功能，提高了开发人员的工作效率，包括编写和读取代码：目标类型 new 表达式static 匿名函数目标类型的条件表达式扩展 GetEnumerator() 支持 foreach 循环Lambda 表达式可以声明弃元参数特性可应用于本地函数C# 9 版本继续致力于让 C# 成为一种新式通用编程语言。 功能继续支持新式工作负载和应用程序类型。C# 8.0 版发布时间：2019 年 9 月C# 8.0 版是专门面向 .NET C# Core 的第一个主要 C# 版本。 一些功能依赖于新的公共语言运行时 (CLR) 功能，而其他功能则依赖于仅在 .NET Core 中添加的库类型。 C# 8.0 向 C# 语言添加了以下功能和增强功能：Readonly 成员默认接口方法模式匹配增强功能：switch 表达式属性模式元组模式位置模式Using 声明静态本地函数可处置的 ref 结构可为空引用类型异步流索引和范围Null 合并赋值非托管构造类型嵌套表达式中的 Stackalloc内插逐字字符串的增强功能默认接口成员需要 CLR 中的增强功能。 这些功能已添加到 .NET Core 3.0 的 CLR 中。 范围和索引以及异步流需要 .NET Core 3.0 库中的新类型。 在编译器中实现时，可为 null 的引用类型在批注库时更有用，因为它可以提供有关参数和返回值的 null 状态的语义信息。 这些批注将添加到 .NET Core 库中。C# 7.3 版发布时间：2018 年 5 月C# 7.3 版本有两个主要主题。 第一个主题提供使安全代码的性能与不安全代码的性能一样好的功能。 第二个主题提供对现有功能的增量改进。 此外，此版本中还添加了新的编译器选项。以下新增功能支持使安全代码获得更好的性能的主题：无需固定即可访问固定的字段。可以重新分配 ref 本地变量。可以使用 stackalloc 数组上的初始值设定项。可以对支持模式的任何类型使用 fixed 语句。可以使用更多泛型约束。对现有功能进行了以下增强：可以使用元组类型测试 == 和 !=。可以在多个位置使用表达式变量。可以将特性附加到自动实现的属性的支持字段。改进了通过 in 区分参数时进行的方法解析。重载解析的多义情况现在变得更少。新的编译器选项为：-publicsign，用于启用程序集的开放源代码软件 (OSS) 签名。-pathmap用于提供源目录的映射。C# 7.2 版发布时间：2017 年 11 月C# 7.2 版添加了几个小型语言功能：stackalloc 数组上的初始值设定项。对支持模式的任何类型使用 fixed 语句。无需固定即可访问固定的字段。重新分配 ref 本地变量。声明 readonly struct 类型，以指示结构不可变且应作为 in 参数传递到其成员方法。在实参上添加 in 修饰符，以指定形参通过引用传递，但不通过调用方法修改。对方法返回项使用 ref readonly 修饰符，以指示方法通过引用返回其值，但不允许写入该对象。声明 ref struct 类型，以指示结构类型直接访问托管的内存，且必须始终分配有堆栈。使用其他泛型约束。非尾随命名参数：位置参数可以跟在命名参数后面。数值文本中的前导下划线：数值文字现可在任何打印数字前放置前导下划线。private protected 访问修饰符：private protected 访问修饰符允许访问同一程序集中的派生类。条件ref表达式：现在可以引用条件表达式 (?:) 的结果。C# 7.1 版发布时间：2017 年 8 月C# 已开始随 C# 7.1 发布单点发行 。 此版本增加了语言版本选择配置元素、三个新的语言功能和新的编译器行为。此版本中新增的语言功能包括：async`Main` 方法应用程序的入口点可以含有 async 修饰符。default 文本表达式在可以推断目标类型的情况下，可在默认值表达式中使用默认文本表达式。推断元组元素名称在许多情况下，可通过元组初始化来推断元组元素的名称。泛型类型参数的模式匹配可以对类型为泛型类型参数的变量使用模式匹配表达式。最后，编译器有 -refout 和 -refonly 两个选项，可用于控制引用程序集生成。C# 7.0 版发布时间：2017 年 3 月C# 7.0 版已与 Visual Studio 2017 一起发布。 此版本继承和发展了 C# 6.0。 以下介绍了部分新增功能：out 变量元组和析构函数模式匹配本地函数已扩展 expression bodied 成员ref 局部变量引用返回其他功能包括：弃元二进制文本和数字分隔符引发表达式所有这些功能都为开发者提供了新功能，帮助编写比以往任何时候都简洁的代码。 重点是缩减了使用 out 关键字的变量声明，并通过元组实现了多个返回值。 .NET Core 现在面向所有操作系统，着眼于云和可移植性。 语言设计者除了推出新功能外，也会在这些新功能方面付出时间和精力。C# 6.0 版发布时间：2015 年 7 月版本 6.0 随 Visual Studio 2015 一起发布，发布了很多使得 C# 编程更有效率的小功能。 以下介绍了部分功能：静态导入异常筛选器自动属性初始化表达式Expression bodied 成员Null 传播器字符串内插nameof 运算符其他新功能包括：索引初始化表达式Catch/Finally 块中的 Await仅限 getter 属性的默认值如果整体看待这些功能，你会发现一个有趣的模式。 在此版本中，C# 开始消除语言样本，让代码更简洁且更具可读性。 所以对喜欢简洁代码的用户来说，此语言版本非常成功。除了发布此版本，他们还做了另一件事，虽然这件事本身与传统的语言功能无关。 他们发布了 Roslyn 编译器即服务。 C# 编译器现在是用 C# 编写的，你可以使用编译器作为编程工作的一部分。C# 5.0 版发布时间：2012 年 8 月C# 版本 5.0 随 Visual Studio 2012 一起发布，是该语言有针对性的一个版本。 对此版本中所做的几乎所有工作都归入另一个突破性语言概念：适用于异步编程的 async 和 await 模型。 下面是主要功能列表：异步成员调用方信息特性代码工程：C# 5.0 中的调用方信息属性调用方信息特性让你可以轻松检索上下文的信息，不需要采用大量样本反射代码。 这在诊断和日志记录任务中也很有用。但是 async 和 await 才是此版本真正的主角。 C# 在 2012 年推出这些功能时，将异步引入语言作为最重要的组成部分，另现状大为改观。C# 4.0 版发布时间：2010 年 4 月C# 4.0 版随 Visual Studio 2010 一起发布，引入了一些有趣的新功能：动态绑定命名参数/可选参数泛型协变和逆变嵌入的互操作类型嵌入式互操作类型缓解了为应用程序创建 COM 互操作程序集的部署难题。 泛型协变和逆变提供了更强的功能来使用泛型，但风格比较偏学术，应该最受框架和库创建者的喜爱。 命名参数和可选参数帮助消除了很多方法重载，让使用更方便。 但是这些功能都没有完全改变模式。主要功能是引入 dynamic 关键字。 在 C# 4.0 版中引入 dynamic 关键字让用户可以替代编译时类型上的编译器。 通过使用 dynamic 关键字，可以创建和动态类型语言（例如 JavaScript）类似的构造。 可以创建 dynamic x = "a string" 再向它添加六个，然后让运行时理清下一步操作。动态绑定存在出错的可能性，不过同时也为你提供了强大的语言功能。C# 3.0 版发布时间：2007 年 11 月C# 3.0 版和 Visual Studio 2008 一起发布于 2007 年下半年，但完整的语言功能是在 .NET Framework 3.5 版中发布的。 此版本标示着 C# 发展过程中的重大更改。 C# 成为了真正强大的编程语言。 我们来看看此版本中的一些主要功能：自动实现的属性匿名类型查询表达式Lambda 表达式表达式树扩展方法隐式类型本地变量分部方法对象和集合初始值设定项回顾过去，这些功能中大多数似乎都是不可或缺，难以分割的。 它们的组合都是经过巧妙布局。 此 C# 版本的杀手锏功能是查询表达式，也就是语言集成查询 (LINQ)。LINQ 的构造可以建立在更细微的视图检查表达式树、Lambda 表达式以及匿名类型的基础上。 不过无论如何 C# 3.0 都提出了革命性的概念。 C# 3.0 开始为 C# 转变为面向对象/函数式混合语言奠定基础。具体来说，你现在可以编写 SQL 样式的声明性查询对集合以及其他项目执行操作。 无需再编写 for 循环来计算整数列表的平均值，现在可改用简单的 list.Average() 方法。 组合使用查询表达式和扩展方法让各种数字变得智能多了。C# 2.0 版发布时间：2005 年 11 月让我们看看 C# 2.0（2005 年发布）和 Visual Studio 2005 中的一些主要功能：泛型分部类型匿名方法可以为 null 的值类型迭代器协变和逆变除现有功能以外的其他 C# 2.0 功能：getter/setter 单独可访问性方法组转换（委托）静态类委托推断虽然 C# 一开始是通用的面向对象 (OO) 语言，但 C# 2.0 版很快改变了这一点。 通过泛型，类型和方法可以操作任意类型，同时保持类型安全性。 例如，通过 List，将获得 List<string> 或 List<int> 并且可以对这些字符串或整数执行类型安全操作，同时对其进行循环访问。 使用泛型优于创建派生自 ArrayList 的 ListInt 类型，也优于从每个操作的 Object 强制转换。C# 2.0 版引入了迭代器。 简单来说，迭代器允许使用 foreach 循环来检查 List（或其他可枚举类型）中的所有项。 拥有迭代器是该语言最重要的一部分，显著提升了语言的可读性以及人们推出代码的能力。C# 版本 1.2发布时间：2003 年 4 月随 Visual Studio .NET 2003 一起提供的 C# 版本 1.2。 它对语言做了一些小改进。 最值得注意的是，从此版本开始，当 IEnumerator 实现 IDisposable 时，foreach 循环中生成的代码会在 IEnumerator 上调用 Dispose。C# 1.0 版发布时间：2002 年 1 月回想起来，和 Visual Studio .NET 2002 一起发布的 C# 版本 1.0 非常像 Java。 作为 ECMA 既定设计目标的一部分，其目标是成为一种“简单、现代、通用的面向对象的语言”。当时，看起来像 Java 意味着它实现了早期的设计目标。不过如果现在回顾 C# 1.0，你会觉得有点晕。 它没有习以为常的内置异步功能和以泛型为中心的巧妙功能。 其实它完全不具备泛型。 那 LINQ 呢？ 尚不可用。 这些新增内容需要几年才能推出。与现在的 C# 相比，C# 1.0 版少了很多功能。 你会发现自己的代码很冗长。 不过凡事总要有个开始。 在 Windows 平台上，C# 1.0 版是 Java 的一个可行的替代之选。C# 1.0 的主要功能包括：类结构接口事件属性委托运算符和表达式语句特性文章最初发布在 NDepend 博客上，由 Erik Dietrich 和 Patrick Smacchia 提供。
在C#9.0中，引入了record，专门用来以最简的方式创建不可变类型的新方式。如果你需要一个行为像值类型的引用类型，你可以使用record；如果你需要整个对象都是不可变的，且行为像一个值，那么你也可考虑将其声明为一个record类型。 那么什么是record类型？1. Record介绍record类型是一种用record关键字声明的新的引用类型，与类不同的是，它是基于值相等而不是唯一的标识符——对象引用。他有着引用类型的支持大对象、继承、多态等特性，也有着结构的基于值相等的特性。可以说有着class和struct两者的优势，在一些情况下可以用以替代class和struct。提到不可变的类型，我们会想到readonly struct，那么为什么要选择添加一个新的类型，而不是用readonly struct呢？这是因为记录有着如下优点：在构造不可变的数据结构时，它的语法简单易用。record为引用类型，不用像值类型在传递时需要内存分配，并进行整体拷贝。构造函数和结构函数为一体的、简化的位置记录有力的相等性支持 —— 重写了Equals(object), IEquatable, 和GetHashCode()这些基本方法。2.1 record类型的定义与使用2.1.1 常规方式record类型可以定义为可变的，也可以是不可变的。现在，我们用record定义一个只有只读属性的Person类型如下。这种只有只读属性的类型，因为其在创建好之后，属性就不能再被修改，我们通常把这种类型叫做不可变类型。public record Person { public string LastName { get; } public string FirstName { get; } public Person(string first, string last) => (FirstName, LastName) = (first, last); }上面这种声明，在使用时，只能用带参的构造函数进行初始化。要创建一个record对象跟类没有什么区别：Person person = new("Andy", "Kang");如果要支持用对象初始化器进行初始化，则在属性中使用init关键字。这种形式，如果不需要用带参的构造函数进行初始化，可以不定义带参的构造函数，上面的Person可以改为下面形式。public record Person { public string? FirstName { get; init; } public string? LastName { get; init; } }现在，创建Person对象时，用初始化器进行初始化如下：Person person = new() { FirstName = "Andy", LastName = "Kang"};如果需要是可变类型的record，我们定义如下。这种因为有set访问器，所以它支持用对象初始化器进行初始化，如果你想用构造函数进行初始化，你可以添加自己的构造函数。public record Person { public string? FirstName { get; set; } public string? LastName { get; set; } }2.1.2 位置记录 / Positional records为了支持将record对象能解构成元组，我们给record添加解构函数Deconstruct。这种record就称为位置记录。下面代码定义的Person，记录的内容是通过构造函数的参数传入，并且通过位置解构函数提取出来。你完全可以在记录中定义你自己的构造和解构函数（注意不是析构函数）。如下所示：。public record Person { public string FirstName { get; init; } public string LastName { get; init; } public Person(string firstName, string lastName) => (FirstName, LastName) = (firstName, lastName); public void Deconstruct(out string firstName, out string lastName) => (firstName, lastName) = (FirstName, LastName); }针对上面如此复杂的代码，C#9.0提供了更精简的语法表达上面同样的内容。需要注意的是，这种记录类型是不可变的。这也就是为什么有record默认是不可变的说法由来。public record Person(string FirstName, string LastName);该方式声明了公开的、仅可初始化的自动属性、构造函数和解构函数。现在创建对象，你就可以写如下代码：var person = new Person("Mads", "Torgersen"); // 位置构造函数 var (firstName, lastName) = person; // 位置解构函数当然，如果你不喜欢产生的自动属性、构造函数和解构函数，你可以自定义同名成员代替。在调用构造函数和解构函数时，将会只使用你自定义的那个。在这种情况下，被自定义参数处于你用于初始化的作用域内，例如，你想让FirstName是个保护属性：public record Person(string FirstName, string LastName) { protected string FirstName { get; init; } = FirstName; }如上例子所示，对位置记录进行扩展，你可以在大括号里添加你想要的任何成员。一个位置记录可以像下面这样调用父类构造函数。public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);2.1.3 小结record默认情况下是被设计用来进行描述不可变类型的，因此位置记录这种短小简明的声明方式是推荐方式。2.2 with表达式当使用不可变的数据时，一个常见的模式是从现存的值创建新值来呈现一个新状态。例如，如果Person打算改变他的姓氏（last name），我们就需要通过拷贝原来数据，并赋予一个不同的last name值来呈现一个新Person。这种技术被称为非破坏性改变。作为描绘随时间变化的person，record呈现了一个特定时间的person的状态。为了帮助进行这种类型的编程，针对records就提出了with表达式，用于拷贝原有对象，并对特定属性进行修改：var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; var otherPerson = person with { LastName = "Torgersen" };如果只是进行拷贝，不需要修改属性，那么无须指定任何属性修改，如下所示：Person clone = person with { };with表达式使用初始化语法来说明新对象在哪里与原有对象不同。with表达式实际上是拷贝原来对象的整个状态值到新对象，然后根据对象初始化器来改变指定值。这意味着属性必须有init或者set访问器，才能用with表达式进行更改。需要注意的是：with表达式左边操作数必须为record类型。record的引用类型的成员在拷贝的时候，只是将所指实例的引用进行了拷贝。2.3 record的面向对象的特性——继承、多态等记录（record)和类一样，在面向对象方面，支持继承，多态等所有特性。除过前面提到的record专有的特性，其他语法写法跟类也是一样。同其他类型一样，record的基类依然是object。要注意的是：记录只能从记录继承，不能从类继承，也不能被任何类继承。record不能定义为static的，但是可以有static成员。下面一个学生record，它继承自Person：public record Person { public string? FirstName { get; init; } public string? LastName { get; init; } } public sealed record Student : Person { public int ID { get; init; } }对于位置记录，只要保持record特有的写法即可：public record Person(string FirstName, string LastName); public sealed record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName); public sealed record Teacher(string FirstName, string LastName, string Title) : Person(FirstName, LastName) { public override string ToString() { StringBuilder s = new(); base.PrintMembers(s); return $"{s.ToString()} is a Teacher"; } }with表达式和值相等性与记录的继承结合的很好，因为他们不仅是静态的已知类型，而且考虑到了整个运行时对象。比如，我创建一个Student对象，将其存在Person变量里。Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };with表达式仍然拷贝整个对象并保持着运行时的类型：var otherStudent = student with { LastName = "Torgersen" }; WriteLine(otherStudent is Student); // true同样地，值相等性确保两个对象有着同样的运行时类型，然后比较他们的所有状态：Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 }; WriteLine(student != similarStudent); //true, 由于ID值不同2.4 record实现原理从本质上来讲，record仍然是一个类，但是关键字record赋予这个类额外的几个像值的行为。也就是，当你定义了record时候，编译器会自动生成以下方法，来实现基于值相等的特性（即只要两个record的所有属性都相等，且类型相同，那么这两个record就相等）、对象的拷贝和成员及其值的输出。基于值相等性的比较方法，如Equals,==,!=,EqualityContract等。重写GetHashCode()拷贝和克隆成员PrintMembers和ToString()方法例如我先定义一个Person的记录类型：public record Person(string FirstName, string LastName);编译器生成的代码和下面的代码定义是等价的。但是要注意的是，跟编译器实际生成的代码相比，名字的命名是有所不同的。public class Person : IEquatable<Person> { private readonly string _FirstName; private readonly string _LastName; protected virtual Type EqualityContract { get { return typeof(Person); } } public string FirstName { get { return _FirstName; } init { _FirstName = value; } } public string LastName { get { return _LastName; } init { _LastName = value; } } public Person(string FirstName, string LastName) { _FirstName = FirstName; _LastName = LastName; } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Person"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("FirstName"); builder.Append(" = "); builder.Append((object)FirstName); builder.Append(", "); builder.Append("LastName"); builder.Append(" = "); builder.Append((object)LastName); return true; } public static bool operator !=(Person r1, Person r2) { return !(r1 == r2); } public static bool operator ==(Person r1, Person r2) { return (object)r1 == r2 || ((object)r1 != null && r1.Equals(r2)); } public override int GetHashCode() { return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(_FirstName)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(_LastName); } public override bool Equals(object obj) { return Equals(obj as Person); } public virtual bool Equals(Person other) { return (object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(_FirstName, other._FirstName) && EqualityComparer<string>.Default.Equals(_LastName, other._LastName); } public virtual Person Clone() { return new Person(this); } protected Person(Person original) { _FirstName = original._FirstName; _LastName = original._LastName; } public void Deconstruct(out string FirstName, out string LastName) { FirstName = this.FirstName; LastName = this.LastName; } }这些由编译器生成的一些成员，是允许编程人员自定义的，一旦编译器发现有自定义的某个成员，它就不会再生成这个成员。由此可见，record实际上就是编译器特性，并且records由他们的内容来界定，不是他们的引用标识符。从这一点上讲，records更接近于结构，但是他们依然是引用类型。2.4.1 基于值的相等所有对象都从object类型继承了 Equals(object)，这是静态方法 Object.Equals(object, object) 用来比较两个非空参数的基础。结构重写了这个方法，通过递归调用每个结构字段的Equals方法，从而有了“基于值的相等”。Recrods也是这样。这意味着只要他们的值保持一致，两个record对象可以不是同一个对象实例就会相等。例如我们将修改的Last name又修改回去了：var originalPerson = otherPerson with { LastName = "Nielsen" };现在我们会得到 ReferenceEquals(person, originalPerson) = false (他们不是同一对象)，但是 Equals(person, originalPerson) = true (他们有同样的值).。与基于值的Equals一起的，还伴有基于值的GetHashCode()的重写。另外，records实现了IEquatable并重载了==和 !=这两个操作符，这些都是为了基于值的行为在所有的不同的相等机制方面保持一致。基于值的相等和可变性契合的不总是那么好。一个问题是改变值可能引起GetHashCode的结果随时变化，如果这个对象被存放在哈希表中，就会出问题。我们没有不允许使用可变的record，但是我们不鼓励那样做，除非你已经想到了后果。如果你不喜欢默认Equals重写的字段与字段比较行为，你可以进行重写。你只需要认真理解基于值的相等时如何在records中工作原理，特别是涉及到继承的时候。除了熟悉的Equals，==和！=操作符之外，record还多了一个新的EqualityContract只读属性，该属性返回类型是Type类型，返回值默认为该record的类型。该属性用来在判断两个具有继承关系不同类型的record相等时，该record所依据的类型。下面我们看一个有关EqualityContract的例子，定义一个学生record，他继承自Person：public record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName);这个时候，我们分别创建一个Person和Student实例，都用来描述同样的人：Person p = new Person("Jerry", "Kang"); Person s = new Student("Jerry", "Kang", 1); WriteLine(p == s); // False这两者比较的结果是False，这与我们实际需求不相符。那么我们可以重写EqualityContract来实现两种相等：public record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName) { protected override Type EqualityContract { get => typeof(Person); } }经过此改造之后，上面例子中的两个实例就会相等。EqualityContract的修饰符是依据下面情况确定的：如果基类是object, 属性是virtual；如果基类是另一个record类型，则该属性是override；如果基类类型是sealed，则该属性也是sealed的。2.4.2 拷贝克隆与with表达式一个record在编译的时候，会自动生成一个带有保护访问级别的“拷贝构造函数”，用来将现有record对象的字段值拷贝到新对象对应字段中：protected Person(Person original) { /* 拷贝所有字段 */ } // 编译器生成with表达式就会引起拷贝构造函数被调用，然后应用对象初始化器来有限更改属性相应值。如果你不喜欢默认的产生的拷贝构造函数，你可以自定义该构造函数，编译器一旦发现有自定义的构造函数，就不会在自动生成，with表达式也会进行调用。public record Person(string FirstName, string LastName) { protected Person(Person original) { this.FirstName = original.FirstName; this.LastName = original.LastName; } }编译器默认地还会生成with表达式会使用的一个Clone方法用于创建新的record对象，这个方法是不能在record类型里面自定义的。2.4.3 PrintMembers和ToString()方法如果你用Console.WriteLine来输出record的实例，就会发现其输出与用class定义的类型的默认的ToString完全不同。其输出为各成员及其值组成的字符串：Person {FirstName = Andy, LastName = Kang}这是因为，基于值相等的类型，我们更加关注于具体的值的情况，因此在编译record类型时会自动生成重写了ToString的行为的代码。针对record类型，编译器也会自动生成一个保护级别的PrintMembers方法，该方法用于生成各成员及其值的字符串，即上面结果中的大括号内部的内容。ToString中，就调用了PrintMembers来生成其成员字符串部分，其他部分在ToString中补充。我们也可以定义PrintMembers和重写ToString方法来实现自己想要的功能，如下面实现ToString输出为Json格式：public record Person(string FirstName, string LastName) { protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("\"FirstName\""); builder.Append(" : "); builder.Append($"\"{ FirstName}\""); builder.Append(", "); builder.Append("\"LastName\""); builder.Append(" : "); builder.Append($"\"{ LastName}\""); return true; } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{"); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } }record因为都是继承自Object，因此ToString都是采用override修饰符。而PrintMembers方法修饰符是依据下面情况决定的：如果记录不是sealed而是从object继承的, 该方法是protected virtual；如果记录基类是另一个record类型，则该方法是protected override；如果记录类型是sealed，则该方法也是private的。3 应用场景3.1 WebAPI用于web api返回的数据，通常作为一种一次性的传输型数据，不需要是可变的，因此适合使用record。3.2 并发和多线程计算作为不可变数据类型record对于并行计算和多线程之间的数据共享非常适合，安全可靠。3.3 数据日志record本身的不可变性和ToString的数据内容的输出，不需要人工编写很多代码，就适合进行日志处理。3.4 其他其他涉及到有大量基于值类型比较和复制的场景，也是record的常用的使用场景。4 结束语在生产应用中，有着众多的使用场景，以便我们用record来替换写一个类。未知的还在等我们进一步探索。