C# 9.0 中的新增功能What's new in C# 9.0

C# 9.0 向 C# 语言添加了以下功能和增强功能:C# 9.0 adds the following features and enhancements to the C# language:

  • 记录Records
  • 仅限 Init 的资源库Init only setters
  • 顶级语句Top-level statements
  • 模式匹配增强功能Pattern matching enhancements
  • 本机大小的整数Native sized integers
  • 函数指针Function pointers
  • 禁止发出 localsinit 标志Suppress emitting localsinit flag
  • 目标类型的新表达式Target-typed new expressions
  • 静态匿名函数static anonymous functions
  • 目标类型的条件表达式Target-typed conditional expressions
  • 协变返回类型Covariant return types
  • 扩展 GetEnumerator 支持 foreach 循环Extension GetEnumerator support for foreach loops
  • Lambda 弃元参数Lambda discard parameters
  • 本地函数的属性Attributes on local functions
  • 模块初始值设定项Module initializers
  • 分部方法的新功能New features for partial methods

.NET 5 支持 C# 9.0。C# 9.0 is supported on .NET 5. 有关详细信息,请参阅 C# 语言版本控制For more information, see C# language versioning.

记录类型Record types

C# 9.0 引入了记录类型,这是一种引用类型,它提供合成方法来提供值语义,从而实现相等性。C# 9.0 introduces record types, which are a reference type that provides synthesized methods to provide value semantics for equality. 默认情况下,记录是不可变的。Records are immutable by default.

使用记录类型可在 .NET 中轻松创建不可变的引用类型。Record types make it easy to create immutable reference types in .NET. 以前,.NET 类型主要分为引用类型(包括类和匿名类型)和值类型(包括结构和元组)。Historically, .NET types are largely classified as reference types (including classes and anonymous types) and value types (including structs and tuples). 虽然建议使用不可变的值类型,但可变的值类型通常不会引入错误。While immutable value types are recommended, mutable value types don’t often introduce errors. 值类型变量可保存值,因此在将值类型传递给方法时,会对原始数据的副本进行更改。Value type variables hold the values so changes are made to a copy of the original data when value types are passed to methods.

不可变的引用类型也有许多优点。There are many advantages to immutable reference types as well. 这些优点在使用共享数据的并发程序中更为明显。These advantages are more pronounced in concurrent programs with shared data. 遗憾的是,C# 强制编写大量额外的代码来创建不可变的引用类型。Unfortunately, C# forced you to write quite a bit of extra code to create immutable reference types. 记录为不可变的引用类型提供类型声明,该引用类型使用值语义实现相等性。Records provide a type declaration for an immutable reference type that uses value semantics for equality. 如果用于实现相等性的合成方法的属性和哈希代码的属性都相等,则认为两条记录相等。The synthesized methods for equality and hash codes consider two records equal if their properties are all equal. 请考虑以下定义:Consider this definition:

public record Person
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

记录定义会创建一个包含两个只读属性(FirstNameLastName)的 Person 类型。The record definition creates a Person type that contains two readonly properties: FirstName and LastName. Person 类型是引用类型。The Person type is a reference type. 如果查看 IL,它就是一个类。If you looked at the IL, it’s a class. 它是不可变的,因为在创建它后,无法修改任何属性。It’s immutable in that none of the properties can be modified once it's been created. 定义记录类型时,编译器会合成其他几种方法:When you define a record type, the compiler synthesizes several other methods for you:

记录支持继承。Records support inheritance. 可声明派生自 Person 的新记录,如下所示:You can declare a new record derived from Person as follows:

public record Teacher : Person
{
    public string Subject { get; }

    public Teacher(string first, string last, string sub)
        : base(first, last) => Subject = sub;
}

还可密封记录以防止进一步派生:You can also seal records to prevent further derivation:

public sealed record Student : Person
{
    public int Level { get; }

    public Student(string first, string last, int level) : base(first, last) => Level = level;
}

