C# 7.0 - C# 7.3 中的新增功能What's new in C# 7.0 through C# 7.3

C# 7.0 - C# 7.3 为 C# 开发体验带来了大量功能和增量改进。C# 7.0 through C# 7.3 brought a number of features and incremental improvements to your development experience with C#. 本文概述了新的语言功能和编译器选项。This article provides an overview of the new language features and compiler options. 说明中描述了 C# 7.3 的行为,C# 7.3 是基于 .NET Framework 的应用程序支持的最新版本。The descriptions describe the behavior for C# 7.3, which is the most recent version supported for .NET Framework-based applications.

C# 7.1 中添加了语言版本选择配置元素,因此你可以在项目文件中指定编译器语言版本。The language version selection configuration element was added with C# 7.1, which enables you to specify the compiler language version in your project file.

C# 7.0-7.3 将这些功能和主题添加到了 C# 语言中:C# 7.0-7.3 adds these features and themes to the C# language:

  • 元组和弃元Tuples and discards
    • 可以创建包含多个公共字段的轻量级未命名类型。You can create lightweight, unnamed types that contain multiple public fields. 编译器和 IDE 工具可理解这些类型的语义。Compilers and IDE tools understand the semantics of these types.
    • 弃元是指在不关心所赋予的值时,赋值中使用的临时只写变量。Discards are temporary, write-only variables used in assignments when you don't care about the value assigned. 在对元组和用户定义类型进行解构,以及在使用 out 参数调用方法时,它们的作用最大。They're most useful when deconstructing tuples and user-defined types, as well as when calling methods with out parameters.
  • 模式匹配Pattern Matching
    • 可以基于任意类型和这些类型的成员的值创建分支逻辑。You can create branching logic based on arbitrary types and values of the members of those types.
  • async Main 方法async Main method
    • 应用程序的入口点可以含有 async 修饰符。The entry point for an application can have the async modifier.
  • 本地函数Local Functions
    • 可以将函数嵌套在其他函数内,以限制其范围和可见性。You can nest functions inside other functions to limit their scope and visibility.
  • 更多的 expression-bodied 成员More expression-bodied members
    • 可使用表达式创作的成员列表有所增长。The list of members that can be authored using expressions has grown.
  • throw 表达式throw Expressions
    • 可以在之前因为 throw 是语句而不被允许的代码构造中引发异常。You can throw exceptions in code constructs that previously weren't allowed because throw was a statement.
  • default 文本表达式default literal expressions
    • 在可以推断目标类型的情况下,可在默认值表达式中使用默认文本表达式。You can use default literal expressions in default value expressions when the target type can be inferred.
  • 数字文本语法改进Numeric literal syntax improvements
    • 新令牌可提高数值常量的可读性。New tokens improve readability for numeric constants.
  • out 变量out variables
    • 可以将 out 值内联作为参数声明到使用这些参数的方法中。You can declare out values inline as arguments to the method where they're used.
  • 非尾随命名参数Non-trailing named arguments
    • 命名的参数可后接位置参数。Named arguments can be followed by positional arguments.
  • private protected 访问修饰符private protected access modifier
    • private protected 访问修饰符允许访问同一程序集中的派生类。The private protected access modifier enables access for derived classes in the same assembly.
  • 改进了重载解析Improved overload resolution
    • 用于解决重载解析歧义的新规则。New rules to resolve overload resolution ambiguity.
  • 编写安全高效代码的技巧Techniques for writing safe efficient code
    • 结合了多项语法改进,可使用引用语义处理值类型。A combination of syntax improvements that enable working with value types using reference semantics.

最后,编译器中添加了新的选项:Finally, the compiler has new options:

  • 控制引用程序集生成-refout-refonly-refout and -refonly that control reference assembly generation.
  • -publicsign,用于启用程序集的开放源代码软件 (OSS) 签名。-publicsign to enable Open Source Software (OSS) signing of assemblies.
  • -pathmap用于提供源目录的映射。-pathmap to provide a mapping for source directories.

本文的其余部分概述了每个功能。The remainder of this article provides an overview of each feature. 你将了解每项功能背后的原理和语法。For each feature, you'll learn the reasoning behind it and the syntax. 可以使用 dotnet try 全局工具在环境中浏览这些功能:You can explore these features in your environment using the dotnet try global tool:

  1. 安装 dotnet-try 全局工具。Install the dotnet-try global tool.
  2. 克隆 dotnet/try-samples 存储库。Clone the dotnet/try-samples repository.
  3. 将当前目录设置为 try-samples 存储库的 csharp7 子目录 。Set the current directory to the csharp7 subdirectory for the try-samples repository.
  4. 运行 dotnet tryRun dotnet try.

元组和弃元Tuples and discards

C# 为用于说明设计意图的类和结构提供了丰富的语法。C# provides a rich syntax for classes and structs that is used to explain your design intent. 但是,这种丰富的语法有时会需要额外的工作,但益处却很少。But sometimes that rich syntax requires extra work with minimal benefit. 你可能经常编写需要包含多个数据元素的简单结构的方法。You may often write methods that need a simple structure containing more than one data element. 为了支持这些方案,已将元组添加到了 C#。To support these scenarios tuples were added to C#. 元组是包含多个字段以表示数据成员的轻量级数据结构。Tuples are lightweight data structures that contain multiple fields to represent the data members. 这些字段没有经过验证,并且你无法定义自己的方法。The fields aren't validated, and you can't define your own methods. C# 元组类型支持 ==!=C# tuple types support == and !=. 有关详细信息,For more information.

