各版本新特性

2.0 #

添加了泛型、可空值类型、迭代器以及匿名方法(lambda表达式的前身)。这些新特性为C#3引入LINQ铺平了道路。

C#2还添加了分布区、静态类以及许多细节功能,例如对命名空间别名限定符、友元程序集和定长缓冲区的支持。

泛型需要在运行时仍然能够确保类型的正确性,因此需要引入新的CLR(CLR2.0)才能实现泛型。

3.0 #

C#3.0增加的特性主要集中在语言集成查询(Language-Integrated Query, LINQ)上。

LINQ令C#程序可以直接编写查询并以静态方式检查其正确性。它可以查询本地集合(如列表或XML文档),也可以查询远程数据源(如数据库)。C#3.0中和LINQ相关的新特性还包括隐式类型局部变量、匿名类型、对象初始化器、lambda表达式、扩展方法、查询表达式和表达式树。

  • 隐式类型局部变量(var关键字)允许在声明语句中省略变量类型,然后由编译器推断其类型。这样可以简化代码并支持匿名类型。匿名类型是一些即时创建的类,它们常用于生成LINQ查询的最终输出结果。数组也可以隐式类型化。
  • 对象初始化器允许在调用构造器之后以内联的方式设置属性,从而简化对象的构造过程。对象初始化器既支持命名类型也支持匿名类型。
  • Lambda表达式是由编译器即时创建的微型函数,适用于创建“流畅的”LINQ查询。
  • 扩展方法可以在不修改类型定义的情况下使用新的方法扩展现有类型,使静态方法变得像实例方法一样。LINQ表达式的查询运算符就是使用扩展方法实现的。
  • 查询表达式提供了编写LINQ查询的更高级语法,大大简化了具有多个序列或范围变量的LINQ查询的编写过程。
  • 表达式树是赋值给一种特殊类型Expression的Lambda表达式的微型代码文档对象模型(Document Object Model, DOM)。表达式树使LINQ查询能够远程执行(例如在数据库服务器上),因为它们可以在运行时进行转换和翻译(例如变成SQL语句)。                     

C#3.0还添加了自动属性和分布方法。

自动属性对在get/set中对私有手段直接读写的属性进行了简化,并将字段的读写逻辑交给编译器自动生成。分布方法(partial method)可以令自动生成的分布类(partial class)自定义需要手动实现的钩子函数,而该函数可以在未被使用的情况下“消失”。

4.0 #

C#4.0引入了四个主要功能增强:

动态绑定 将绑定过程(解析类型与成员的过程)从编译时推迟到运行时。这种方法适用于一些需要避免使用复杂反射代码的场合。动态绑定还适用于实现动态语言以及COM组件的互操作。

可选参数 允许函数指定参数的默认值,这样调用者就可以省略一些参数,而命名参数则允许函数的调用者按名字而非位置指定参数。

类型变化规则在C#4.0中进行了一定程度的放宽,因此泛型接口和泛型委托类型参数可以标记为协变(covariant)或者逆变(contravariant),从而支持更加自然的类型转换。

COM互操作性 在C#4.0中进行了三个方面的改进。第一,参数可以通过引用传递,并无需使用ref关键字(特别适合与可选参数一同使用)。第二,包含COM互操作类型的程序集可以链接而无需引用。连接的互操作类型支持类型等价转换,无需使用主互操作程序集(primary interop assembly),并且解决了版本控制和部署的难题。第三,链接的互操作类型中的函数若返回COM变体类型,则会映射为dynamic而不是object,因此无需进行强制类型转换。

5.0 #

C#5.0最大的新特性是通过两个关键字async和await支持异步功能。异步功能支持异步延续(asynchronous continuation),从而简化响应式和线程安全的富客户端应用程序的编写。它还有利于编写高并发和搞笑的I/O密集型应用程序,无需为每一个操作绑定一个线程资源。

6.0 #

C#6.0随着vs studio2015发布,同时发布的有崭新的完全由C#实现的、代号为“Roslyn”的编译器。新的编译器将一整条编译流水线通过程序库进行开放,从而实现对任意源代码的分析。

此外,C#为改善代码的清晰性引入了一系列小而精的改进。

