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

C# 7.0 向 C# 语言添加了许多新功能:C# 7.0 adds a number of new features to the C# language:

  • out 变量out variables
    • 可以将 out 值内联作为参数声明到使用这些参数的方法中。You can declare out values inline as arguments to the method where they're used.
  • 元组Tuples
    • 可以创建包含多个公共字段的轻量级未命名类型。You can create lightweight, unnamed types that contain multiple public fields. 编译器和 IDE 工具可理解这些类型的语义。Compilers and IDE tools understand the semantics of these types.
  • 弃元Discards
    • 弃元是指在不关心所赋予的值时,赋值中使用的临时只写变量。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.
  • ref 局部变量和返回结果ref locals and returns
    • 方法局部参数和返回值可以是对其他存储的引用。Method local variables and return values can be references to other storage.
  • 本地函数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.
  • 通用的异步返回类型Generalized async return types
    • 使用 async 修饰符声明的方法可以返回除 TaskTask<T> 以外的其他类型。Methods declared with the async modifier can return other types in addition to Task and Task<T>.
  • 数字文本语法改进Numeric literal syntax improvements
    • 新令牌可提高数值常量的可读性。New tokens improve readability for numeric constants.

本文的其余部分概述了每个功能。The remainder of this article provides an overview of each feature. 你将了解每项功能背后的原理。For each feature, you'll learn the reasoning behind it. 将了解语法。You'll learn 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.

out 变量out variables

支持 out 参数的现有语法已在此版本中得到改进。The existing syntax that supports out parameters has been improved in this version. 现在可以在方法调用的参数列表中声明 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 above. 但是,该语言支持使用隐式类型的局部变量: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 another line above.
  • 无需分配初始值。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.

元组Tuples

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# 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 runtime.

在进行元组赋值时,还可以指定赋值右侧的字段的名称: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;

可在元组相关文章中深入了解有关元组的详细信息。You can learn more in depth about tuples in the tuples article.

弃元Discards

通常,在进行元组解构或使用 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 feature that allows you to implement method dispatch on properties other than the type of an object. 你可能已经熟悉基于对象类型的方法分派。You're probably already familiar with method dispatch based on the type of an object. 在面向对象的编程中,虚拟和重写方法提供语言语法来实现基于对象类型的方法分派。In object-oriented programming, virtual and override methods provide language syntax to implement method dispatching based on an object's type. 基类和派生类提供不同的实现。Base and Derived classes provide different implementations. 模式匹配表达式扩展了这一概念,以便你可以通过继承层次结构为不相关的类型和数据元素轻松实现类似的分派模式。Pattern matching expressions extend this concept so that you can easily implement similar dispatch patterns for types and data elements that aren't related through an inheritance hierarchy.

模式匹配支持 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# 中的模式匹配中了解有关模式匹配的更多信息。You can learn more about pattern matching in Pattern Matching in C#.

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 关键字一文。For more information, see the ref keyword article.

本地函数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.";
    }
}

备注

本地函数支持的某些设计也可以使用 lambda 表达式 来完成。Some of the designs that are supported by local functions could also be accomplished using lambda expressions. 感兴趣的人可以阅读有关差异的详细信息Those interested can read more about the differences

更多的 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.

通用的异步返回类型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 framework 中,以使用这一新语言功能:As one concrete example, the ValueTask type has been added to the .NET framework 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.

数字文本语法改进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 above in the binary constant. 数字分隔符可以出现在常量的任何位置。The digit separator can appear anywhere in the constant. 对于十进制数字,通常将其用作千位分隔符:For base 10 numbers, it is common to use it as a thousands separator:

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.