下文将倒序介绍C#各个版本的新特性以方便熟悉旧版本语言的读者。
C# 10随Visual Studio 2022发布。C# 10.0可用于目标运行时为.NET 6的程序。
通常情况下,一个文件中的所有类型都会定义在单一命名空间中。因此,C# 10中的文件范围命名空间能够有效地避免代码混乱同时消除多余的缩进:
在 using 指令前添加 global 关键字可以将该指令应用到当前工程下的所有文件中:
global using 指令可以避免在每一个文件中重复书写相同的指令。此外, global using 指令也支持 using static 写法。
除了全局using指令外,.NET 6工程还支持隐式全局using指令。当工程文件中的 ImplicitUsings 元素的值为true时。编译器将自动(根据SDK工程类型)导入最常见的命名空间。请参见2.12.3节。
C# 9使用 with 关键字在record上执行非破坏性更改。在C# 10中,这种做法同样适用于匿名对象:
C# 7可以在元组或任意实现了 Deconstruct 方法的类型上使用解构语法。而C# 10能够在解构时同时进行赋值和声明:
从C# 10开始,我们可以在结构体(请参见3.4)中引入字段初始化器与无参构造器。当然,这些功能只在显式调用构造器的情况下才会生效,因此我们可以轻易地越过这些机制——例如,使用 default 关键字。该功能主要用于record struct类型。
C# 9引入了record,它是一种由编译器增强的类(class)。而在C# 10中,record也支持 struct :
两类record的规则是相似的:record struct 与class struct 的功能相近(请参见4.12节),但编译器为record struct 生成的属性是可写的。如果需生成只读属性,则需要在record声明之前辅以 readonly 关键字。
C# 10对Lambda表达式的语法进行了多项增强。首先,可以使用 var 隐式类型声明Lambda表达式:
Lambda表达式的隐式类型或为 Action 委托或为 Func 委托。因此本例中 greeter 的类型为 Func<string> 。而对于含有参数的表达式则必须显式指定每一个参数的类型:
其次,Lambda表达式可以指定返回类型:
这种举措主要是为了改善编译器处理复杂嵌套表达式的性能问题。
再次,我们可以将Lambda表达式传递到参数类型为 object 、 Delegate 或 Expression 的方法中。
最后,我们可以在Lambda表达式编译生成的方法上添加特性(除此之外,还可以在其参数和返回值上添加特性):
有关Lambda表达式特性的更多细节,请参见4.14.4节。
C# 10支持嵌套属性模式匹配(请参见4.13.6节),因此以下的语法是合法的:
上述语法等价于:
若在方法的参数上应用 [CallerArgumentExpression] 特性,则(编译器)将捕获方法调用者的参数表达式,并将其传递给该参数:
该特性主要用于验证库和断言库的开发(请参见4.15.1节)。
C# 10增强了 #line 预处理指令的功能。现在我们可以在该指令中指定列与范围。
在C# 10中,如果字符串插值中的值为常量(字符串),则插值后的字符串仍然可以是常量。
在C# 10中,record可以将 ToString() 方法标记为seal的,以便派生类能够使用相同的表示形式。
C#改进了确定性赋值 的分析方式,因此类似以下代码的表达式是合法的:
[在C# 10以前的编译器中编译上述代码将得到:“Use of unassigned local variable ‘number’”(变量‘number’在使用前未被赋值)错误。]
C# 9.0随Visual Studio 2019一同发布,可用于目标运行时为.NET 5的程序。
我们可以使用顶级语句(请参见2.3.2.7节)直接编写程序而无须将代码包裹在 Program 类的 Main 方法中:
顶级语句可以包含方法(该方法将作为局部方法使用)。同时也可以访问“神奇的” args 变量,并将值返回给调用者。我们可以在顶级语句之后声明类型和命名空间。
属性声明中只用于初始化的set访问器(请参见3.1.8.6节)使用 init 关键字而不是 set 关键字:
上述属性的行为与只读属性类似,只不过这种属性可以在对象初始化器中赋值:
该访问器可用于创建不可变(只读)类型,并使用对象初始化器(而不是构造器)进行初始化。同时,这种做法还可以避免出现构造器中接收大量可选参数的反模式。结合使用该访问器和“记录”(record)功能还可以实现非破坏性更改(nondesctructive mutation)。
记录(请参见4.12节)是一种可以良好支持不可变数据的特殊类型。它最特别的功能是可以使用一种新的关键字( with )进行非破坏性更改:
在简单的情况下,记录可以避免大量用于定义属性、编写构造器和解构器的样板代码。例如,上述 Point 记录的定义可以替换为以下语句而不会损失任何功能:
和元组相似,记录也默认进行结构化相等比较。记录可以继承其他记录,并且具备和类相同的内部结构。编译器在运行时也会将记录实现为类。
关系模式功能(请参见4.13节)允许在模式中使用 < 、 > 、 <= 和 >= 运算符:
模式连接符功能可以使用三个新关键字( and 、 or 与 not )将模式组合起来:
和 && 与 || 一样, and 比 or 有更高的优先级。我们可以通过添加括号更改运算顺序。
not 连接符可以和类型模式一起使用,以测试对象是否不是某种类型:
使用C# 9创建对象时,若编译器可以无二义性地推断目标类型,则可以省略类型名称:
上述方式在代码变量声明和初始化分离的情况下非常有用:
在以下场景中也同样适用:
更多信息,请参见2.8.8节。
C# 9引入了函数指针(请参见4.18.9节与24.3.1节)。其主要目的是允许非托管代码在C#中调用静态方法而无须承担委托实例的开销,当参数与返回类型可以在两侧直接进行位块传输(即在两侧的表示方法相同)时绕过P/Invoke层。
C# 9还引入了 nint 和 nuint 两种原生大小的整数类型(请参见4.18.8节)。这两种类型在运行时将分别映射为 System.IntPtr 和 System.UIntPtr 类型。在编译期,它们的行为与数字类型相同,并可以使用算术运算符。
此外,C# 9还可以:
· 重写方法或只读属性,并返回更深的派生类型(请参见3.2.3.1节)。
· 在局部函数上应用特性(请参见4.14节)。
· 在Lambda表达式或局部函数上使用 static 关键字以确保不会意外地捕获局部变量或实例变量(请参见4.3.2.1节)。
· 通过编写 GetEnumerator 扩展方法令所有类型都支持 foreach 语句。
· 在static void的无参数方法上应用 [ModuleInitializer] 特性定义模块初始化器方法,使其仅在程序集第一次加载时执行一次。
· 使用丢弃变量(下划线符号)作为Lambda表达式的参数。
· 编写必须要求实现的扩展分部方法。它的使用场景如Roslyn编译器的新代码生成器(请参见3.1.13.2节)。
· 在方法、类型或模块上应用特性以阻止局部变量被运行时初始化(请参见4.18.10节)。
C# 8.0最初随Visual Studio 2019发布,目前仍然可以在目标运行时为.NET Core 3或.NET Standard 2.1的程序中使用。
索引(index)与范围(range)简化了访问数组元素或访问部分数组(或者诸如 Span<T> 以及 ReadOnlySpan<T> 等低层次类型)的工作。
在索引中使用 ^ 运算符可以从数组的结尾处开始引用数组的元素。例如, ^1 引用最后一个元素,而 ^2 则引用倒数第二个元素,以此类推:
范围指的是在数组中可以使用 .. 运算符将数组切片:
C#通过 Index 和 Range 这两种类型实现了上述索引和范围操作:
当然,我们也可以在自定义类型中使用 Index 和 Range 参数来支持索引和范围操作:
关于索引和范围的详细介绍,请参见2.7.2节。
??= 运算符会在值为null的时候执行赋值操作。因而如下的语句:
可以写为
若忽略 using 语句的括号和语句块,那么这条语句就变成了using声明。当程序执行超越该声明所在的语句块时,将调用相应资源的 Dispose 方法:
在上述代码中,当代码执行超出 if 语句的代码块时,将调用 reader 的 Dispose 方法。
C# 8支持对struct中的函数附加 readonly 修饰符,以确保在该函数试图修改任何字段时产生编译错误:
如果 readonly 函数调用一个非 readonly 函数,则编译器会生成警告(并防御性地创建一个该值类型对象的拷贝以避免产生更改)。
在局部方法中添加 static 修饰符可以避免该方法使用其所在的外层方法中的局部变量和参数。这不但能够降低耦合,还能够在局部方法中随心所欲地定义变量而不必担心和所在的外层方法中定义的变量冲突。
C# 8可以在接口成员中添加默认实现,这样就无须每次都实现该成员:
这意味着即使在接口中添加了新的方法也不会破坏现有的实现。默认的实现必须通过显示接口类型才能进行调用:
现在还可以在接口中定义静态成员(包括静态字段),而接口的默认实现可以访问这些成员:
此外,也可以从接口外访问这些静态成员。除非这些静态接口成员被访问修饰符(例如 private 、 protected 或 internal )限制:
接口无法定义实例字段。关于该内容的详细描述,请参见3.6.6节。
C# 8支持在表达式上下文中使用 switch 语句:
更多的示例,请参见2.11.3.6节。
C# 8支持三种新的模式,这些模式对switch语句和switch表达式(请参见4.13节)均有裨益。元组模式可以直接对多个值进行分支选择:
位置模式和对象的解构器类似,而属性模式可用于匹配对象的属性值。这三种模式均可以用于switch语句和 is 运算符。以下示例使用属性模式来确认 obj 是否是一个长度为4的字符串:
可空值类型令值类型对象也可以为null,而可空引用类型则正好做了相反的事情。它在一定程度上防止了引用类型对象的值为null,从而避免 NullReferenceException 的出现。可空引用类型完全依靠编译器在发现可能产生 NullReferenceException 的代码时产生警告或错误,从而提供一定的安全保障。
可空引用类型特性可以在工程级别(通过.csproj工程文件中的 Nullable 元素)进行配置,也可以在代码级别(使用 #nullable 指令)进行配置。当该特性处于开启状态时,编译器将默认引用值不可为null。如果想让一个引用类型变量接受null,则必须使用 ? 后缀对其进行修饰,以声明该变量为可空引用类型:
(未标记为可空引用类型的)未经初始化的字段将产生编译警告。此外,在解引用一个可空引用类型的变量时,若编译器认为该操作会产生 NullReferenceException ,则也将产生编译警告:
若想消除该警告,则需要使用允许空值运算符( ! ):
有关可空引用类型的完整讨论,请参见4.8节。
在C# 8之前,我们可以使用 yield return 来书写迭代器,或使用 await 来书写异步函数。但是无法同时使用两者来实现一个异步生成数据的迭代器。C# 8通过引入异步流弥补了这个遗憾:
可以使用 await forreach 语句来消费该异步流:
关于这个专题的更多信息,请参见14.5.4节。
C# 7最初随Visual Studio 2017发布。在Visual Studio 2019中,若程序的目标运行时为.NET Core 2、.NET Framework 4.6~4.8或.NET Standard 2.0,则仍然可以使用C# 7.3。
C# 7.3在先前的功能上做了微小的改进,例如,使用相等运算符和不等运算符对元组进行比较、改进重载解析,以及可以在自动属性对应的字段上附加特性:
C# 7.3在C# 7.2先进的底层内存分配编程特性之上添加了对引用局部变量(ref local)的重复赋值功能、对 fixed 字段进行索引操作时无须固定内存的功能,以及使用stackalloc初始化字段的功能:
需要注意的是栈分配的内存可以直接赋值给 Span<T> 对象,我们将在第23章来介绍 Span<T> 及其使用场景。
C# 7.2添加了 private protected 修饰符(它是 internal 和 protected 的交集),在调用方法时支持占位的命名参数,此外还添加了 readonly 结构体。 readonly 结构体的所有的字段都是 readonly 的,这不但令其声明上十分清晰而且还为编译器提供了更多的优化空间:
C# 7.2还进行了一些细微的性能优化并添加了一些底层资源分配编程相关的功能。具体请参见2.8.4.6节、2.8.5节、2.8.6节以及3.4.3节。
C# 7.1可以使用 default 关键字在能够推断类型的情况下忽略类型信息,例如:
C# 7.1放宽了对switch语句匹配规则的约束(现在,可以对泛型类型参数实施模式匹配了),支持将程序的 Main 函数声明为异步函数,并支持元组中元素名称的推断功能:
在C# 7中,数字字面量可以使用下划线来改善可读性。这些称为数字分隔符,编译器会忽略它们:
二进制字面量可以使用 0b 前缀进行标识:
在C# 7中,调用含有out参数的方法将更加容易。首先,可以非常自然地声明输出变量(请参见2.8.4.4节):
当调用含有多个out参数的方法时,可以使用下划线字符忽略你并不关心的参数:
is 运算符也可以自然地引入变量了。它们称为模式变量(请参见3.2.2.5节):
switch 语句同样支持模式,因此我们不仅可以按常量switch,还可以按类型switch(请参见2.11.3.5节)。可以使用 when 子句来指定判断条件或是直接选择null:
局部方法是声明在其他函数内部的方法(请参见3.1.3.2节)
局部方法仅仅在包含它的函数内可见,它们可以像Lambda表达式那样捕获局部变量。
C# 6引入了以“胖箭头”语法表示的表达式体方法、只读属性、运算符以及索引器。而C# 7更将其扩展到了构造函数、读/写属性和终结器中:
C# 7引入了解构器模式(请参见3.1.5节)。构造器一般接受一系列值(作为参数)并将它们赋值给字段,而解构器则正相反,它将字段反向赋值给变量。以下示例为 Person 类书写了一个解构器(不包含异常处理):
解构器以特定的语法进行调用:
也许对于C# 7来说最值得一提的改进当属显式的元组(tuple)支持(请参见4.11节)。元组提供了一种简单方式来存储一系列相关值:
C#的新元组实质上是使用 System.ValueTuple<...> 泛型结构的语法糖。在编译器“魔法”的帮助下,我们还可以对元组的元素进行命名:
有了元组,函数再也不必通过一系列out参数或通过多余的类型包装来返回多个值了:
元组隐式支持解构模式,因此很容易解构为若干独立的变量。因此,可将元组轻易地解构为两个独立局部变量 row 和 column :
在C# 7之前, throw 一直是语句。现在,它也可以作为表达式出现在表达式体函数中:
throw 表达式也可以出现在三元条件表达式中:
C# 6.0随Visual Studio 2015发布,随之一起发布的有崭新的、完全使用C#实现的代号为“Roslyn”的编译器。新的编译器将一整条编译流水线通过程序库进行开放,从而实现对任意的源代码的分析。编译器本身是开源的,其源代码可以从GitHub( http://github.com/dotnet/rosly n)上获得。
此外,C# 6.0为了改善代码的清晰性引入了一系列小而精的改进。
null条件(“Elvis”)运算符(请参见2.10节)可以避免在调用方法或访问类型的成员之前显式地编写null判断的语句。在以下示例中, result 将会为null而不会抛出 NullReferenceException :
表达式体函数(expression-bodied function,请参见3.1.3节)可以以Lambda表达式的形式书写仅仅包含一个表达式的方法、属性、运算符以及索引器,使代码更加简短:
属性初始化器(property initializer,参见第3章)可以对自动属性进行初始赋值:
这种初始化也支持只读属性:
只读属性也可以在构造器中进行赋值,这令创建不可变(只读)类型变得更加容易了。
索引初始化器(index initializer,见第4章)可以一次性初始化具有索引器的任意类型:
字符串插值(string interploation,参见2.6.2节)用更加简单的方式替代了 string.Format :
异常过滤器(exception filter,请参见4.5节)可以在catch块上再添加一个条件:
using static (参见2.12节)指令可以引入一个类型的所有静态成员,这样就可以不用书写类型而直接使用这些成员:
nameof (参见第3章)运算符返回变量、类型或者其他符号的名称,这样在Visual Studio中就可以避免变量重命名造成不一致的代码:
最后值得一提的是,C# 6.0可以在 catch 和 finally 块中使用 await 。
C# 5.0最大的新特性是通过关键字 async 和 await 支持异步功能(asynchronous function)。异步功能支持异步延续(asynchronous continuation),从而简化响应式和线程安全的富客户端应用程序的编写。它还有利于编写高并发和高效的I/O密集型应用程序,而不需要为每一个操作绑定一个线程资源。第14章将详细介绍异步功能。
C# 4.0引入了四个主要的功能增强:
动态绑定(参见第4章和第19章)将绑定过程(解析类型与成员的过程)从编译时推迟到运行时。这种方法适用于一些需要避免使用复杂反射代码的场合。动态绑定还适合于实现动态语言以及COM组件的互操作。
可选参数(参见第2章)允许函数指定参数的默认值,这样调用者就可以省略一些参数,而命名参数则允许函数的调用者按名字而非按位置指定参数。
类型变化规则在C# 4.0进行了一定程度的放宽(参见第3章和第4章),因此泛型接口和泛型委托类型参数可以标记为协变(covariant)或逆变(contravariant),从而支持更加自然的类型转换。
COM互操作性(参见第24章)在C# 4.0中进行了三个方面的改进。第一,参数可以通过引用传递,并无须使用 ref 关键字(特别适用于与可选参数一同使用)。第二,包含COM互操作(interop)类型的程序集可以链接而无须引用。链接的互操作类型支持类型相等转换,无须使用主互操作程序集(Primary Interop Assembly),并且解决了版本控制和部署的难题。第三,链接的互操作类型中的函数若返回COM变体类型,则会映射为 dynamic 而不是 object ,因此无须进行强制类型转换。
C# 3.0增加的特性主要集中在语言集成查询(Language Integrated Query,LINQ)上。LINQ令C#程序可以直接编写查询并以静态方式检查其正确性。它可以查询本地集合(如列表或XML文档),也可以查询远程数据源(如数据库)。C# 3.0中和LINQ相关的新特性还包括隐式类型局部变量、匿名类型、对象构造器、Lambda表达式、扩展方法、查询表达式和表达式树。
隐式类型局部变量( var 关键字,参见第2章)允许在声明语句中省略变量类型,然后由编译器推断其类型。这样可以简化代码并支持匿名类型(参见第4章)。匿名类型是一些即时创建的类,它们常用于生成LINQ查询的最终输出结果。数组也可以隐式类型化(参见第2章)。
对象初始化器(参见第3章)允许在调用构造器之后以内联的方式设置属性,从而简化对象的构造过程。对象初始化器不仅支持命名类型也支持匿名类型。
Lambda表达式(参见第4章)是由编译器即时创建的微型函数,适用于创建“流畅的”LINQ查询(参见第8章)。
扩展方法(参见第4章)可以在不修改类型定义的情况下使用新的方法扩展现有类型,使静态方法变得像实例方法一样。LINQ表达式的查询运算符就是使用扩展方法实现的。
查询表达式 (参见第8章)提供了编写LINQ查询的更高级语法,大大简化了具有多个序列或范围变量的LINQ查询的编写过程。
表达式树(参见第8章)是赋值给一种特殊类型 Expression<TDelegate> 的Lambda表达式的DOM(Document Object Model,文档对象模型)模型。表达式树使LINQ查询能够远程执行(例如在数据库服务器上),因为它们可以在运行时进行内省和转换(例如,变成SQL语句)。
C# 3.0还添加了自动化属性和分部方法。
自动化属性(参见第3章)简化了在 get/set 中对私有字段直接读写的属性,并将字段的读写逻辑交给编译器自动生成。分部方法(Partial Method,参见第3章)可以令自动生成的分部类(Partial Class)自定义需要手动实现的钩子函数,而该函数可以在没有使用的情况下“消失”。
C# 2.0提供的新特性包括泛型(参见第3章)、可空值类型(nullable type)(参见第4章)、迭代器(参见第4章)以及匿名方法(Lambda表达式的前身)。这些新特性为C# 3.0引入LINQ铺平了道路。
C# 2.0还添加了分部类、静态类以及许多细节功能,例如,对命名空间别名、友元程序集和定长缓冲区的支持。
泛型需要在运行时仍然能够确保类型的正确性,因此需要引入新的CLR(CLR 2.0)才能达成该目标。