编译器会合成上述方法的不同版本。The compiler synthesizes different versions of the methods above. 方法签名取决于记录类型是否密封以及直接基类是否为对象。The method signatures depend on if the record type is sealed and if the direct base class is object. 记录应具有以下功能:Records should have the following capabilities:

  • 相等性是基于值的,包括检查类型是否匹配。Equality is value-based, and includes a check that the types match. 例如,即使两条记录的名称相同,Student 也不能等于 PersonFor example, a Student can't be equal to a Person, even if the two records share the same name.
  • 记录具有为你生成的一致的字符串表示形式。Records have a consistent string representation generated for you.
  • 记录支持副本构造。Records support copy construction. 正确的副本构造必须包括继承层次结构和开发人员添加的属性。Correct copy construction must include inheritance hierarchies, and properties added by developers.
  • 可通过修改复制记录。Records can be copied with modification. 这些复制和修改操作支持非破坏性转变。These copy and modify operations supports non-destructive mutation.

除了熟悉的 Equals 重载、operator ==operator != 外,编译器还会合成新的 EqualityContract 属性。In addition to the familiar Equals overloads, operator ==, and operator !=, the compiler synthesizes a new EqualityContract property. 该属性返回与记录类型匹配的 Type 对象。The property returns a Type object that matches the type of the record. 如果基类型为 object,则属性为 virtualIf the base type is object, the property is virtual. 如果基类型是其他记录类型,则属性为 overrideIf the base type is another record type, the property is an override. 如果记录类型为 sealed,则属性为 sealedIf the record type is sealed, the property is sealed. 合成的 GetHashCode 使用基类型和记录类型中声明的所有属性和字段中的 GetHashCodeThe synthesized GetHashCode uses the GetHashCode from all properties and fields declared in the base type and the record type. 这些合成方法在整个继承层次结构中强制执行基于值的相等性。These synthesized methods enforce value-based equality throughout an inheritance hierarchy. 这意味着,绝不会将 Student 视为与同名的 Person 相等。That means a Student will never be considered equal to a Person with the same name. 两条记录的类型必须匹配,而且记录类型之间共享的所有属性也必须相等。The types of the two records must match as well as all properties shared among the record types being equal.

记录还具有合成的构造函数和用于创建副本的“克隆”方法。Records also have a synthesized constructor and a "clone" method for creating copies. 合成的构造函数具有记录类型的一个参数。The synthesized constructor has one argument of the record type. 该函数会为记录的所有属性生成具有相同值的新记录。It produces a new record with the same values for all properties of the record. 如果记录是密封的,则此构造函数是专用函数;否则它将受到保护。This constructor is private if the record is sealed, otherwise it's protected. 合成的“克隆”方法支持用于记录层次结构的副本构造。The synthesized "clone" method supports copy construction for record hierarchies. “克隆”一词用引号引起来,因为实际名称是编译器生成的。The term "clone" is in quotes because the actual name is compiler generated. 无法在记录类型中创建名为 Clone 的方法。You can't create a method named Clone in a record type. 合成的“克隆”方法返回使用虚拟调度复制的记录类型。The synthesized "clone" method returns the type of record being copied using virtual dispatch. 编译器根据 record 上的访问修饰符为“克隆”方法添加不同的修饰符:The compiler adds different modifiers for the "clone" method depending on the access modifiers on the record:

  • 如果记录类型为 abstract,则“克隆”方法也为 abstractIf the record type is abstract, the "clone" method is also abstract. 如果基类型不是 object,则方法也是 overrideIf the base type isn't object, the method is also override.
  • 当基类型为 object 时,对于不是 abstract 的记录类型:For record types that aren't abstract when the base type is object:
    • 如果记录为 sealed,则不向“克隆”方法添加其他修饰符(这意味着它不是 virtual)。If the record is sealed, no additional modifiers are added to the "clone" method (meaning it is not virtual).
    • 如果记录不是 sealed,则“克隆”方法为 virtualIf the record isn't sealed, the "clone" method is virtual.
  • 当基类型不是 object 时,对于不是 abstract 的记录类型:For record types that aren't abstract when the base type is not object:
    • 如果记录是 sealed,则“克隆”方法也是 sealedIf the record is sealed, the "clone" method is also sealed.
    • 如果记录不是 sealed,则“克隆”方法为 overrideIf the record isn't sealed, the "clone" method is override.

所有这些规则的结果都是,跨记录类型的任何层次结构一致地实现了相等性。The result of all these rules is the equality is implemented consistently across any hierarchy of record types. 如果两条记录的属性相等且类型相同,则它们彼此相等,如下例所示:Two records are equal to each other if their properties are equal and their types are the same, as shown in the following example:

var person = new Person("Bill", "Wagner");
var student = new Student("Bill", "Wagner", 11);