null条件(”ELVIS”)运算符可以避免在调用方法或访问类型的成员之前显示地编写判断null的语句。在以下示例中,result的计算结果为null而不会抛出NullReferenceException:

System.Text.StringBuilder sb=null;
string result = sb?.ToString();  //结果是null

表达式体函数(expression-bodied function)可以以Lambda表达式的形式编写仅包含一个表达式的方法、属性、运算符以及索引器,使代码更加简短:

public int TimesTwo(int x)=>x*2;
public string SomeProperty =>"Property value";

属性初始化器可以对自动属性进行初始赋值:

public DateTime TimeCreated{get; set;} = DateTime.Now;

这种初始化也支持只读属性:

public DateTime TimeCreated{get;}=DateTime.Now;

也可以在构造器中对只读属性进行赋值,这使创建不可变(只读)类型变得更加容易。

索引初始化器可以一次性初始化具有索引器的任意类型:

var dict = new Dictionary()

{

[3]="three";

[10]="ten";

}

字符串插值用更简单的方式替代了string.Format:

string s= $"It is {DateTime.Now.DayOfWeek} today";

异常过滤器可以在catch块上再添加一个条件:

string html;

try

{

html = new WebClient().DownloadString("http://asef");

}

catch(WebException ex) when(ex.Status==WebExceptionStatus.Timeout)

{

…

}

using static指令可以引入一个类型的所有静态成员,这样就可以不用编写类型而直接使用这些成员:

using static System.Console;

…

WriteLine("Hello,world");

nameof运算值返回变量、类型或者其他符号的名称。这样在VS中就可以避免因变量重命名而造成不一致的代码:

int capacity=123;

string x=nameof(capacity); //x是capacity

string y =nameof(Uri.Host); //y是Host

最后,C#6.0可以在catch和finally块中使用await。

7.0 #

数字字面量的改进

C#7中,数字字面量可以使用下划线来改善可读性,它们称为数字分隔符而被编译器忽略:

int million=1_000_000;

二进制字面量可以使用0b前缀进行标识:

var b = 0b1010_1011_1101_1110_1111;

out变量及丢弃变量

C#7中,调用含有out参数的方法将更加容易。首先,可以非常自然地声明输出变量:

bool successful=int.TryParse("123",out int result);

Console.WriteLine(result);

当调用含有多个out参数的方法时,可以使用下划线字符忽略你并不关心的参数:

SomeBigMethod(out_, out_, out_, out int x, out_, out_, out_);

Console.WriteLine(x);

类型变量与模式变量

is运算符也可以自然地引入变量了,称为模式变量:

void Foo(object X)

{

if(x is string s)

Console.WriteLine(s.Length);

}

switch语句同样支持类型模式,因此不仅可以按常量switch,还可以按类型switch。

可以使用when子句来指定一个判断条件,或是直接选择null:

switch(x)

{

case int i:

Console.WriteLine("It’s an int!");

case string s:

Console.WriteLine(s.Length);

break;

case bool b when b==true:

Console.WriteLine("True");

break;

case null:

Console.WriteLine("Nothing");

break;

}

局部方法

局部方法是声明在其他函数内部的方法

void WriteCubes()

{

Console.WriteLine(Cube(3));

Console.WriteLine(Cube(4));

Console.WriteLine(Cube(5));

int Cube(int value)=>valuevaluevalue;

}

局部方法仅在包含它的函数内可见,并且可以像Lambda表达式那样捕获局部变量。

更多的表达式体成员

C#6引入了以“胖箭头”语法表示的表达式体方法、只读属性、运算符以及索引器。

而C#7将其扩展到了构造函数、读/写属性和终结器中:

public class Person

{

string name;

public Person(string name)=>Name=name;

public string Name

{

get=>name;

set=>name=value??"";

}

~Person()=>Console.WriteLine("finalize");

}

解构器

C#7引入了解构器模式。构造器一般接受一系列值(作为参数)并将其赋值给字段,而解构器则正相反,它将字段反向赋值给变量。以下示例为Person类编写了一个解构器(不包含异常处理):

public void Deconstruct(out string firstName,  out string lastName)

