C# 更新记录

在此记录C#版本更新时带来的新特性,并给出C#版本和使用场景的对应关系。

版本映射

C#版本 更新年份 .NET 框架 使用场景
4.0 2010 .NET Framework 4 《C#高级编程(第7版)》
5.0 2012 .NET Framework 4.5
6.0 2015 .NET Framework 4.6
7.0 2017 .NET Framework 4.6.2
7.1 2017 .NET Framework 4.7
7.2 2017 .NET Framework 4.7.1
7.3 2018 .NET Framework 4.7.2
8.0 2019 .NET Standard2.1 Unity 2023版本使用
9.0 2020 .NET 5
10.0 2021 .NET 6
11.0 2022 .NET 7
12.0 2023 .NET 8 Godot 4.4+版本使用/现长期支持版本
13.0 2024 .NET 9

C# 5.0

C#5.0引入了async/await语法糖,这一点已经被广泛使用,无需赘述。除此之外,加入了对调试很有帮助的调用方信息特性,也就是[CallerMemberNames][CallerLineNumber][CallerFilePath][CallerArgumentExpression]属性。

通过向函数的参数中标记特性,可以自动获取对应的string,例如:

public void TraceMessage(string message,
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0);

后面三个参数分别获取了调用成员的名称,调用文件的路径(编译时),调用文件的行号。

[CallerArgumentExpression]会将表达式转化为string并输出,例如:

public void Validate(bool condition, [CallerArgumentExpression("condition")] string? message=null);
Validate(nameof(func), func is not null);

此时message的值会自动设置为传入的表达式转化为string的值即"func is not null"。

C# 6.0

C#6.0更新了很多更有效率的小功能,包括经常被使用的表达式体函数(就是=>定义函数内容/返回值)、还有?.、?[]运算符等等,这些由于已经被经常使用而不再赘述。

第一个介绍的特性是nameof表达式,可用于生成变量、类型或成员的名称作为字符串常量,在编译时求值。相比于硬编码,nameof表达式提供了类型安全检查,确保对象存在才能通过编译,并且能够被重构工具识别,无需手动修改。

第二个介绍的特性是异常筛选,通过when关键字给出的表达式用于额外判断catch语句是否执行,可用于对异常处理的情况进行细分,特别是在网络传输中。例如:

catch (HttpRequestException e) when (e.Message.Contains("301")) { }
catch (HttpRequestException e) when (e.Message.Contains("404")) { }

C# 7.0

C#7.0更新了很多有用的新特性,下面将展开叙述。

简单解释的特性有out参数和throw表达式,前者用于实现多值return,后者用于在条件运算符(? :)、null合并运算符(??)和=>表达式中直接抛出异常。

元组和析构

元组(System.ValueTuple)的行为和Python中很类似,可以原生进行打包和解包(析构),但是C#中的元组是静态类型,具有类型安全检查。

元组可以显式给每个成员命名,使用字段名使用而不是索引访问,如果未指定,则默认按照Item1、Item2的顺序给出字段名。

(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");

ref变量

ref变量类似于C++中的指针和引用的结合体,但是提供了类型安全检查,且不具有指针所有的危险性,以及引用的不可变性。

= ref是引用赋值运算符,用于更改引用引用的对象,ref readonly限制了不能使用普通的赋值运算符,但依旧可以使用赋值运算符。

int variable = 0, anotherVarible = 1;
ref int aliasOfvariable = ref variable;
aliasOfvariable = ref anotherVarible;

同时,函数可以返回ref变量,根据接收者是不是ref变量决定使用引用赋值还是普通赋值,但是和C++一致的是,返回ref的源对象生命周期一定要超过函数内,同时每一处return语句都需要加上ref修饰。

public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return ref matrix[i, j];
    throw new InvalidOperationException("Not found");
}

模式匹配