备注

低于 C# 7.0 的版本中也提供元组,但它们效率低下且不具有语言支持。Tuples were available before C# 7.0, but they were inefficient and had no language support. 这意味着元组元素只能作为 Item1Item2 等引用。This meant that tuple elements could only be referenced as Item1, Item2 and so on. C# 7.0 引入了对元组的语言支持,可利用更有效的新元组类型向元组字段赋予语义名称。C# 7.0 introduces language support for tuples, which enables semantic names for the fields of a tuple using new, more efficient tuple types.

可以通过为每个成员赋值来创建元组,并可选择为元组的每个成员提供语义名称:You can create a tuple by assigning a value to each member, and optionally providing semantic names to each of the members of the tuple:

(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

namedLetters 元组包含称为 AlphaBeta 的字段。The namedLetters tuple contains fields referred to as Alpha and Beta. 这些名称仅存在于编译时且不保留,例如在运行时使用反射来检查元组时。Those names exist only at compile time and aren't preserved, for example when inspecting the tuple using reflection at run time.

在进行元组赋值时,还可以指定赋值右侧的字段的名称:In a tuple assignment, you can also specify the names of the fields on the right-hand side of the assignment:

var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

在某些时候,你可能想要解包从方法返回的元组的成员。There may be times when you want to unpackage the members of a tuple that were returned from a method. 可通过为元组中的每个值声明单独的变量来实现此目的。You can do that by declaring separate variables for each of the values in the tuple. 这种解包操作称为解构元组:This unpackaging is called deconstructing the tuple:

(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

还可以为 .NET 中的任何类型提供类似的析构。You can also provide a similar deconstruction for any type in .NET. 编写 Deconstruct 方法,用作类的成员。You write a Deconstruct method as a member of the class. Deconstruct 方法为你要提取的每个属性提供一组 out 参数。That Deconstruct method provides a set of out arguments for each of the properties you want to extract. 考虑提供析构函数方法的此 Point 类,该方法提取 XY 坐标:Consider this Point class that provides a deconstructor method that extracts the X and Y coordinates:

public class Point
{
    public Point(double x, double y)
        => (X, Y) = (x, y);

    public double X { get; }
    public double Y { get; }

    public void Deconstruct(out double x, out double y) =>
        (x, y) = (X, Y);
}

可以通过向元组分配 Point 来提取各个字段:You can extract the individual fields by assigning a Point to a tuple:

var p = new Point(3.14, 2.71);
(double X, double Y) = p;

在初始化元组时,许多时候,赋值操作右侧的变量名与用于元组元素的名称相同:元组元素的名称可通过用于初始化元组的变量进行推断:Many times when you initialize a tuple, the variables used for the right side of the assignment are the same as the names you'd like for the tuple elements: The names of tuple elements can be inferred from the variables used to initialize the tuple:

int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // element names are "count" and "label"

若要详细了解此功能,可以参阅元组类型一文。You can learn more about this feature in the Tuple types article.

通常,在进行元组解构或使用 out 参数调用方法时,必须定义一个其值无关紧要且你不打算使用的变量。Often when deconstructing a tuple or calling a method with out parameters, you're forced to define a variable whose value you don't care about and don't intend to use. 为处理此情况,C# 增添了对弃元的支持。C# adds support for discards to handle this scenario. 弃元是一个名为 _(下划线字符)的只写变量,可向单个变量赋予要放弃的所有值。A discard is a write-only variable whose name is _ (the underscore character); you can assign all of the values that you intend to discard to the single variable. 弃元类似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)。A discard is like an unassigned variable; apart from the assignment statement, the discard can't be used in code.

在以下方案中支持弃元:Discards are supported in the following scenarios:

  • 在对元组或用户定义的类型进行解构时。When deconstructing tuples or user-defined types.
  • 在使用 out 参数调用方法时。When calling methods with out parameters.
  • 在使用 isswitch 语句匹配操作的模式中。In a pattern matching operation with the is and switch statements.
  • 在要将某赋值的值显式标识为弃元时用作独立标识符。As a standalone identifier when you want to explicitly identify the value of an assignment as a discard.

以下示例定义了 QueryCityDataForYears 方法,它返回一个包含两个不同年份的城市数据的六元组。The following example defines a QueryCityDataForYears method that returns a 6-tuple that contains data for a city for two different years. 本例中,方法调用仅与此方法返回的两个人口值相关,因此在进行元组解构时,将元组中的其余值视为弃元。The method call in the example is concerned only with the two population values returned by the method and so treats the remaining values in the tuple as discards when it deconstructs the tuple.

using System;
using System.Collections.Generic;

public class Example
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }

    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;

        if (name == "New York City")
        {
            area = 468.48;
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

有关详细信息,请参阅弃元For more information, see Discards.

模式匹配Pattern matching

模式匹配是一组功能,利用这些功能,你可以通过新的方式在代码中表示控制流。Pattern matching is a set of features that enable new ways to express control flow in your code. 你可以测试变量的类型、值或其属性的值。You can test variables for their type, values or the values of their properties. 这些方法可创建可读性更佳的代码流。These techniques create more readable code flow.

模式匹配支持 is 表达式和 switch 表达式。Pattern matching supports is expressions and switch expressions. 每个表达式都允许检查对象及其属性以确定该对象是否满足所寻求的模式。Each enables inspecting an object and its properties to determine if that object satisfies the sought pattern. 使用 when 关键字来指定模式的其他规则。You use the when keyword to specify additional rules to the pattern.

is 模式表达式扩展了常用 is 运算符以查询关于其类型的对象,并在一条指令分配结果。The is pattern expression extends the familiar is operator to query an object about its type and assign the result in one instruction. 以下代码检查变量是否为 int,如果是,则将其添加到当前总和:The following code checks if a variable is an int, and if so, adds it to the current sum:

if (input is int count)
    sum += count;

前面的小型示例演示了 is 表达式的增强功能。The preceding small example demonstrates the enhancements to the is expression. 可以针对值类型和引用类型进行测试,并且可以将成功结果分配给类型正确的新变量。You can test against value types as well as reference types, and you can assign the successful result to a new variable of the correct type.

switch 匹配表达式具有常见的语法,它基于已包含在 C# 语言中的 switch 语句。The switch match expression has a familiar syntax, based on the switch statement already part of the C# language. 更新后的 switch 语句有几个新构造:The updated switch statement has several new constructs:

  • switch 表达式的控制类型不再局限于整数类型、Enum 类型、string 或与这些类型之一对应的可为 null 的类型。The governing type of a switch expression is no longer restricted to integral types, Enum types, string, or a nullable type corresponding to one of those types. 可能会使用任何类型。Any type may be used.
  • 可以在每个 case 标签中测试 switch 表达式的类型。You can test the type of the switch expression in each case label. is 表达式一样,可以为该类型指定一个新变量。As with the is expression, you may assign a new variable to that type.
  • 可以添加 when 子句以进一步测试该变量的条件。You may add a when clause to further test conditions on that variable.
  • case 标签的顺序现在很重要。The order of case labels is now important. 执行匹配的第一个分支;其他将跳过。The first branch to match is executed; others are skipped.

以下代码演示了这些新功能:The following code demonstrates these new features:

public static int SumPositiveNumbers(IEnumerable<object> sequence)
{
    int sum = 0;
    foreach (var i in sequence)
    {
        switch (i)
        {
            case 0:
                break;
            case IEnumerable<int> childSequence:
            {
                foreach(var item in childSequence)
                    sum += (item > 0) ? item : 0;
                break;
            }
            case int n when n > 0:
                sum += n;
                break;
            case null:
                throw new NullReferenceException("Null found in sequence");
            default:
                throw new InvalidOperationException("Unrecognized type");
        }
    }
    return sum;
}
  • case 0: 是常见的常量模式。case 0: is the familiar constant pattern.
  • case IEnumerable<int> childSequence: 是一种类型模式。case IEnumerable<int> childSequence: is a type pattern.
  • case int n when n > 0: 是具有附加 when 条件的类型模式。case int n when n > 0: is a type pattern with an additional when condition.
  • case null: 是 null 模式。case null: is the null pattern.
  • default: 是常见的默认事例。default: is the familiar default case.

自 C# 7.1 起,isswitch 类型模式的模式表达式的类型可能为泛型类型参数。Beginning with C# 7.1, the pattern expression for is and the switch type pattern may have the type of a generic type parameter. 这可能在检查 structclass 类型且要避免装箱时最有用。This can be most useful when checking types that may be either struct or class types, and you want to avoid boxing.

可以在 C# 中的模式匹配中了解有关模式匹配的更多信息。You can learn more about pattern matching in Pattern Matching in C#.

异步 main 方法Async main

异步 Main 方法使你能够在 Main 方法中使用 await 关键字。An async main method enables you to use await in your Main method. 在过去,需要编写:Previously you would need to write:

static int Main()
{
    return DoAsyncWork().GetAwaiter().GetResult();
}

现在,您可以编写:You can now write:

static async Task<int> Main()
{
    // This could also be replaced with the body
    // DoAsyncWork, including its await expressions:
    return await DoAsyncWork();
}

如果程序不返回退出代码,可以声明返回 TaskMain 方法:If your program doesn't return an exit code, you can declare a Main method that returns a Task:

static async Task Main()
{
    await SomeAsyncMethod();
}

如需了解更多详情,可以阅读编程指南中的异步 Main 一文。You can read more about the details in the async main article in the programming guide.

本地函数Local functions

许多类的设计都包括仅从一个位置调用的方法。Many designs for classes include methods that are called from only one location. 这些额外的私有方法使每个方法保持小且集中。These additional private methods keep each method small and focused. 本地函数使你能够在另一个方法的上下文内声明方法。Local functions enable you to declare methods inside the context of another method. 本地函数使得类的阅读者更容易看到本地方法仅从声明它的上下文中调用。Local functions make it easier for readers of the class to see that the local method is only called from the context in which it is declared.

对于本地函数有两个常见的用例:公共迭代器方法和公共异步方法。There are two common use cases for local functions: public iterator methods and public async methods. 这两种类型的方法都生成报告错误的时间晚于程序员期望时间的代码。Both types of methods generate code that reports errors later than programmers might expect. 在迭代器方法中,只有在调用枚举返回的序列的代码时才会观察到任何异常。In iterator methods, any exceptions are observed only when calling code that enumerates the returned sequence. 在异步方法中,只有当返回的 Task 处于等待状态时才会观察到任何异常。In async methods, any exceptions are only observed when the returned Task is awaited. 以下示例演示如何使用本地函数将参数验证与迭代器实现分离:The following example demonstrates separating parameter validation from the iterator implementation using a local function:

public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
    if (start < 'a' || start > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
    if (end < 'a' || end > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");

    if (end <= start)
        throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");

    return alphabetSubsetImplementation();

    IEnumerable<char> alphabetSubsetImplementation()
    {
        for (var c = start; c < end; c++)
            yield return c;
    }
}

可以对 async 方法采用相同的技术,以确保在异步工作开始之前引发由参数验证引起的异常:The same technique can be employed with async methods to ensure that exceptions arising from argument validation are thrown before the asynchronous work begins:

public Task<string> PerformLongRunningWork(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    return longRunningWorkImplementation();

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}

现在支持此语法:This syntax is now supported:

[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }

属性 SomeThingAboutFieldAttribute 应用于编译器生成的 SomeProperty 的支持字段。The attribute SomeThingAboutFieldAttribute is applied to the compiler generated backing field for SomeProperty. 有关详细信息,请参阅 C# 编程指南中的属性For more information, see attributes in the C# programming guide.

备注

本地函数支持的某些设计也可以使用 lambda 表达式来完成。Some of the designs that are supported by local functions can also be accomplished using lambda expressions. 有关详细信息,请参阅本地函数与 Lambda 表达式For more information, see Local functions vs. lambda expressions.

更多的 expression-bodied 成员More expression-bodied members

C# 6 为成员函数和只读属性引入了 expression-bodied 成员。C# 6 introduced expression-bodied members for member functions and read-only properties. C# 7.0 扩展了可作为表达式实现的允许的成员。C# 7.0 expands the allowed members that can be implemented as expressions. 在 C# 7.0 中,你可以在属性和索引器上实现构造函数、终结器以及 getset 访问器。In C# 7.0, you can implement constructors, finalizers, and get and set accessors on properties and indexers. 以下代码演示了每种情况的示例:The following code shows examples of each:

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

备注

本示例不需要终结器,但显示它是为了演示语法。This example does not need a finalizer, but it is shown to demonstrate the syntax. 不应在类中实现终结器,除非有必要发布非托管资源。You should not implement a finalizer in your class unless it is necessary to release unmanaged resources. 还应考虑使用 SafeHandle 类,而不是直接管理非托管资源。You should also consider using the SafeHandle class instead of managing unmanaged resources directly.

这些 expression-bodied 成员的新位置代表了 C# 语言的一个重要里程碑:这些功能由致力于开发开放源代码 Roslyn 项目的社区成员实现。These new locations for expression-bodied members represent an important milestone for the C# language: These features were implemented by community members working on the open-source Roslyn project.

将方法更改为 expression bodied 成员是二进制兼容的更改Changing a method to an expression bodied member is a binary compatible change.

引发表达式Throw expressions

在 C# 中,throw 始终是一个语句。In C#, throw has always been a statement. 因为 throw 是一个语句而非表达式,所以在某些 C# 构造中无法使用它。Because throw is a statement, not an expression, there were C# constructs where you couldn't use it. 它们包括条件表达式、null 合并表达式和一些 lambda 表达式。These included conditional expressions, null coalescing expressions, and some lambda expressions. 添加 expression-bodied 成员将添加更多位置,在这些位置中,throw 表达式会很有用。The addition of expression-bodied members adds more locations where throw expressions would be useful. 为了可以编写这些构造,C# 7.0 引入了 throw 表达式So that you can write any of these constructs, C# 7.0 introduces throw expressions.

这使得编写更多基于表达式的代码变得更容易。This addition makes it easier to write more expression-based code. 不需要其他语句来进行错误检查。You don't need additional statements for error checking.

默认文本表达式Default literal expressions

默认文本表达式是针对默认值表达式的一项增强功能。Default literal expressions are an enhancement to default value expressions. 这些表达式将变量初始化为默认值。These expressions initialize a variable to the default value. 过去会这么编写:Where you previously would write:

Func<string, bool> whereClause = default(Func<string, bool>);

现在,可以省略掉初始化右侧的类型:You can now omit the type on the right-hand side of the initialization:

Func<string, bool> whereClause = default;

有关详细信息,请参阅 default 运算符一文中的 default 文本部分。For more information, see the default literal section of the default operator article.

数字文本语法改进Numeric literal syntax improvements

误读的数值常量可能使第一次阅读代码时更难理解。Misreading numeric constants can make it harder to understand code when reading it for the first time. 位掩码或其他符号值容易产生误解。Bit masks or other symbolic values are prone to misunderstanding. C# 7.0 包括两项新功能,可用于以最可读的方式写入数字来用于预期用途:二进制文本和数字分隔符 。C# 7.0 includes two new features to write numbers in the most readable fashion for the intended use: binary literals, and digit separators.

在创建位掩码时,或每当数字的二进制表示形式使代码最具可读性时,以二进制形式写入该数字:For those times when you're creating bit masks, or whenever a binary representation of a number makes the most readable code, write that number in binary:

public const int Sixteen =   0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;

常量开头的 0b 表示该数字以二进制数形式写入。The 0b at the beginning of the constant indicates that the number is written as a binary number. 二进制数可能会很长,因此通过引入 _ 作为数字分隔符通常更易于查看位模式,如前面示例中的二进制常量所示。Binary numbers can get long, so it's often easier to see the bit patterns by introducing the _ as a digit separator, as shown in the binary constant in the preceding example. 数字分隔符可以出现在常量的任何位置。The digit separator can appear anywhere in the constant. 对于十进制数字,通常将其用作千位分隔符。For base 10 numbers, it is common to use it as a thousands separator. 十六进制和二进制文本可采用 _ 开头:Hex and binary numeric literals may begin with an _:

public const long BillionsAndBillions = 100_000_000_000;

数字分隔符也可以与 decimalfloatdouble 类型一起使用:The digit separator can be used with decimal, float, and double types as well:

public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

综观来说,你可以声明可读性更强的数值常量。Taken together, you can declare numeric constants with much more readability.

out 变量out variables

支持 out 参数的现有语法已在 C# 7 中得到改进。The existing syntax that supports out parameters has been improved in C# 7. 现在可以在方法调用的参数列表中声明 out 变量,而不是编写单独的声明语句:You can now declare out variables in the argument list of a method call, rather than writing a separate declaration statement:

if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

为清楚起见,我们建议你指定 out 变量的类型,如前面的示例所示。You may want to specify the type of the out variable for clarity, as shown in the preceding example. 但是,该语言支持使用隐式类型的局部变量:However, the language does support using an implicitly typed local variable:

if (int.TryParse(input, out var answer))
    Console.WriteLine(answer);
else
    Console.WriteLine("Could not parse input");
  • 代码更易于阅读。The code is easier to read.
    • 在使用 out 变量的地方声明 out 变量,而不是在上面的代码行中声明。You declare the out variable where you use it, not on a preceding line of code.
  • 无需分配初始值。No need to assign an initial value.
    • 通过在方法调用中使用 out 变量的位置声明该变量,使得在分配它之前不可能意外使用它。By declaring the out variable where it's used in a method call, you can't accidentally use it before it is assigned.

已对在 C# 7.0 中添加的允许 out 变量声明的语法进行了扩展,以包含字段初始值设定项、属性初始值设定项、构造函数初始值设定项和查询子句。The syntax added in C# 7.0 to allow out variable declarations has been extended to include field initializers, property initializers, constructor initializers, and query clauses. 它允许使用如以下示例中所示的代码:It enables code such as the following example:

public class B
{
   public B(int i, out int j)
   {
      j = i;
   }
}

public class D : B
{
   public D(int i) : base(i, out var j)
   {
      Console.WriteLine($"The value of 'j' is {j}");
   }
}

非尾随命名参数Non-trailing named arguments

方法调用现可使用位于位置参数前面的命名参数(若这些命名参数的位置正确)。Method calls may now use named arguments that precede positional arguments when those named arguments are in the correct positions. 如需了解详情,请参阅命名参数和可选参数For more information, see Named and optional arguments.

private protected 访问修饰符private protected access modifier

新的复合访问修饰符:private protected 指示可通过包含同一程序集中声明的类或派生类来访问成员。A new compound access modifier: private protected indicates that a member may be accessed by containing class or derived classes that are declared in the same assembly. 虽然 protected internal 允许通过同一程序集中的类或派生类进行访问,但 private protected 限制对同一程序集中声明的派生类的访问。While protected internal allows access by derived classes or classes that are in the same assembly, private protected limits access to derived types declared in the same assembly.

如需了解详情,请参阅语言参考中的访问修饰符For more information, see access modifiers in the language reference.

改进了重载候选项Improved overload candidates

在每个版本中,对重载解析规则进行了更新,以解决多义方法调用具有“明显”选择的情况。In every release, the overload resolution rules get updated to address situations where ambiguous method invocations have an "obvious" choice. 此版本添加了三个新规则,以帮助编译器选取明显的选择:This release adds three new rules to help the compiler pick the obvious choice:

  1. 当方法组同时包含实例和静态成员时,如果方法在不含实例接收器或上下文的情况下被调用,则编译器将丢弃实例成员。When a method group contains both instance and static members, the compiler discards the instance members if the method was invoked without an instance receiver or context. 如果方法在含有实例接收器的情况下被调用,则编译器将丢弃静态成员。The compiler discards the static members if the method was invoked with an instance receiver. 在没有接收器时,编译器将仅添加静态上下文中的静态成员,否则,将同时添加静态成员和实例成员。When there is no receiver, the compiler includes only static members in a static context, otherwise both static and instance members. 当接收器是不明确的实例或类型时,编译器将同时添加两者。When the receiver is ambiguously an instance or type, the compiler includes both. 静态上下文(其中隐式 this 实例接收器无法使用)包含未定义 this 的成员的正文(例如,静态成员),以及不能使用 this 的位置(例如,字段初始值设定项和构造函数初始值设定项)。A static context, where an implicit this instance receiver cannot be used, includes the body of members where no this is defined, such as static members, as well as places where this cannot be used, such as field initializers and constructor-initializers.
  2. 当一个方法组包含类型参数不满足其约束的某些泛型方法时,这些成员将从候选集中移除。When a method group contains some generic methods whose type arguments do not satisfy their constraints, these members are removed from the candidate set.
  3. 对于方法组转换,返回类型与委托的返回类型不匹配的候选方法将从集中移除。For a method group conversion, candidate methods whose return type doesn't match up with the delegate's return type are removed from the set.

你将注意到此更改,因为当你确定哪个方法更好时,你将发现多义方法重载具有更少的编译器错误。You'll only notice this change because you'll find fewer compiler errors for ambiguous method overloads when you are sure which method is better.

启用更高效的安全代码Enabling more efficient safe code

你应能够安全地编写性能与不安全代码一样好的 C# 代码。You should be able to write C# code safely that performs as well as unsafe code. 安全代码可避免错误类,例如缓冲区溢出、杂散指针和其他内存访问错误。Safe code avoids classes of errors, such as buffer overruns, stray pointers, and other memory access errors. 这些新功能扩展了可验证安全代码的功能。These new features expand the capabilities of verifiable safe code. 努力使用安全结构编写更多代码。Strive to write more of your code using safe constructs. 这些功能使其更容易实现。These features make that easier.

以下新增功能支持使安全代码获得更好的性能的主题:The following new features support the theme of better performance for safe code:

  • 无需固定即可访问固定的字段。You can access fixed fields without pinning.
  • 可以重新分配 ref 本地变量。You can reassign ref local variables.
  • 可以使用 stackalloc 数组上的初始值设定项。You can use initializers on stackalloc arrays.
  • 可以对支持模式的任何类型使用 fixed 语句。You can use fixed statements with any type that supports a pattern.
  • 可以使用其他泛型约束。You can use additional generic constraints.
  • 针对实参的 in 修饰符,指定形参通过引用传递,但不通过调用方法修改。The in modifier on parameters, to specify that an argument is passed by reference but not modified by the called method. in 修饰符添加到参数是源兼容的更改Adding the in modifier to an argument is a source compatible change.
  • 针对方法返回的 ref readonly 修饰符,指示方法通过引用返回其值,但不允许写入该对象。The ref readonly modifier on method returns, to indicate that a method returns its value by reference but doesn't allow writes to that object. 如果向某个值赋予返回值,则添加 ref readonly 修饰符是源兼容的更改Adding the ref readonly modifier is a source compatible change, if the return is assigned to a value. readonly 修饰符添加到现有的 ref 返回语句是不兼容的更改Adding the readonly modifier to an existing ref return statement is an incompatible change. 它要求调用方更新 ref 本地变量的声明以包含 readonly 修饰符。It requires callers to update the declaration of ref local variables to include the readonly modifier.
  • readonly struct 声明,指示结构不可变,且应作为 in 参数传递到其成员方法。The readonly struct declaration, to indicate that a struct is immutable and should be passed as an in parameter to its member methods. readonly 修饰符添加到现有的结构声明是二进制兼容的更改Adding the readonly modifier to an existing struct declaration is a binary compatible change.
  • ref struct 声明,指示结构类型直接访问托管的内存,且必须始终分配有堆栈。The ref struct declaration, to indicate that a struct type accesses managed memory directly and must always be stack allocated. ref 修饰符添加到现有 struct 声明是不兼容的更改Adding the ref modifier to an existing struct declaration is an incompatible change. ref struct 不能是类的成员,也不能用于可能在堆上分配的其他位置。A ref struct cannot be a member of a class or used in other locations where it may be allocated on the heap.

可以在编写安全高效的代码中详细了解所有这些更改。You can read more about all these changes in Write safe efficient code.

Ref 局部变量和返回结果Ref locals and returns

此功能允许使用并返回对变量的引用的算法,这些变量在其他位置定义。This feature enables algorithms that use and return references to variables defined elsewhere. 一个示例是使用大型矩阵并查找具有某些特征的单个位置。One example is working with large matrices, and finding a single location with certain characteristics. 下面的方法在矩阵中向该存储返回“引用”:The following method returns a reference to that storage in the matrix:

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

可以将返回值声明为 ref 并在矩阵中修改该值,如以下代码所示:You can declare the return value as a ref and modify that value in the matrix, as shown in the following code:

ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);

C# 语言还有多个规则,可保护你免于误用 ref 局部变量和返回结果:The C# language has several rules that protect you from misusing the ref locals and returns:

  • 必须将 ref 关键字添加到方法签名和方法中的所有 return 语句中。You must add the ref keyword to the method signature and to all return statements in a method.
    • 这清楚地表明,该方法在整个方法中通过引用返回。That makes it clear the method returns by reference throughout the method.
  • 可以将 ref return 分配给值变量或 ref 变量。A ref return may be assigned to a value variable, or a ref variable.
    • 调用方控制是否复制返回值。The caller controls whether the return value is copied or not. 在分配返回值时省略 ref 修饰符表示调用方需要该值的副本,而不是对存储的引用。Omitting the ref modifier when assigning the return value indicates that the caller wants a copy of the value, not a reference to the storage.
  • 不可向 ref 本地变量赋予标准方法返回值。You can't assign a standard method return value to a ref local variable.
    • 因为那将禁止类似 ref int i = sequence.Count(); 这样的语句That disallows statements like ref int i = sequence.Count();
  • 不能将 ref 返回给其生存期不超出方法执行的变量。You can't return a ref to a variable whose lifetime doesn't extend beyond the execution of the method.
    • 这意味着不可返回对本地变量或对类似作用域变量的引用。That means you can't return a reference to a local variable or a variable with a similar scope.
  • ref 局部变量和返回结果不可用于异步方法。ref locals and returns can't be used with async methods.
    • 编译器无法知道异步方法返回时,引用的变量是否已设置为其最终值。The compiler can't know if the referenced variable has been set to its final value when the async method returns.

添加 ref 局部变量和 ref 返回结果可通过避免复制值或多次执行取消引用操作,允许更为高效的算法。The addition of ref locals and ref returns enables algorithms that are more efficient by avoiding copying values, or performing dereferencing operations multiple times.

向返回值添加 ref源兼容的更改Adding ref to the return value is a source compatible change. 现有代码会进行编译,但在分配时复制 ref 返回值。Existing code compiles, but the ref return value is copied when assigned. 调用方必须将存储的返回值更新为 ref 局部变量,从而将返回值存储为引用。Callers must update the storage for the return value to a ref local variable to store the return as a reference.

现在,在对 ref 局部变量进行初始化后,可能会对其重新分配,以引用不同的实例。Now, ref locals may be reassigned to refer to different instances after being initialized. 以下代码现在编译:The following code now compiles:

ref VeryLargeStruct refLocal = ref veryLargeStruct; // initialization
refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

有关详细信息,请参阅有关 ref 返回和 ref 局部变量以及 foreach 的文章。For more information, see the article on ref returns and ref locals, and the article on foreach.

有关详细信息,请参阅 ref 关键字一文。For more information, see the ref keyword article.

条件 ref 表达式Conditional ref expressions

最后,条件表达式可能生成 ref 结果而不是值。Finally, the conditional expression may produce a ref result instead of a value result. 例如,你将编写以下内容以检索对两个数组之一中第一个元素的引用:For example, you would write the following to retrieve a reference to the first element in one of two arrays:

ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);

变量 r 是对 arrotherArr 中第一个值的引用。The variable r is a reference to the first value in either arr or otherArr.

有关详细信息,请参阅语言参考中的条件运算符 (?:)For more information, see conditional operator (?:) in the language reference.

in 参数修饰符in parameter modifier

in 关键字补充了现有的 ref 和 out 关键字,以按引用传递参数。The in keyword complements the existing ref and out keywords to pass arguments by reference. in 关键字指定按引用传递参数,但调用的方法不修改值。The in keyword specifies passing the argument by reference, but the called method doesn't modify the value.

你可以声明按值或只读引用传递的重载,如以下代码所示:You may declare overloads that pass by value or by readonly reference, as shown in the following code:

static void M(S arg);
static void M(in S arg);

按值(前面示例中的第一个)传递的重载比按只读引用传递的重载更好。The by value (first in the preceding example) overload is better than the by readonly reference version. 若要使用只读引用参数调用版本,必须在调用方法前添加 in 修饰符。To call the version with the readonly reference argument, you must include the in modifier when calling the method.

有关详细信息,请参阅有关 in 参数修饰符的文章。For more information, see the article on the in parameter modifier.

更多类型支持 fixed 语句More types support the fixed statement

fixed 语句支持有限的一组类型。The fixed statement supported a limited set of types. 从 C# 7.3 开始,任何包含返回 ref Tref readonly TGetPinnableReference() 方法的类型均有可能为 fixedStarting with C# 7.3, any type that contains a GetPinnableReference() method that returns a ref T or ref readonly T may be fixed. 添加此功能意味着 fixed 可与 System.Span<T> 和相关类型配合使用。Adding this feature means that fixed can be used with System.Span<T> and related types.

有关详细信息,请参阅语言参考中的 fixed 语句一文。For more information, see the fixed statement article in the language reference.

索引 fixed 字段不需要进行固定Indexing fixed fields does not require pinning

请考虑此结构:Consider this struct:

unsafe struct S
{
    public fixed int myFixedField[10];
}

在早期版本的 C# 中,需要固定变量才能访问属于 myFixedField 的整数之一。In earlier versions of C#, you needed to pin a variable to access one of the integers that are part of myFixedField. 现在,以下代码进行编译,而不将变量 p 固定到单独的 fixed 语句中:Now, the following code compiles without pinning the variable p inside a separate fixed statement:

class C
{
    static S s = new S();

    unsafe public void M()
    {
        int p = s.myFixedField[5];
    }
}

变量 p 访问 myFixedField 中的一个元素。The variable p accesses one element in myFixedField. 无需声明单独的 int* 变量。You don't need to declare a separate int* variable. 仍需要 unsafe 上下文。You still need an unsafe context. 在早期版本的 C# 中,需要声明第二个固定的指针:In earlier versions of C#, you need to declare a second fixed pointer:

class C
{
    static S s = new S();

    unsafe public void M()
    {
        fixed (int* ptr = s.myFixedField)
        {
            int p = ptr[5];
        }
    }
}

有关详细信息,请参阅有关 fixed 语句的文章。For more information, see the article on the fixed statement.

stackalloc 数组支持初始值设定项stackalloc arrays support initializers

当你对数组中的元素的值进行初始值设定时,你已能够指定该值:You've been able to specify the values for elements in an array when you initialize it:

var arr = new int[3] {1, 2, 3};
var arr2 = new int[] {1, 2, 3};

现在,可向使用 stackalloc 进行声明的数组应用同一语法:Now, that same syntax can be applied to arrays that are declared with stackalloc:

int* pArr = stackalloc int[3] {1, 2, 3};
int* pArr2 = stackalloc int[] {1, 2, 3};
Span<int> arr = stackalloc [] {1, 2, 3};

有关详细信息,请参阅stackalloc运算符一文。For more information, see the stackalloc operator article.

增强的泛型约束Enhanced generic constraints

现在,可以将类型 System.EnumSystem.Delegate 指定为类型参数的基类约束。You can now specify the type System.Enum or System.Delegate as base class constraints for a type parameter.

现在也可以使用新的 unmanaged 约束来指定类型参数必须是不可为 null 的“非托管类型”You can also use the new unmanaged constraint, to specify that a type parameter must be a non-nullable unmanaged type.

有关详细信息,请参阅有关 where 泛型约束类型参数的约束的文章。For more information, see the articles on where generic constraints and constraints on type parameters.

将这些约束添加到现有类型是不兼容的更改Adding these constraints to existing types is an incompatible change. 封闭式泛型类型可能不再满足这些新约束的要求。Closed generic types may no longer meet these new constraints.

通用的异步返回类型Generalized async return types

从异步方法返回 Task 对象可能在某些路径中导致性能瓶颈。Returning a Task object from async methods can introduce performance bottlenecks in certain paths. Task 是引用类型,因此使用它意味着分配对象。Task is a reference type, so using it means allocating an object. 如果使用 async 修饰符声明的方法返回缓存结果或以同步方式完成,那么额外的分配在代码的性能关键部分可能要耗费相当长的时间。In cases where a method declared with the async modifier returns a cached result, or completes synchronously, the extra allocations can become a significant time cost in performance critical sections of code. 如果这些分配发生在紧凑循环中,则成本会变高。It can become costly if those allocations occur in tight loops.

新语言功能意味着异步方法返回类型不限于 TaskTask<T>voidThe new language feature means that async method return types aren't limited to Task, Task<T>, and void. 返回类型必须仍满足异步模式,这意味着 GetAwaiter 方法必须是可访问的。The returned type must still satisfy the async pattern, meaning a GetAwaiter method must be accessible. 作为一个具体示例,已将 ValueTask 类型添加到 .NET 中,以使用这一新语言功能:As one concrete example, the ValueTask type has been added to .NET to make use of this new language feature:

public async ValueTask<int> Func()
{
    await Task.Delay(100);
    return 5;
}

备注

你需要添加 NuGet 包 System.Threading.Tasks.Extensions 才能使用 ValueTask<TResult> 类型。You need to add the NuGet package System.Threading.Tasks.Extensions > in order to use the ValueTask<TResult> type.

此增强功能对于库作者最有用,可避免在性能关键型代码中分配 TaskThis enhancement is most useful for library authors to avoid allocating a Task in performance critical code.

新的编译器选项New compiler options

新的编译器选项支持 C# 程序的新版本和 DevOps 方案。New compiler options support new build and DevOps scenarios for C# programs.

引用程序集生成Reference assembly generation

有两种新的编译器选项可生成仅引用程序集:ProduceReferenceAssemblyProduceOnlyReferenceAssemblyThere are two new compiler options that generate reference-only assemblies: ProduceReferenceAssembly and ProduceOnlyReferenceAssembly. 链接的文章详细介绍了这些选项和引用程序集。The linked articles explain these options and reference assemblies in more detail.

公共或开放源代码签名Public or Open Source signing

PublicSign 编译器选项指示编译器使用公钥对程序集进行签名。The PublicSign compiler option instructs the compiler to sign the assembly using a public key. 程序集被标记为已签名,但签名取自公钥。The assembly is marked as signed, but the signature is taken from the public key. 此选项使你能够使用公钥在开放源代码项目中构建签名的程序集。This option enables you to build signed assemblies from open-source projects using a public key.

有关详细信息,请参阅 PublicSign 编译器选项一文。For more information, see the PublicSign compiler option article.

pathmappathmap

PathMap 编译器选项指示编译器将生成环境中的源路径替换为映射的源路径。The PathMap compiler option instructs the compiler to replace source paths from the build environment with mapped source paths. PathMap 选项控制由编译器编写入 PDB 文件或为 CallerFilePathAttribute 编写的源路径。The PathMap option controls the source path written by the compiler to PDB files or for the CallerFilePathAttribute.

有关详细信息,请参阅 PathMap 编译器选项一文。For more information, see the PathMap compiler option article.