Console.WriteLine(student == person); // false

编译器合成了两种支持打印输出的方法:ToString() 替代和 PrintMembersThe compiler synthesizes two methods that support printed output: a ToString() override, and PrintMembers. PrintMembers 采用 System.Text.StringBuilder 作为其参数。The PrintMembers takes a System.Text.StringBuilder as its argument. 它对记录类型中的所有属性追加一个用逗号分隔的属性名称和值的列表。It appends a comma-separated list of property names and values for all properties in the record type. PrintMembers 会调用派生自其他记录的任何记录的基本实现。PrintMembers calls the base implementation for any records derived from other records. ToString() 替代会返回由 PrintMembers 生成的字符串,并将其括在 {} 内。The ToString() override returns the string produced by PrintMembers, surrounded by { and }. 例如,StudentToString() 方法返回一个 string,类似于以下代码:For example, the ToString() method for Student returns a string like the following code:

"Student { LastName = Wagner, FirstName = Bill, Level = 11 }"

截至目前显示的示例都使用传统语法声明属性。The examples shown so far use traditional syntax to declare properties. 还有一种更简洁的格式,称为“位置记录”。There's a more concise form called positional records. 下面是先前定义为位置记录的 3 种记录类型:Here are the three record types defined earlier as positional records:

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);

这些声明创建的功能与早期版本相同(以下部分介绍了几项额外的功能)。These declarations create the same functionality as the earlier version (with a couple extra features covered in the following section). 这些声明以分号而不是方括号结尾,因为这些记录没有添加其他方法。These declarations end with a semicolon instead of brackets because these records don't add additional methods. 可添加正文,还可包括其他任何方法:You can add a body, and include any additional methods as well:

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 方法。The compiler produces a Deconstruct method for positional records. Deconstruct 方法的参数与记录类型中所有公共属性的名称匹配。The Deconstruct method has parameters that match the names of all public properties in the record type. Deconstruct 方法可用于将记录析构为其组件属性:The Deconstruct method can be used to deconstruct the record into its component properties:

var person = new Person("Bill", "Wagner");

var (first, last) = person;
Console.WriteLine(first);
Console.WriteLine(last);

最后,记录支持 with 表达式。Finally, records support with-expressions. with 表达式指示编译器创建记录的副本,但修改了指定的属性:A with-expression instructs the compiler to create a copy of a record, but with specified properties modified:

Person brother = person with { FirstName = "Paul" };

上述行创建新的 Person 记录,其中 LastName属性是 person 的副本,FirstName 为“Paul”。The above line creates a new Person record where the LastName property is a copy of person, and the FirstName is "Paul". 可在 with 表达式中设置任意数量的属性。You can set any number of properties in a with-expression. 你可编写除“克隆”方法以外的任何合成成员。Any of the synthesized members except the "clone" method may be written by you. 如果记录类型的方法与任何合成方法的签名匹配,则编译器不会合成该方法。If a record type has a method that matches the signature of any synthesized method, the compiler doesn't synthesize that method. 较早的 Dog 记录示例包含手动编码的 ToString() 方法作为示例。The earlier Dog record example contains a hand coded ToString() method as an example.

仅限 Init 的资源库Init only setters

仅限 init 的资源库提供一致的语法来初始化对象的成员。Init only setters provide consistent syntax to initialize members of an object. 属性初始值设定项可明确哪个值正在设置哪个属性。Property initializers make it clear which value is setting which property. 缺点是这些属性必须是可设置的。The downside is that those properties must be settable. 从 C# 9.0 开始,可为属性和索引器创建 init 访问器,而不是 set 访问器。Starting with C# 9.0, you can create init accessors instead of set accessors for properties and indexers. 调用方可使用属性初始化表达式语法在创建表达式中设置这些值,但构造完成后,这些属性将变为只读。Callers can use property initializer syntax to set these values in creation expressions, but those properties are readonly once construction has completed. 仅限 init 的资源库提供了一个窗口用来更改状态。Init only setters provide a window to change state. 构造阶段结束时,该窗口关闭。That window closes when the construction phase ends. 在完成所有初始化(包括属性初始化表达式和 with 表达式)之后,构造阶段实际上就结束了。The construction phase effectively ends after all initialization, including property initializers and with-expressions have completed.