{

int spacePos=name.IndexOf(' '); //定位空格的位置

firstName=name.Substring(0,spacePos);

lastName=name.Substring(spacePos+1);

}

解构器以特定的语法进行调用:

var joe=new Person("Joe Bloggs");

var(first,last)=joe; //解构

Console.WriteLine(first); //Joe

Console.WriteLine(last); //Bloggs

元组

对C#7来说最值得一提的改进当属显式的元组(turple)支持。

元组提供了一种存储一系列相关值的简单方式:

var bob=("Bob",23);

Console.WriteLine(bob.Item1); //Bob

Console.WriteLine(bob.Item2); //23

C#的新元组实质上是使用System.ValueTurple<…>泛型结构的语法糖。

多亏了编译器的“魔力”,我们还可以对元组的元素进行命名:

var turple=(name:"Bob",age:23);

Console.WriteLine(tuple.names); //Bob

Console.WriteLine(tuple.age); //23

有了元组,函数再也不必通过一系列out参数或通过额外的类型包来返回多个值了:

static (int row, int column) GetFilePosition()=>(3, 10);

static void Main()

{

var pos =GetFilePosition();

Console.WriteLine(pos.row); //3

Console.WriteLine(pos.column); //10

}

元组隐式地支持解构模式,因此可将元组轻易地解构至多个独立局部变量中:

static void Main()

{

(int row, int column)= GetFilePosition(); //创建两个局部变量

Console.WriteLine(row); //3

Console.WriteLine(column); //10

}

throw表达式

在C#7之前,throw一直是一个语句。现在,它也可以作为表达式出现在表达式体函数中:

public string Foo()=> throw new NotImplementedException();

throw表达式也可以出现在三元条件表达式中:

string Capitalize(string value)=>

value == null? throw new ArgumentException("value");

value == ""?"":

char.ToUpper(value[0])+value.Substring(1);

8.0 #

索引与范围

索引(index)与范围(range)简化了访问数组元素或访问部分数组(或者诸如Span以及ReadOnlySpan等低层次类型)的工作。

在索引中使用^运算符可以从数组的结尾处开始引用数组的元素。例如,^1引用最后一个元素,而^2则引用倒数第二个元素,以此类推:

char[] vowels = new char[]{'a','e','I','o','u'};

char lastElement = vowels [^1]; //'u'

char secondToLast = vowels [^2]; //'o'

范围指的是在数组中可以使用..运算符将数组切片:

char[] firstTwo = vowels[..2]; //'a','e'

char[] lastThree = vowels[2..]; //'i','o','u'

char[] middleOne = vowels[2..3]; //'i'

char[] lastTwo = vowels[^2..]; //'o','u'

C#通过Index和Range两种类型实现了上述索引和范围操作:

Index last = ^1;

Range firstTwoRange = 0..2;

char[] firstTwo = vowels[firstTwoRange]; //'a','e'

也可以在自定义 类中使用Index和Range参数类型来支持索引和范围操作:

class Sentence

{

string[] words = "The quick brown fox".split();

public string this [Index index]=>words[index];

public string[] this [Range range]=>words[range];

}

合并赋值

??=运算符会在其值为null时执行赋值操作。因此,如下语句

if(s==null)s="Hello,world";

可以写为

s ??="Hello,world";

using声明

若忽略using语句的括号和语句块,那么这个语句就变成了using声明。而相应的资源也会在其执行超越该声明所在的语句块时调用其Dispose方法:

if(File.Exists("file.txt"))

{

using var reader = File.OpenText(file.txt);

Console.WriteLine(reader.ReadLine());

…

}

在上述代码中,当代码执行超出if语句的代码块时将调用reader的Dispose方法。

readonly成员

C#8支持对struct中的函数附加readonly修饰符,确保在该函数试图修改任何字段时产生编译错误:

struct Point

{

public int X,Y;

public readonly void ResetX() => X=0; //Error!

}

如果readonly函数调用一个非readonly函数,则编译器会生成一个警告(并防御性地创建该值类型对象的一个副本以避免产生更改。)

静态局部方法

在局部方法中添加static修饰符可以避免该方法使用其所在的外层方法中的局部变量和参数。这不但能够降低耦合,还能够在局部方法中随心所欲地定义变量而不必担心和所在的外层方法中定义的变量冲突。

