C# 9.0 向 C# 语言添加了以下功能和增强功能:
- 记录
- 仅限 Init 的资源库
- 顶级语句
- 模式匹配增强功能
- 本机大小的整数
- 函数指针
- 禁止发出 localsinit 标志
- 目标类型的新表达式
- 静态匿名函数
- 目标类型的条件表达式
- 协变返回类型
- 扩展
GetEnumerator支持foreach循环 - Lambda 弃元参数
- 本地函数的属性
- 模块初始值设定项
- 分部方法的新功能
C# 语言版本控制。
默认情况下,记录是不可变的。
值类型变量可保存值,因此在将值类型传递给方法时,会对原始数据的副本进行更改。
请考虑以下定义:
public record Person { public string LastName { get; } public string FirstName { get; } public Person(string first, string last) => (FirstName, LastName) = (first, last); }
定义记录类型时,编译器会合成其他几种方法:
- 基于值的相等性比较方法
- GetHashCode()
- 复制和克隆成员
- ToString()
可声明派生自 Person 的新记录,如下所示:
public record Teacher : Person { public string Subject { get; } public Teacher(string first, string last, string sub) : base(first, last) => Subject = sub; }
还可密封记录以防止进一步派生:
public sealed record Student : Person { public int Level { get; } public Student(string first, string last, int level) : base(first, last) => Level = level; }
记录应具有以下功能:
- 例如,即使两条记录的名称相同,
Student也不能等于Person。 - 记录具有为你生成的一致的字符串表示形式。
- 正确的副本构造必须包括继承层次结构和开发人员添加的属性。
- 这些复制和修改操作支持非破坏性转变。
两条记录的类型必须匹配,而且记录类型之间共享的所有属性也必须相等。
编译器根据 record 上的访问修饰符为“克隆”方法添加不同的修饰符:
- 如果基类型不是
object,则方法也是override。 -
当基类型为
object时,对于不是abstract的记录类型:- 如果记录为
sealed,则不向“克隆”方法添加其他修饰符(这意味着它不是virtual)。 - 如果记录不是
sealed,则“克隆”方法为virtual。
- 如果记录为
-
当基类型不是
object时,对于不是abstract的记录类型:- 如果记录是
sealed,则“克隆”方法也是sealed。 - 如果记录不是
sealed,则“克隆”方法为override。
- 如果记录是
如果两条记录的属性相等且类型相同,则它们彼此相等,如下例所示:
var person = new Person("Bill", "Wagner"); var student = new Student("Bill", "Wagner", 11); Console.WriteLine(student == person); // false
ToString() 方法返回一个 string,类似于以下代码:
"Student { LastName = Wagner, FirstName = Bill, Level = 11 }"
下面是先前定义为位置记录的 3 种记录类型:
public record Person(string FirstName, string LastName); public record Teacher(string FirstName, string LastName, string Subject) : Person(FirstName, LastName); public sealed record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName);
可添加正文,还可包括其他任何方法:
public record Pet(string Name) { public void ShredTheFurniture() => Console.WriteLine("Shredding furniture"); } public record Dog(string Name) : Pet(Name) { public void WagTail() => Console.WriteLine("It's tail wagging time"); public override string ToString() { StringBuilder s = new(); base.PrintMembers(s); return $"{s.ToString()} is a dog"; } }
Deconstruct 方法可用于将记录析构为其组件属性:
var person = new Person("Bill", "Wagner"); var (first, last) = person; Console.WriteLine(first); Console.WriteLine(last);
with 表达式指示编译器创建记录的副本,但修改了指定的属性:
Person brother = person with { FirstName = "Paul" };
ToString() 方法作为示例。
在完成所有初始化(包括属性初始化表达式和 with 表达式)之后,构造阶段实际上就结束了。
例如,以下结构定义了天气观察结构:
public struct WeatherObservation { public DateTime RecordedAt { get; init; } public decimal TemperatureInCelsius { get; init; } public decimal PressureInMillibars { get; init; } public override string ToString() => $"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " + $"Temp = {TemperatureInCelsius}, with {PressureInMillibars} pressure"; }
调用方可使用属性初始化表达式语法来设置值,同时仍保留不变性:
var now = new WeatherObservation { RecordedAt = DateTime.Now, TemperatureInCelsius = 20, PressureInMillibars = 998.0m };
但在初始化后更改观察值是错误的,它会在初始化之外分配给仅限 init 的属性:
// Error! CS8852. now.TemperatureInCelsius = 18;
可为定义的任何 class 或 struct 声明仅限 init 的资源库。
程序:
using System; namespace HelloWorld { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } }
借助顶级语句,可使用 using 语句和执行操作的一行替换所有样本:
using System; Console.WriteLine("Hello World!");
如果需要单行程序,可删除 using 指令,并使用完全限定的类型名称:
System.Console.WriteLine("Hello World!");
从某种意义上讲,可认为一个文件包含通常位于 Program 类的 Main 方法中的语句。
Azure 函数是顶级语句的理想用例。
在这种情况下,合成入口点将返回 Task 或 Task<int>。
C# 9 包括新的模式匹配改进:
- 类型模式要求在变量是一种类型时匹配
- 带圆括号的模式强制或强调模式组合的优先级
- 联合
and模式要求两个模式都匹配 - 析取
or模式要求任一模式匹配 - 求反
not模式要求模式不匹配 - 关系模式要求输入小于、大于、小于等于或大于等于给定常数。
请考虑下列示例:
public static bool IsLetter(this char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
还可使用可选的括号来明确 and 的优先级高于 or:
public static bool IsLetterOrSeparator(this char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';
最常见的用途之一是用于 NULL 检查的新语法:
if (e is not null) { // ... }
这些模式中的任何一种都可在允许使用模式的任何上下文中使用:is 模式表达式、switch 表达式、嵌套模式以及 switch 语句的 case 标签的模式。
3 项新功能改进了对需要高性能的本机互操作性和低级别库的支持:本机大小的整数、函数指针和省略 localsinit 标志。
在广泛使用整数数学且需要尽可能快的性能的情况下,本机大小的整数可提高性能。
可使用 delegate* 声明中的属性来指定其他调用约定。
此属性不会影响 abstract 方法,它会影响为实现生成的代码。
其他功能需要不安全的代码。
最常见的用法是在字段声明中:
private List<WeatherObservation> _observations = new();
请考虑使用以下签名的 ForecastFor() 方法:
public WeatherForecast ForecastFor(DateTime forecastDate, WeatherForecastOptions options)
可按如下所示调用该方法:
var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());
此功能还有一个不错的用途是,将其与仅限 init 的属性组合使用来初始化新对象:
WeatherStation station = new() { Location = "Seattle, WA" };
可使用 return new(); 语句返回由默认构造函数创建的实例。
你会注意到,某些以前需要强制转换或无法编译的条件表达式现在可以正常工作。
static 修饰符可防止意外捕获其他变量。
这对于记录和其他支持虚拟克隆或工厂方法的类型很有用。
在设计中,应将其限制为在枚举对象有意义时使用。
Lambda 表达式的输入参数一节。
可为空的属性注释应用于本地函数。
典型的代码生成器会在代码中搜索属性或其他约定。
源生成器只能添加代码,不能修改编译中的任何现有代码。
如果分部方法包括 private 访问修饰符,则由新规则控制该分部方法。
模块初始化表达式方法:
- 必须是静态的
- 必须没有参数
- 必须返回 void
- 不能是泛型方法
- 不能包含在泛型类中
- 必须能够从包含模块访问
方法不能为本地函数。
https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-9#record-types