上述位置记录示例演示了如何使用仅限 init 的资源库通过 with 表达式来设置属性。The preceding example for positional records demonstrates using an init-only setter to set a property using a with expression. 可在编写的任何类型中声明仅限 init 的资源库。You can declare init only setters in any type you write. 例如,以下结构定义了天气观察结构:For example, the following struct defines a weather observation structure:

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";
}

调用方可使用属性初始化表达式语法来设置值,同时仍保留不变性:Callers can use property initializer syntax to set the values, while still preserving the immutability:

var now = new WeatherObservation 
{ 
    RecordedAt = DateTime.Now, 
    TemperatureInCelsius = 20, 
    PressureInMillibars = 998.0m 
};

但在初始化后更改观察值是错误的,它会在初始化之外分配给仅限 init 的属性:But, changing an observation after initialization is an error by assigning to an init-only property outside of initialization:

// Error! CS8852.
now.TemperatureInCelsius = 18;

对于从派生类设置基类属性,仅限 init 的资源库很有用。Init only setters can be useful to set base class properties from derived classes. 它们还可通过基类中的帮助程序来设置派生属性。They can also set derived properties through helpers in a base class. 位置记录使用仅限 init 的资源库声明属性。Positional records declare properties using init only setters. 这些设置器可在 with 表达式中使用。Those setters are used in with-expressions. 可为定义的任何 classstruct 声明仅限 init 的资源库。You can declare init only setters for any class or struct you define.

顶级语句Top-level statements

顶级语句从许多应用程序中删除了不必要的流程。Top-level statements remove unnecessary ceremony from many applications. 请考虑规范的“Hello World!”Consider the canonical "Hello World!" 程序:program:

using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

只有一行代码执行所有操作。There’s only one line of code that does anything. 借助顶级语句,可使用 using 语句和执行操作的一行替换所有样本:With top-level statements, you can replace all that boilerplate with the using statement and the single line that does the work:

using System;

Console.WriteLine("Hello World!");

如果需要单行程序,可删除 using 指令,并使用完全限定的类型名称:If you wanted a one-line program, you could remove the using directive and use the fully qualified type name:

System.Console.WriteLine("Hello World!");

应用程序中只有一个文件可使用顶级语句。Only one file in your application may use top-level statements. 如果编译器在多个源文件中找到顶级语句,则是错误的。If the compiler finds top-level statements in multiple source files, it’s an error. 如果将顶级语句与声明的程序入口点方法(通常为 Main 方法)结合使用,也会出现错误。It’s also an error if you combine top-level statements with a declared program entry point method, typically a Main method. 从某种意义上讲,可认为一个文件包含通常位于 Program 类的 Main 方法中的语句。In a sense, you can think that one file contains the statements that would normally be in the Main method of a Program class.

此功能最常见的用途之一是创建材料。One of the most common uses for this feature is creating teaching materials. C# 初级开发人员可以用一两行代码Beginner C# developers can write the canonical “Hello World!” 编写规范的“Hello World!”。in one or two lines of code. 不需要额外的工作。None of the extra ceremony is needed. 不过,经验丰富的开发人员还会发现此功能的许多用途。However, seasoned developers will find many uses for this feature as well. 顶级语句可提供类似脚本的试验体验,这与 Jupyter 笔记本提供的很类似。Top-level statements enable a script-like experience for experimentation similar to what Jupyter notebooks provide. 顶级语句非常适合小型控制台程序和实用程序。Top-level statements are great for small console programs and utilities. Azure 函数是顶级语句的理想用例。Azure functions are an ideal use case for top-level statements.

最重要的是,顶层语句不会限制应用程序的范围或复杂程度。Most importantly, top-level statements don't limit your application’s scope or complexity. 这些语句可访问或使用任何 .NET 类。Those statements can access or use any .NET class. 它们也不会限制你对命令行参数或返回值的使用。They also don’t limit your use of command-line arguments or return values. 顶级语句可访问名为 args 的字符串数组。Top-level statements can access an array of strings named args. 如果顶级语句返回整数值,则该值将成为来自合成 Main 方法的整数返回代码。If the top-level statements return an integer value, that value becomes the integer return code from a synthesized Main method. 顶级语句可能包含异步表达式。The top-level statements may contain async expressions. 在这种情况下,合成入口点将返回 TaskTask<int>In that case, the synthesized entry point returns a Task, or Task<int>.

模式匹配增强功能Pattern matching enhancements