switch表达式

C#8支持在表达式上下文中使用switch语句:

string cardName=cardNumber switch  //假设cardNumber是一个整型

{

13=>"King",

12=>"Queen",

11=>"Jack",

_=>"Pip card"   //相当于'default'

};

默认接口成员

C#8可以在接口成员中添加默认实现,这样就无须每次都实现该成员。

interface Ilogger

{

void Log(string text) =>Console.WriteLine(text);

}

这样,即使在接口中添加了新的方法也不必破坏现有的实现了。默认的实现必须通过显式接口类型才能进行调用:

((ILogger)new Logger()).Log("message");

现在还可以在接口中定义静态成员(包括静态字段),而接口的默认实现可以访问你这些成员。

interface Ilogger

{

void Log (string text)=>Console.WriteLine(Prefix+text);

static string Prefix="";

}

除此之外,也可以从外部访问这些静态成员:

Ilogger.Prefix="File log:";

这些静态成员也可以利用访问修饰符(例如private、protexted和internal)对其进行限制。接口无法定义实例字段。

元组模式、位置模式和属性模式

C#8支持三种新的模式,这些模式对switch语句和switch表达式均有裨益。

元组模式可以直接对多个值进行分支选择:

int cardNumber=12; string suite="spades";

string cardName=(cardNumber,suite)switch

{

(13,"spades")=>"King of spades",

(13,"clubs")=>"King of clubs",

…

}

位置模式和对象的解构器类似,属性模式可以用于匹配对象的属性值。这三种模式均可以用于switch语句和is运算符。以下示例使用属性模式来确认obj是否是一个长度为4的字符串:

If(obj is string {Length:4})…

可空引用类型

可空值类型令值类型对象也可以为null,而可空引用类型则正好做了相反的事情。

它一定程度上防止了引用类型对象的值为null,从而避免NullReferenceException的出现。可空引用类型完全依靠编译器在发现可能产生NullReferenceException的代码时产生警告或错误,从而提供一定的安全保障。

可空引用类型特性可以在工程级别(通过.csproject工程文件中的Nullable元素)进行配置,也可以在代码级别(使用#nullable指令)进行配置。当该特性处于开启状态时,编译器将默认引用的值不可为null。如需令一个引用类型变量接受null,则必须使用?后缀对其进行修饰,以声明该变量为可空引用类型:

nullable enable //从此语句开始启用可空引用特性 #

string s1=null; //会产生编译错误

string? s2 =null; //没问题,因为s2是可空引用类型变量

(未标记为可空引用类型的)未经初始化的字段将产生编译警告;此外,在解引用一个可空引用类型的变量时,若编译器认为该操作会产生NullReferenceException,则也将产生编译警告:

void Foo(string? s)=>Console.Write(s.Length); // Warning(.Length)

若想消除该警告,则需要使用允许控制运算符(!):

void Foo (string? s)=> Console.Write(s!.Length);

异步流

在C#8之前,可以使用yield return来编写迭代器,或使用await来写一个异步函数。但是无法同时使用两者来实现一个异步生成数据的迭代器。

C#8通过引入异步流(asynchronous stream)弥补了这个遗憾:

async IAsyncEnumerableRangeAsync(int startm int count, int delay)

{

for(int i=start;i<start+count;i++)

{

await Task.Delay(delay);

yield return i;

}

}

可以使用await foreach语句来消费该异步流:

awai foreach(var number in RangeAsync(0,10,100))

 Console.WriteLine(number);

9.0 #

新增record类型,可以记录不可变的数据

public record Person(string FirstName,int Age);

也可以使用构造函数构建

public record Person(string FirstName)

{

public int Age{get;init;}

}

init和set的区别在于init的值只能在构造器中赋予,所以在类实例化时不强行要求填写

可以这样用大括号添加:

Person person = new Person("Bin"){Age=25};

增加了with语句,可以这样使用来新建一个基于其他记录的对象,若大括号内为空则可以新建一个和基石一样的对象。

Person person2=person with {Age=26}

record直接tostring,会直接输出各个属性字段的值

Console.WriteLine(person.Tostring);

更新 2023年 2月 8日