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");
}
模式匹配
模式匹配用于检测表达式是否具有某种特征,主要由is和switch表达式(由C#8.0添加)来判断,前者用于匹配单个模式,后者用于批量处理多种模式。在不同的模式之间,可以使用and、not、or来组合不同的模式。
声明和类型模式匹配
用于检测是否与指定类型匹配(继承或实现),对于可空变量,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这一模式匹配。