C# 9 包括新的模式匹配改进:C# 9 includes new pattern matching improvements:

  • 类型模式要求在变量是一种类型时匹配Type patterns match a variable is a type
  • 带圆括号的模式强制或强调模式组合的优先级Parenthesized patterns enforce or emphasize the precedence of pattern combinations
  • 联合 and 模式要求两个模式都匹配Conjunctive and patterns require both patterns to match
  • 析取 or 模式要求任一模式匹配Disjunctive or patterns require either pattern to match
  • 求反 not 模式要求模式不匹配Negated not patterns require that a pattern doesn’t match
  • 关系模式要求输入小于、大于、小于等于或大于等于给定常数。Relational patterns require the input be less than, greater than, less than or equal, or greater than or equal to a given constant.

这些模式丰富了模式的语法。These patterns enrich the syntax for patterns. 请考虑下列示例:Consider these examples:

public static bool IsLetter(this char c) =>
    c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

还可使用可选的括号来明确 and 的优先级高于 orAlternatively, with optional parentheses to make it clear that and has higher precedence than or:

public static bool IsLetterOrSeparator(this char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';

最常见的用途之一是用于 NULL 检查的新语法:One of the most common uses is a new syntax for a null check:

if (e is not null)
{
    // ...
}

这些模式中的任何一种都可在允许使用模式的任何上下文中使用:is 模式表达式、switch 表达式、嵌套模式以及 switch 语句的 case 标签的模式。Any of these patterns can be used in any context where patterns are allowed: is pattern expressions, switch expressions, nested patterns, and the pattern of a switch statement’s case label.

性能和互操作性Performance and interop

3 项新功能改进了对需要高性能的本机互操作性和低级别库的支持:本机大小的整数、函数指针和省略 localsinit 标志。Three new features improve support for native interop and low-level libraries that require high performance: native sized integers, function pointers, and omitting the localsinit flag.

本机大小的整数 nintnuint 是整数类型。Native sized integers, nint and nuint, are integer types. 它们由基础类型 System.IntPtrSystem.UIntPtr 表示。They're expressed by the underlying types System.IntPtr and System.UIntPtr. 编译器将这些类型的其他转换和操作作为本机整数公开。The compiler surfaces additional conversions and operations for these types as native ints. 本机大小的整数定义 MaxValueMinValue 的属性。Native sized integers define properties for MaxValue or MinValue. 这些值不能表示为编译时编译时,因为它取决于目标计算机上整数的本机大小。These values can't be expressed as compile time constants because they depend on the native size of an integer on the target machine. 这些值在运行时是只读的。Those values are readonly at runtime. 可在以下范围内对 nint 使用常量值:[int.MinValue ..You can use constant values for nint in the range [int.MinValue .. int.MaxValue].int.MaxValue]. 可在以下范围内对 nuint 使用常量值:[uint.MinValue ..You can use constant values for nuint in the range [uint.MinValue .. uint.MaxValue].uint.MaxValue]. 编译器使用 System.Int32System.UInt32 类型为所有一元和二元运算符执行常量折叠。The compiler performs constant folding for all unary and binary operators using the System.Int32 and System.UInt32 types. 如果结果不满足 32 位,操作将在运行时执行,且不会被视为常量。If the result doesn't fit in 32 bits, the operation is executed at runtime and isn't considered a constant. 在广泛使用整数数学且需要尽可能快的性能的情况下,本机大小的整数可提高性能。Native sized integers can increase performance in scenarios where integer math is used extensively and needs to have the fastest performance possible.

函数指针提供了一种简单的语法来访问 IL 操作码 ldftncalliFunction pointers provide an easy syntax to access the IL opcodes ldftn and calli. 可使用新的 delegate* 语法声明函数指针。You can declare function pointers using new delegate* syntax. delegate* 类型是指针类型。A delegate* type is a pointer type. 调用 delegate* 类型会使用 calli,而不是使用在 Invoke() 方法上采用 callvirt 的委托。Invoking the delegate* type uses calli, in contrast to a delegate that uses callvirt on the Invoke() method. 从语法上讲,调用是相同的。Syntactically, the invocations are identical. 函数指针调用使用 managed 调用约定。Function pointer invocation uses the managed calling convention. delegate* 语法后面添加 unmanaged 关键字,以声明想要 unmanaged 调用约定。You add the unmanaged keyword after the delegate* syntax to declare that you want the unmanaged calling convention. 可使用 delegate* 声明中的属性来指定其他调用约定。Other calling conventions can be specified using attributes on the delegate* declaration.

最后,可添加 System.Runtime.CompilerServices.SkipLocalsInitAttribute 来指示编译器不要发出 localsinit 标志。Finally, you can add the System.Runtime.CompilerServices.SkipLocalsInitAttribute to instruct the compiler not to emit the localsinit flag. 此标志指示 CLR 对所有局部变量进行零初始化。This flag instructs the CLR to zero-initialize all local variables. 从 1.0 开始,localsinit 标志一直是 C# 的默认行为。The localsinit flag has been the default behavior for C# since 1.0. 但在某些情况下,额外的零初始化可能会对性能产生可衡量的影响,However, the extra zero-initialization may have measurable performance impact in some scenarios. 特别是在使用 stackalloc 时。In particular, when you use stackalloc. 在这些情况下,可添加 SkipLocalsInitAttributeIn those cases, you can add the SkipLocalsInitAttribute. 可将它添加到单个方法或属性中,或者添加到 classstructinterface,甚至是模块中。You may add it to a single method or property, or to a class, struct, interface, or even a module. 此属性不会影响 abstract 方法,它会影响为实现生成的代码。This attribute doesn't affect abstract methods; it affects the code generated for the implementation.

这些功能在某些情况下可提高性能。These features can improve performance in some scenarios. 仅应在采用前后对这些功能进行仔细的基准测试之后使用它们。They should be used only after careful benchmarking both before and after adoption. 涉及本机大小整数的代码必须在使用不同整数大小的多个目标平台上进行测试。Code involving native sized integers must be tested on multiple target platforms with different integer sizes. 其他功能需要不安全的代码。The other features require unsafe code.

调整和完成功能Fit and finish features

还有其他很多功能有助于更高效地编写代码。Many of the other features help you write code more efficiently. 在 C# 9.0 中,已知创建对象的类型时,可在 new 表达式中省略该类型。In C# 9.0, you can omit the type in a new expression when the created object's type is already known. 最常见的用法是在字段声明中:The most common use is in field declarations:

private List<WeatherObservation> _observations = new();

当需要创建新对象作为参数传递给方法时,也可使用目标类型 newTarget-typed new can also be used when you need to create a new object to pass as an argument to a method. 请考虑使用以下签名的 ForecastFor() 方法:Consider a ForecastFor() method with the following signature:

public WeatherForecast ForecastFor(DateTime forecastDate, WeatherForecastOptions options)

可按如下所示调用该方法:You could call it as follows:

var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());