模式匹配用于检测表达式是否具有某种特征,主要由isswitch表达式(由C#8.0添加)来判断,前者用于匹配单个模式,后者用于批量处理多种模式。在不同的模式之间,可以使用andnotor来组合不同的模式。

声明和类型模式匹配

用于检测是否与指定类型匹配(继承或实现),对于可空变量,is <realType>匹配同时会先额外匹配is not null。可以在匹配之后声明变量,之后可以将新变量当声明类使用。

sequence is IList<T> list
sequence is not null

常量模式匹配

用于匹配和常量值相等的模式,例如:

command is "SystemTest"

var模式匹配

用于匹配任何表达式(包括 null),并将其结果分配给新的局部变量,例如:

SimulateDataFetch(id) is var results 

is表达式中,var可以省略,但是在嵌套模式匹配中(列表、位置、属性)中,则不能省略某个子匹配的var

弃元模式匹配

用于匹配任何表达式(包括 null),并丢弃值,例如:

// 该表达式会永远返回true
a is _

关系模式匹配

用于匹配满足大小关系的模式,例如:

tempInFahrenheit is < 32

列表模式匹配

用于根据列表内容来匹配模式,使用弃元_来匹配任何元素,使用切片模式..来匹配零个或任意个元素。该匹配等效于对列表中的每一个元素再做一次模式匹配,使用样例如下:

transaction is [_, "WITHDRAWAL", .., var amount]

弃元

弃元(_)是一种在应用程序代码中人为取消使用的占位符变量,用于忽略表达式的值。弃元不是变量,而是一种丢弃值的说明,除非上下文中已经定义了标识符为_的对象。

第一种用途是元组的解包,对于不关注的值可以使用弃元来丢弃,例如:

(_, _, area) = city.GetCityInformation(cityName);

第二种用途是switch匹配,在上一章中已经说明。

第三种用途是给out变量使用,用于忽视输出,例如:

DateTime.TryParse(dateString, out _)

最后一种是独立弃元来指示要忽略的任何变量,比如使用弃元来让无法单独存在的表达式变成语句而得以执行,比如??表达式,或者对于异步任务的执行表示不等待执行、不捕获异常也不关心返回值。例如:

_ = arg ?? throw new ArgumentNullException();
_ = Task.Run(() => { ... });

C# 8.0

C# 8.0更新了很多的特性,这里仅介绍一部分特性,剩下的特性将在下一篇文章中介绍。

readonly

readonly可以用于修饰struct,表示所有成员都是只读,不能在构造之后修改值,这里遵循的是最严格的值不变性。

readonly也可以用于struct或者class的某个成员,比如方法、属性get和字段。对于引用类型,加入readonly值之后只能保证引用的对象不会改变,但仍然可以修改对象包含的内容。

using声明

使用using语句可以保证只读变量被正确释放,在离开using语句块的时候,会自动释放获取的 IDisposable 实例。如果因为异常提前离开了语句块,也会依旧释放。using语句也可以不含代码块,将会在它的作用域末尾自动释放,比如函数结束。

针对IAsyncDisposable,可以使用await using声明。使用样例如下:

using (StreamReader reader = File.OpenText("numbers.txt")) { }
await using (var resource = new AsyncDisposableExample()) { }
using StreamReader reader = File.OpenText(filePath);

模式匹配

C#8.0中新增了switch表达式,该表达式会一次匹配多个模式,并针对每一个模式返回一个表达式值。

除此之外,模式匹配部分还新增了两种模式:属性、位置。

位置模式匹配

用于对一个元组进行模式匹配,如果一个对象实现了Deconstruct函数那么可以将该对象自动解包并以元组的形式进行模式匹配。元组模式匹配类似于列表模式匹配,依旧相当于对于每一个元素做模式匹配。使用样例如下:

point switch
{
    (0, 0) => "Origin",
    (1, 0) => "positive X basis end",
    (0, 1) => "positive Y basis end",
    _ => "Just a point",
};

属性模式匹配

用于对一个对象是否具有指定字段/属性且满足子模式匹配进行模式匹配,需要指出字段/属性的名称,以及该字段/属性需要匹配的模式。本质类似于列表和位置模式匹配,都是进行嵌套的模式匹配。使用样例如下:

input switch
{
    string { Length: >= 5 } s => s.Substring(0, 5),
    string s => s,

    ICollection<char> { Count: >= 5 } symbols => ...
    ICollection<char> symbols => new string(symbols.ToArray()),

    null => ...,
    _ => ...,
};

样例中的第一个模式匹配string类型,且此类型有一个字段/属性Length,满足>=5这一模式匹配。