此功能还有一个不错的用途是,将其与仅限 init 的属性组合使用来初始化新对象:Another nice use for this feature is to combine it with init only properties to initialize a new object:

WeatherStation station = new() { Location = "Seattle, WA" };

可使用 return new(); 语句返回由默认构造函数创建的实例。You can return an instance created by the default constructor using a return new(); statement.

类似的功能可改进条件表达式的目标类型解析。A similar feature improves the target type resolution of conditional expressions. 进行此更改后,两个表达式无需从一个隐式转换到另一个,而是都可隐式转换为目标类型。With this change, the two expressions need not have an implicit conversion from one to the other, but may both have implicit conversions to a target type. 你可能不会注意到此更改。You likely won’t notice this change. 你会注意到,某些以前需要强制转换或无法编译的条件表达式现在可以正常工作。What you will notice is that some conditional expressions that previously required casts or wouldn’t compile now just work.

从 C# 9.0 开始,可将 static 修饰符添加到 Lambda 表达式匿名方法Starting in C# 9.0, you can add the static modifier to lambda expressions or anonymous methods. 静态 Lambda 表达式类似于 static 局部函数:静态 Lambda 或匿名方法无法捕获局部变量或实例状态。Static lambda expressions are analogous to the static local functions: a static lambda or anonymous method can't capture local variables or instance state. static 修饰符可防止意外捕获其他变量。The static modifier prevents accidentally capturing other variables.

协变返回类型为替代函数的返回类型提供了灵活性。Covariant return types provide flexibility for the return types of overridden functions. 替代的虚函数可返回从基类方法中声明的返回类型派生的类型。An overridden virtual function can return a type derived from the return type declared in the base class method. 这对于记录和其他支持虚拟克隆或工厂方法的类型很有用。This can be useful for Records, and for other types that support virtual clone or factory methods.

此外,foreach 循环将识别并使用扩展方法 GetEnumerator,否则将满足 foreach 模式。In addition, the foreach loop will recognize and use an extension method GetEnumerator that otherwise satisfies the foreach pattern. 此更改意味着 foreach 与其他基于模式的构造(例如异步模式和基于模式的析构)一致。This change means foreach is consistent with other pattern-based constructions such as the async pattern, and pattern-based deconstruction. 实际上,此更改意味着可以为任何类型添加 foreach 支持。In practice, this change means you can add foreach support to any type. 在设计中,应将其限制为在枚举对象有意义时使用。You should limit its use to when enumerating an object makes sense in your design.

接下来,可使用弃元作为 Lambda 表达式的参数。Next, you can use discards as parameters to lambda expressions. 这样可免于为参数命名,并且编译器也可避免使用它。This convenience enables you to avoid naming the argument, and the compiler may avoid using it. 可将 _ 用于任何参数。You use the _ for any argument. 有关详细信息,请参阅 Lambda 表达式一文中的 Lambda 表达式的输入参数一节。For more information, see the Input parameters of a lambda expression section of the Lambda expressions article.

最后,现在可将属性应用于本地函数Finally, you can now apply attributes to local functions. 例如,可将可为空的属性注释应用于本地函数。For example, you can apply nullable attribute annotations to local functions.

支持代码生成器Support for code generators

最后两项功能支持 C# 代码生成器。Two final features support C# code generators. C# 代码生成器是可编写的组件,类似于 roslyn 分析器或代码修补程序。C# code generators are a component you can write that is similar to a roslyn analyzer or code fix. 区别在于,代码生成器会在编译过程中分析代码并编写新的源代码文件。The difference is that code generators analyze code and write new source code files as part of the compilation process. 典型的代码生成器会在代码中搜索属性或其他约定。A typical code generator searches code for attributes or other conventions.

代码生成器使用 Roslyn 分析 API 读取属性或其他代码元素。A code generator read attributes or other code elements using the Roslyn analysis APIs. 通过该信息,它将新代码添加到编译中。From that information, it adds new code to the compilation. 源生成器只能添加代码,不能修改编译中的任何现有代码。Source generators can only add code; they aren't allowed to modify any existing code in the compilation.

为代码生成器添加的两项功能是分部方法语法和模块初始化表达式的扩展。The two features added for code generators are extensions to partial method syntax, and module initializers. 首先是对分部方法的更改。First, the changes to partial methods. 在 C# 9.0 之前,分部方法为 private,但不能指定访问修饰符、不能返回 void,也不能具有 out 参数。Before C# 9.0, partial methods are private but can't specify an access modifier, have a void return, and can't have out parameters. 这些限制意味着,如果未提供任何方法实现,编译器会删除对分部方法的所有调用。These restrictions meant that if no method implementation is provided, the compiler removes all calls to the partial method. C# 9.0 消除了这些限制,但要求分部方法声明必须具有实现。C# 9.0 removes these restrictions, but requires that partial method declarations have an implementation. 代码生成器可提供这种实现。Code generators can provide that implementation. 为了避免引入中断性变更,编译器会考虑没有访问修饰符的任何分部方法,以遵循旧规则。To avoid introducing a breaking change, the compiler considers any partial method without an access modifier to follow the old rules. 如果分部方法包括 private 访问修饰符,则由新规则控制该分部方法。If the partial method includes the private access modifier, the new rules govern that partial method.

代码生成器的第二项新功能是模块初始化表达式。The second new feature for code generators is module initializers. 模块初始化表达式是附加了 ModuleInitializerAttribute 属性的方法。Module initializers are methods that have the ModuleInitializerAttribute attribute attached to them. 程序集加载时,运行时将调用这些方法。These methods will be called by the runtime when the assembly loads. 模块初始化表达式方法:A module initializer method:

  • 必须是静态的Must be static
  • 必须没有参数Must be parameterless
  • 必须返回 voidMust return void
  • 不能是泛型方法Must not be a generic method
  • 不能包含在泛型类中Must not be contained in a generic class
  • 必须能够从包含模块访问Must be accessible from the containing module

最后一个要点实际上意味着该方法及其包含类必须是内部的或公共的。That last bullet point effectively means the method and its containing class must be internal or public. 方法不能为本地函数。The method can't be a local function.