C# 6 中的新增功能What's New in C# 6

C# 6.0 版本包含许多可提高开发人员工作效率的功能。The 6.0 release of C# contained many features that improve productivity for developers. 此版本中的功能包括:Features in this release include:

这些功能的总体效果是让你编写的代码更简洁、更具可读性。The overall effect of these features is that you write more concise code that is also more readable. 该语法不像许多常见做法那样繁琐。The syntax contains less ceremony for many common practices. 可以更轻松地看出设计意图。It's easier to see the design intent with less ceremony. 好好了解这些功能有助于你提高工作效率、编写更具可读性的代码,并更专注于核心功能而不是语言的构造。Learn these features well, and you'll be more productive, write more readable code, and concentrate more on your core features than on the constructs of the language.

此主题的其余部分提供有关上述每种功能的详细信息。The remainder of this topic provides details on each of these features.

自动属性增强功能Auto-Property enhancements

通过自动实现属性的语法(通常称为“自动属性”),可轻松创建具有简单 get 和 set 访问器的属性:The syntax for automatically implemented properties (usually referred to as 'auto-properties') made it very easy to create properties that had simple get and set accessors:

public string FirstName { get; set; }
public string LastName { get; set; }

但是,这种简单的语法限制了你可以使用自动属性支持的设计类型。However, this simple syntax limited the kinds of designs you could support using auto-properties. C# 6 改进了自动属性功能,以便用户可以在更多方案中使用它们。C# 6 improves the auto-properties capabilities so that you can use them in more scenarios. 无需频繁地求助于手动声明和操纵支持字段的更详细的语法。You won't need to fall back on the more verbose syntax of declaring and manipulating the backing field by hand so often.

新语法可处理适用于只读属性的方案以及初始化自动属性后的变量存储的方案。The new syntax addresses scenarios for read-only properties, and for initializing the variable storage behind an auto-property.

只读自动属性Read-only auto-properties

只读自动属性提供了更简洁的语法来创建不可变类型。Read-only auto-properties provide a more concise syntax to create immutable types. 在 C# 的早期版本中,创建不可变类型的最直接方法是声明专用资源库:The closest you could get to immutable types in earlier versions of C# was to declare private setters:

public string FirstName { get; private set; }
public string LastName { get; private set; }

使用此语法,编译器并不能确保类型是不可变的。Using this syntax, the compiler doesn't ensure that the type really is immutable. 它仅强制规定不会从类外部的任何代码修改 FirstNameLastName 属性。It only enforces that the FirstName and LastName properties are not modified from any code outside the class.

只读自动属性实现真正的只读行为。Read-only auto-properties enable true read-only behavior. 你声明仅具有 get 访问器的自动属性:You declare the auto-property with only a get accessor:

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

FirstNameLastName 属性只能在构造函数的主体中设置:The FirstName and LastName properties can be set only in the body of a constructor:

public Student(string firstName, string lastName)
{
    if (IsNullOrWhiteSpace(lastName))
        throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));
    FirstName = firstName;
    LastName = lastName;
}

尝试在另一种方法中设置 LastName 会生成 CS0200 编译错误:Trying to set LastName in another method generates a CS0200 compilation error:

public class Student
{
    public string LastName { get;  }

    public void ChangeName(string newLastName)
    {
        // Generates CS0200: Property or indexer cannot be assigned to -- it is read only
        LastName = newLastName;
    }
}

此功能实现用于创建不可变类型和使用更简洁且方便的自动属性语法的真正语言支持。This feature enables true language support for creating immutable types and using the more concise and convenient auto-property syntax.

自动属性初始值设定项Auto-Property Initializers

自动属性初始值设定项可让你在属性声明中声明自动属性的初始值。Auto-Property Initializers let you declare the initial value for an auto-property as part of the property declaration. 在早期版本中,这些属性需要具有资源库,你需要使用该资源库来初始化支持字段使用的数据存储。In earlier versions, these properties would need to have setters and you would need to use that setter to initialize the data storage used by the backing field. 对于包含姓名和学生成绩列表的学生,请考虑此类:Consider this class for a student that contains the name and a list of the student's grades:

public Student(string firstName, string lastName)
{
    FirstName = firstName;
    LastName = lastName;
}

随着此类的增长,你可以包含其他构造函数。As this class grows, you may include other constructors. 每个构造函数都需要初始化此字段,否则将引入错误。Each constructor needs to initialize this field, or you'll introduce errors.

通过 C# 6,可为自动属性声明中的自动属性所使用的存储分配初始值:C# 6 enables you to assign an initial value for the storage used by an auto-property in the auto-property declaration:

public ICollection<double> Grades { get; } = new List<double>();

Grades 成员在声明它的位置处被初始化。The Grades member is initialized where it is declared. 这样,就能更容易地仅执行一次初始化。That makes it easier to perform the initialization exactly once. 初始化是属性声明的一部分,可更轻松地将存储分配等同于 Student 对象的公用接口。The initialization is part of the property declaration, making it easier to equate the storage allocation with public interface for Student objects.

属性初始化表达式可与读/写属性以及只读属性一起使用,如下所示。Property Initializers can be used with read/write properties as well as read-only properties, as shown here.

public Standing YearInSchool { get; set; } = Standing.Freshman;

Expression-bodied 函数成员Expression-bodied function members

我们编写的很多成员的主体只包含一条可以表示为表达式的语句。The body of a lot of members that we write consist of only one statement that can be represented as an expression. 可通过改为编写 expression-bodied 成员来简化该语法。You can reduce that syntax by writing an expression-bodied member instead. 这适用于方法和只读属性。It works for methods and read-only properties. 例如,重写 ToString() 通常是理想之选:For example, an override of ToString() is often a great candidate:

public override string ToString() => $"{LastName}, {FirstName}";

还可以在只读属性中使用 expression-bodied 成员:You can also use expression-bodied members in read-only properties as well:

public string FullName => $"{FirstName} {LastName}";

using staticusing static

using static 增强功能可用于导入单个类的静态方法。The using static enhancement enables you to import the static methods of a single class. 以前,using 语句将所有类型导入命名空间中。Previously, the using statement imported all types in a namespace.

通常,我们在整个代码中使用类的静态方法。Often we use a class' static methods throughout our code. 重复键入类名可能会导致代码的含义难以理解。Repeatedly typing the class name can obscure the meaning of your code. 一个常见的例子是当你编写执行许多数值计算的类时。A common example is when you write classes that perform many numeric calculations. 你的代码中将充满 SinSqrt 以及对 Math 类中不同方法的其他调用。Your code will be littered with Sin, Sqrt and other calls to different methods in the Math class. 新的 using static 语法可以使这些类更简洁、更易读。The new using static syntax can make these classes much cleaner to read. 指定要使用的类:You specify the class you're using:

using static System.Math;

现在,可以使用 Math 类中的任何静态方法而不必限定 Math 类。And now, you can use any static method in the Math class without qualifying the Math class. Math 类是此功能的一个很好的用例,因为它不包含任何实例方法。The Math class is a great use case for this feature because it does not contain any instance methods. 还可以使用 using static 为具有静态和实例方法的类导入类的静态方法。You can also use using static to import a class' static methods for a class that has both static and instance methods. 最有用的示例之一是 StringOne of the most useful examples is String:

using static System.String;

备注

在 static using 语句中必须使用完全限定的类名 System.StringYou must use the fully qualified class name, System.String in a static using statement. 而不能使用 string 关键字。You cannot use the string keyword instead.

现在可以调用 String 类中定义的静态方法,而不必将这些方法限定为该类的成员:You can now call static methods defined in the String class without qualifying those methods as members of that class:

if (IsNullOrWhiteSpace(lastName))
    throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));

static using 功能和扩展方法以有趣的方式进行交互,并且该语言设计包含一些特定处理这些交互的规则。The static using feature and extension methods interact in interesting ways, and the language design included some rules that specifically address those interactions. 目标是尽可能减少现有基本代码(包括你的基本代码)中发生重大更改的可能性。The goal is to minimize any chances of breaking changes in existing codebases, including yours.

仅在使用扩展方法调用语法调用扩展方法(而不是作为静态方法调用)时,扩展方法才在范围内。Extension methods are only in scope when called using the extension method invocation syntax, not when called as a static method. 你在 LINQ 查询中会经常看到这种情况。You'll often see this in LINQ queries. 可以通过导入 Enumerable 来导入 LINQ 模式。You can import the LINQ pattern by importing Enumerable.

using static System.Linq.Enumerable;

这将导入 Enumerable 类中的所有方法。This imports all the methods in the Enumerable class. 但是,扩展方法仅在作为扩展方法调用时才在范围内。However, the extension methods are only in scope when called as extension methods. 如果使用静态方法语法调用扩展方法,则它们不在范围内:They are not in scope if they are called using the static method syntax:

public bool MakesDeansList()
{
    return Grades.All(g => g > 3.5) && Grades.Any();
    // Code below generates CS0103: 
    // The name 'All' does not exist in the current context.
    //return All(Grades, g => g > 3.5) && Grades.Any();
}

这个决定是因为扩展方法通常使用扩展方法调用表达式来调用。This decision is because extension methods are typically called using extension method invocation expressions. 在使用静态方法调用语法调用它们的罕见情况下,是为了解析多义性。In the rare case where they are called using the static method call syntax it is to resolve ambiguity. 在调用中要求使用类名似乎是比较明智的做法。Requiring the class name as part of the invocation seems wise.

static using 还有最后一项功能。There's one last feature of static using. static using 指令还可以导入任何嵌套的类型。The static using directive also imports any nested types. 这样,你可以引用任何嵌套的类型,而无需限定。That enables you to reference any nested types without qualification.

Null 条件运算符Null-conditional operators

Null 值使代码变得复杂。Null values complicate code. 需要检查变量的每个访问,以确保没有取消对 null 的引用。You need to check every access of variables to ensure you are not dereferencing null. Null 条件运算符使这些检查更轻松、更流畅。The null conditional operator makes those checks much easier and fluid.

只需将成员访问 . 替换为 ?.Simply replace the member access . with ?.:

var first = person?.FirstName; 

在前面的示例中,如果 Person 对象是 null,则将变量 first 赋值为 nullIn the preceding example, the variable first is assigned null if the person object is null. 否则,它将被分配 FirstName 属性的值。Otherwise, it gets assigned the value of the FirstName property. 最重要的是,?. 意味着当 person 变量为 null 时,此行代码不会生成 NullReferenceExceptionMost importantly, the ?. means that this line of code does not generate a NullReferenceException when the person variable is null. 它会短路并生成 nullInstead, it short-circuits and produces null.

此外,请注意,无论 person 的值是什么,此表达式均返回 stringAlso, note that this expression returns a string, regardless of the value of person. 在短路的情况下,键入返回的 null 值以匹配整个表达式。In the case of short circuiting, the null value returned is typed to match the full expression.

通常,可以将此构造与 null 合并运算符一起使用,以在其中一个属性为 null 时分配默认值:You can often use this construct with the null coalescing operator to assign default values when one of the properties are null:

first = person?.FirstName ?? "Unspecified";

?. 运算符的右侧操作数不仅限于属性或字段。The right hand side operand of the ?. operator is not limited to properties or fields. 还可以将其用于有条件地调用方法。You can also use it to conditionally invoke methods. 具有 null 条件运算符的成员函数的最常见用法是用于安全地调用可能为 null 的委托(或事件处理程序)。The most common use of member functions with the null conditional operator is to safely invoke delegates (or event handlers) that may be null. 方法是使用 ?. 运算符调用该委托的 Invoke 方法来访问成员。You'll do this by calling the delegate's Invoke method using the ?. operator to access the member. 可以在You can see an example in the
委托模式主题中看到一个示例。delegate patterns topic.

?. 运算符的规则确保运算符的左侧仅计算一次。The rules of the ?. operator ensure that the left-hand side of the operator is evaluated only once. 这很重要,它可实现许多语法,包括使用事件处理程序的示例。This is important and enables many idioms, including the example using event handlers. 让我们从事件处理程序的使用开始。Let's start with the event handler usage. 在以前的 C# 版本中,建议编写与下面类似的代码:In previous versions of C#, you were encouraged to write code like this:

var handler = this.SomethingHappened;
if (handler != null)
    handler(this, eventArgs);

它的推荐度高于以下较简单的语法:This was preferred over a simpler syntax:

// Not recommended
if (this.SomethingHappened != null)
    this.SomethingHappened(this, eventArgs);

重要

前一个示例引入了一个争用条件。The preceding example introduces a race condition. 在针对 null 进行检查时,SomethingHappened 事件可能具有订阅服务器,在引发该事件之前这些订阅服务器可能已被删除。The SomethingHappened event may have subscribers when checked against null, and those subscribers may have been removed before the event is raised. 这会导致引发 NullReferenceExceptionThat would cause a NullReferenceException to be thrown.

在第二个版本中,SomethingHappened 事件处理程序在测试时可能为非 null,但如果其他代码删除处理程序,则在调用事件处理程序时,它可能仍为 null。In this second version, the SomethingHappened event handler might be non-null when tested, but if other code removes a handler, it could still be null when the event handler was called.

编译器生成 ?. 运算符的代码,以确保 ?. 表达式的左侧 (this.SomethingHappened) 计算一次,并且缓存结果:The compiler generates code for the ?. operator that ensures the left side (this.SomethingHappened) of the ?. expression is evaluated once, and the result is cached:

// preferred in C# 6:
this.SomethingHappened?.Invoke(this, eventArgs);

通过确保左侧仅计算一次,可在 ?. 的左侧使用任何表达式(包括方法调用)。即使它们具有副作用,但因为只计算一次,所以副作用只产生一次 。Ensuring that the left side is evaluated only once also enables you to use any expression, including method calls, on the left side of the ?. Even if these have side-effects, they are evaluated once, so the side effects occur only once. 可以在有关事件的内容中看到一个示例。You can see an example in our content on events.

字符串内插String Interpolation

C# 6 包含新语法,用于从格式字符串和表达式编写字符串,可以通过计算这些字符串来生成其他字符串值。C# 6 contains new syntax for composing strings from a format string and expressions that are evaluated to produce other string values.

传统上,需要在类似 string.Format 的方法中使用位置参数:Traditionally, you needed to use positional parameters in a method like string.Format:

public string FullName
{
    get
    {
        return string.Format("{0} {1}", FirstName, LastName);
    }
}

使用 C# 6,新的字符串内插功能可以在格式字符串中嵌入表达式。With C# 6, the new string interpolation feature enables you to embed the expressions in the format string. 只需在字符串前面加上 $Simply preface the string with $:

public string FullName => $"{FirstName} {LastName}";

此初始示例使用替代表达式的属性表达式。This initial example uses property expressions for the substituted expressions. 可以扩展此语法以使用任何表达式。You can expand on this syntax to use any expression. 例如,可以在内插过程中计算学生的成绩平均值:For example, you could compute a student's grade point average as part of the interpolation:

public string GetFormattedGradePoint() =>
    $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Average()}";

运行前面的示例,你会发现 Grades.Average() 的输出的小数位数可能比你需要的多。Running the preceding example, you would find that the output for Grades.Average() might have more decimal places than you would like. 字符串内插语法支持可使用前面的格式设置方法的所有格式字符串。The string interpolation syntax supports all the format strings available using earlier formatting methods. 在大括号内添加格式字符串。You add the format strings inside the braces. 在要设置格式的表达式后面添加 :Add a : following the expression to format:

public string GetGradePointPercentage() =>
    $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Average():F2}";

上一行代码将 Grades.Average() 的值格式设置为具有两位小数的浮点数。The preceding line of code formats the value for Grades.Average() as a floating-point number with two decimal places.

: 始终解释为要设置格式的表达式和格式字符串之间的分隔符。The : is always interpreted as the separator between the expression being formatted and the format string. 当表达式以另一种方式(如条件运算符)使用 : 时,这可能会产生问题:This can introduce problems when your expression uses a : in another way, such as a conditional operator:

public string GetGradePointPercentages() =>
    $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Any() ? Grades.Average() : double.NaN:F2}";

在上一示例中,: 解析为格式字符串的开头,而不是条件运算符的一部分。In the preceding example, the : is parsed as the beginning of the format string, not part of the conditional operator. 在发生此问题的所有情况下,可以用括号将表达式括起来,强制编译器按照你的意图解释该表达式:In all cases where this happens, you can surround the expression with parentheses to force the compiler to interpret the expression as you intend:

public string GetGradePointPercentages() =>
    $"Name: {LastName}, {FirstName}. G.P.A: {(Grades.Any() ? Grades.Average() : double.NaN):F2}";

对可以放在大括号之间的表达式没有任何限制。There aren't any limitations on the expressions you can place between the braces. 可以在内插字符串中执行复杂的 LINQ 查询,以执行计算并显示结果:You can execute a complex LINQ query inside an interpolated string to perform computations and display the result:

public string GetAllGrades() =>
    $@"All Grades: {Grades.OrderByDescending(g => g)
    .Select(s => s.ToString("F2")).Aggregate((partial, element) => $"{partial}, {element}")}";

可以从此示例中看出,甚至可以将字符串内插表达式嵌套在另一个字符串内插表达式中。You can see from this sample that you can even nest a string interpolation expression inside another string interpolation expression. 此示例在生产代码中很有可能比你想的更加复杂。This example is very likely more complex than you would want in production code. 但它说明了该功能的范围。Rather, it is illustrative of the breadth of the feature. 任何 C# 表达式都可以放置在内插字符串的大括号之间。Any C# expression can be placed between the curly braces of an interpolated string.

字符串内插和特定区域性String interpolation and specific cultures

前面部分中显示的所有示例使用执行代码的计算机上的当前区域性和语言设置字符串格式。All the examples shown in the preceding section format the strings using the current culture and language on the machine where the code executes. 通常,可能需要使用特定区域性设置生成的字符串的格式。Often you may need to format the string produced using a specific culture. 为此,请利用通过字符串内插生成的对象可以隐式转换为 FormattableString 这一事实。To do that use the fact that the object produced by a string interpolation can be implicitly converted to FormattableString.

FormattableString 实例包含格式字符串,以及在将其转换为字符串之前评估表达式的结果。The FormattableString instance contains the format string, and the results of evaluating the expressions before converting them to strings. 在设置字符串的格式时,可以使用 FormattableString 的公共方法指定区域性。You can use public methods of FormattableString to specify the culture when formatting a string. 例如,以下示例使用德国区域性生成字符串。For example, the following example produces a string using German culture. (它使用“,”字符作为小数分隔符,使用“.”字符作为千位分隔符。)(It uses the ',' character for the decimal separator, and the '.' character as the thousands separator.)

FormattableString str = $"Average grade is {s.Grades.Average()}";
var gradeStr = str.ToString(new System.Globalization.CultureInfo("de-DE"));

有关详细信息,请参阅字符串内插主题。For more information, see the String interpolation topic.

异常筛选器Exception Filters

C# 6 中的另一个新功能是异常筛选器。Another new feature in C# 6 is exception filters. 异常筛选器是确定何时应该应用给定的 catch 子句的子句。Exception Filters are clauses that determine when a given catch clause should be applied. 如果用于异常筛选器的表达式计算结果为 true,则 catch 子句将对异常执行正常处理。If the expression used for an exception filter evaluates to true, the catch clause performs its normal processing on an exception. 如果表达式计算结果为 false,则将跳过 catch 子句。If the expression evaluates to false, then the catch clause is skipped.

一种用途是检查有关异常的信息,以确定 catch 子句是否可以处理该异常:One use is to examine information about an exception to determine if a catch clause can process the exception:

public static async Task<string> MakeRequest()
{
    WebRequestHandler webRequestHandler = new WebRequestHandler();
    webRequestHandler.AllowAutoRedirect = false;
    using (HttpClient client = new HttpClient(webRequestHandler))
    {
        var stringTask = client.GetStringAsync("https://docs.microsoft.com/en-us/dotnet/about/");
        try
        {
            var responseText = await stringTask;
            return responseText;
        }
        catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
        {
            return "Site Moved";
        }
    }
}

由异常筛选器生成的代码提供了有关已引发且未处理的异常的更好信息。The code generated by exception filters provides better information about an exception that is thrown and not processed. 在将异常筛选器添加到语言之前,需要创建如下所示的代码:Before exception filters were added to the language, you would need to create code like the following:

public static async Task<string> MakeRequest()
{ 
    var client = new System.Net.Http.HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try {
        var responseText = await streamTask;
        return responseText;
    } catch (System.Net.Http.HttpRequestException e)
    {
        if (e.Message.Contains("301"))
            return "Site Moved";
        else
            throw;
    }
}

这两个示例中,引发异常的点发生了更改。The point where the exception is thrown changes between these two examples. 在前一个代码中,使用了 throw 子句,对故障转储的任何堆栈跟踪分析或检查都将显示异常是从 catch 子句中的 throw 语句引发的。In the previous code, where a throw clause is used, any stack trace analysis or examination of crash dumps will show that the exception was thrown from the throw statement in your catch clause. 实际的异常对象将包含原始调用堆栈,但是在此引发点与原始引发点位置之间的调用堆栈中的任何变量的所有其他信息已丢失。The actual exception object will contain the original call stack, but all other information about any variables in the call stack between this throw point and the location of the original throw point has been lost.

对比使用异常筛选器的代码的处理方式:异常筛选器表达式的计算结果为 falseContrast that with how the code using an exception filter is processed: the exception filter expression evaluates to false. 因此,执行决不会进入 catch 子句。Therefore, execution never enters the catch clause. 因为 catch 子句不会执行,不会发生堆栈展开。Because the catch clause does not execute, no stack unwinding takes place. 这表示将保留原始引发位置,以用于以后将会发生的任何调试活动。That means the original throw location is preserved for any debugging activities that would take place later.

每当需要评估异常的字段或属性,而不是仅仅依赖异常类型时,请使用异常筛选器保留更多调试信息。Whenever you need to evaluate fields or properties of an exception, instead of relying solely on the exception type, use an exception filter to preserve more debugging information.

使用异常筛选器的另一种推荐模式是将其用于日志记录例程。Another recommended pattern with exception filters is to use them for logging routines. 这种用法还会利用当异常筛选器计算结果为 false 时,保留异常引发点的方式。This usage also leverages the manner in which the exception throw point is preserved when an exception filter evaluates to false.

日志记录方法将是这样一种方法:其参数为无条件返回 false 的异常:A logging method would be a method whose argument is the exception that unconditionally returns false:

public static bool LogException(this Exception e)
{
    Console.Error.WriteLine($"Exceptions happen: {e}");
    return false;
} 

每当要记录异常时,可以添加一个 catch 子句,并将此方法用作异常筛选器:Whenever you want to log an exception, you can add a catch clause, and use this method as the exception filter:

public void MethodThatFailsSometimes()
{
    try {
        PerformFailingOperation();
    } catch (Exception e) when (e.LogException())
    {
        // This is never reached!
    }
} 

永远不会捕获异常,因为 LogException 方法始终返回 falseThe exceptions are never caught, because the LogException method always returns false. 始终为 false 的异常筛选器意味着可以将此日志记录处理程序放置在任何其他异常处理程序之前:That always false exception filter means that you can place this logging handler before any other exception handlers:

public void MethodThatFailsButHasRecoveryPath()
{
    try {
        PerformFailingOperation();
    } catch (Exception e) when (e.LogException())
    {
        // This is never reached!
    }
    catch (RecoverableException ex)
    {
        Console.WriteLine(ex.ToString());
        // This can still catch the more specific
        // exception because the exception filter
        // above always returns false.
        // Perform recovery here 
    }
}

前一示例中强调了异常筛选器的一个非常重要的方面。The preceding example highlights a very important facet of exception filters. 通过异常筛选器,可实现以下方案:较常规的异常 catch 子句可出现在较具体的 catch 子句前。The exception filters enable scenarios where a more general exception catch clause may appear before a more specific one. 也可以在多个 catch 子句中出现相同的异常类型:It's also possible to have the same exception type appear in multiple catch clauses:

public static async Task<string> MakeRequestWithNotModifiedSupport()
{ 
    var client = new System.Net.Http.HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try {
        var responseText = await streamTask;
        return responseText;
    } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
    {
        return "Site Moved";
    } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("304"))
    {
        return "Use the Cache";
    }
}

另一种建议模式有助于防止 catch 子句在附加调试器时处理异常。Another recommended pattern helps prevent catch clauses from processing exceptions when a debugger is attached. 通过此技术,可以使用调试器运行应用程序,并在引发异常时停止执行。This technique enables you to run an application with the debugger, and stop execution when an exception is thrown.

在代码中添加一个异常筛选器,使任何恢复代码仅在未附加调试器时执行:In your code, add an exception filter so that any recovery code executes only when a debugger is not attached:

public void MethodThatFailsWhenDebuggerIsNotAttached()
{
    try {
        PerformFailingOperation();
    } catch (Exception e) when (e.LogException())
    {
        // This is never reached!
    }
    catch (RecoverableException ex) when (!System.Diagnostics.Debugger.IsAttached)
    {
        Console.WriteLine(ex.ToString());
        // Only catch exceptions when a debugger is not attached.
        // Otherwise, this should stop in the debugger. 
    }
}

在代码中添加之后,将调试器设置为在所有未处理的异常处中断。After adding this in code, you set your debugger to break on all unhandled exceptions. 在调试器下运行程序,每当 PerformFailingOperation() 引发 RecoverableException 时,调试器就会中断。Run the program under the debugger, and the debugger breaks whenever PerformFailingOperation() throws a RecoverableException. 调试器将中断程序,因为不会执行 catch 子句(由于异常筛选器返回 false)。The debugger breaks your program, because the catch clause won't be executed due to the false-returning exception filter.

nameof 表达式nameof Expressions

nameof 表达式的计算结果为符号的名称。The nameof expression evaluates to the name of a symbol. 每当需要变量、属性或成员字段的名称时,这是让工具正常运行的好办法。It's a great way to get tools working whenever you need the name of a variable, a property, or a member field.

nameof 的其中一个最常见的用途是提供引起异常的符号的名称:One of the most common uses for nameof is to provide the name of a symbol that caused an exception:

if (IsNullOrWhiteSpace(lastName))
    throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));

另一个用途是用于实现 INotifyPropertyChanged 接口的基于 XAML 的应用程序:Another use is with XAML based applications that implement the INotifyPropertyChanged interface:

public string LastName
{
    get { return lastName; }
    set
    {
        if (value != lastName)
        {
            lastName = value;
            PropertyChanged?.Invoke(this, 
                new PropertyChangedEventArgs(nameof(LastName)));
        }
    }
}
private string lastName;

相较于使用常量字符串,使用 nameof 运算符的优点是工具可以了解符号。The advantage of using the nameof operator over a constant string is that tools can understand the symbol. 如果使用重构工具重命名符号,会在 nameof 表达式中对其重命名。If you use refactoring tools to rename the symbol, it will rename it in the nameof expression. 常量字符串没有这一优势。Constant strings don't have that advantage. 请在你最喜爱的编辑器中亲自尝试一下:重命名一个变量,任何 nameof 表达式也将更新。Try it yourself in your favorite editor: rename a variable, and any nameof expressions will update as well.

nameof 表达式生成其参数(在前面的示例中为 LastName)的非限定名称,即使使用参数的完全限定名称也是如此:The nameof expression produces the unqualified name of its argument (LastName in the previous examples) even if you use the fully qualified name for the argument:

public string FirstName
{
    get { return firstName; }
    set
    {
        if (value != firstName)
        {
            firstName = value;
            PropertyChanged?.Invoke(this, 
                new PropertyChangedEventArgs(nameof(UXComponents.ViewModel.FirstName)));
        }
    }
}
private string firstName;

nameof 表达式生成 FirstName,而不是 UXComponents.ViewModel.FirstNameThis nameof expression produces FirstName, not UXComponents.ViewModel.FirstName.

Catch 和 Finally 块中的 AwaitAwait in Catch and Finally blocks

C# 5 对于可放置 await 表达式的位置有若干限制。C# 5 had several limitations around where you could place await expressions. 其中一个限制已在 C# 6 中删除。One of those has been removed in C# 6. 现在,可以在 catchfinally 表达式中使用 awaitYou can now use await in catch or finally expressions.

在 catch 和 finally 块中添加 await 表达式可能会使这些表达式的处理方式变得复杂。The addition of await expressions in catch and finally blocks may appear to complicate how those are processed. 让我们添加一个示例对此进行讨论。Let's add an example to discuss how this appears. 在任何异步方法中,都可以在 finally 子句中使用 await 表达式。In any async method, you can use an await expression in a finally clause.

使用 C# 6,还可以在 catch 表达式中使用 await。With C# 6, you can also await in catch expressions. 这通常用于日志记录方案:This is most often used with logging scenarios:

public static async Task<string> MakeRequestAndLogFailures()
{ 
    await logMethodEntrance();
    var client = new System.Net.Http.HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try {
        var responseText = await streamTask;
        return responseText;
    } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
    {
        await logError("Recovered from redirect", e);
        return "Site Moved";
    }
    finally
    {
        await logMethodExit();
        client.Dispose();
    }
}

catchfinally 子句中添加 await 支持的实现细节可确保该行为与同步代码的行为一致。The implementation details for adding await support inside catch and finally clauses ensures that the behavior is consistent with the behavior for synchronous code. 当在 catchfinally 子句中执行的代码引发异常时,执行将在下一个外层块中查找合适的 catch 子句。When code executed in a catch or finally clause throws, execution looks for a suitable catch clause in the next surrounding block. 如果存在当前异常,则该异常将丢失。If there was a current exception, that exception is lost. catchfinally 子句中的 awaited 表达式也会发生同样的情况:搜索合适的 catch,并且当前异常(如果有)将丢失。The same happens with awaited expressions in catch and finally clauses: a suitable catch is searched for, and the current exception, if any, is lost.

备注

鉴于此行为,建议仔细编写 catchfinally 子句,避免引入新的异常。This behavior is the reason it's recommended to write catch and finally clauses carefully, to avoid introducing new exceptions.

索引初始值设定项Index Initializers

索引初始值设定项是提高集合初始值设定项与索引用途一致性的两个功能之一。Index Initializers is one of two features that make collection initializers more consistent with index usage. 在早期版本的 C# 中,只能将集合初始值设定项用于序列样式集合,包括在键值对周围添加括号而得到 Dictionary<TKey,TValue>In earlier releases of C#, you could use collection initializers only with sequence style collections, including Dictionary<TKey,TValue> by adding braces around key and value pairs:

private List<string> messages = new List<string> 
{
    "Page not Found",
    "Page moved, but left a forwarding address.",
    "The web server can't come out to play today."
};

现在,可以将它们用于 Dictionary<TKey,TValue> 集合及类似的类型:Now, you can use them with Dictionary<TKey,TValue> collections and similar types. 新语法支持使用索引分配到集合中:The new syntax supports assignment using an index into the collection:

private Dictionary<int, string> webErrors = new Dictionary<int, string>
{
    [404] = "Page not Found",
    [302] = "Page moved, but left a forwarding address.",
    [500] = "The web server can't come out to play today."
};

此功能意味着,可以使用与多个版本中已有的序列容器语法类似的语法初始化关联容器。This feature means that associative containers can be initialized using syntax similar to what's been in place for sequence containers for several versions.

集合初始值设定项中的扩展 Add 方法Extension Add methods in collection initializers

使集合初始化更容易的另一个功能是对 Add 方法使用扩展方法。Another feature that makes collection initialization easier is the ability to use an extension method for the Add method. 添加此功能的目的是进行 Visual Basic 的奇偶校验。This feature was added for parity with Visual Basic.

如果自定义集合类的方法具有通过语义方式添加新项的名称,则此功能非常有用。The feature is most useful when you have a custom collection class that has a method with a different name to semantically add new items.

例如,请思考以下学生集合:For example, consider a collection of students like this:

public class Enrollment : IEnumerable<Student>
{
    private List<Student> allStudents = new List<Student>();

    public void Enroll(Student s)
    {
        allStudents.Add(s);
    }

    public IEnumerator<Student> GetEnumerator()
    {
        return ((IEnumerable<Student>)allStudents).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable<Student>)allStudents).GetEnumerator();
    }
}

Enroll 方法会添加一个学生。The Enroll method adds a student. 但它不遵循 Add 模式。But it doesn't follow the Add pattern. 在以前的 C# 版本中,你不能对 Enrollment 对象使用集合初始值设定项:In previous versions of C#, you could not use collection initializers with an Enrollment object:

var classList = new Enrollment()
{
    new Student("Lessie", "Crosby"),
    new Student("Vicki", "Petty"),
    new Student("Ofelia", "Hobbs"),
    new Student("Leah", "Kinney"),
    new Student("Alton", "Stoker"),
    new Student("Luella", "Ferrell"),
    new Student("Marcy", "Riggs"),
    new Student("Ida", "Bean"),
    new Student("Ollie", "Cottle"),
    new Student("Tommy", "Broadnax"),
    new Student("Jody", "Yates"),
    new Student("Marguerite", "Dawson"),
    new Student("Francisca", "Barnett"),
    new Student("Arlene", "Velasquez"),
    new Student("Jodi", "Green"),
    new Student("Fran", "Mosley"),
    new Student("Taylor", "Nesmith"),
    new Student("Ernesto", "Greathouse"),
    new Student("Margret", "Albert"),
    new Student("Pansy", "House"),
    new Student("Sharon", "Byrd"),
    new Student("Keith", "Roldan"),
    new Student("Martha", "Miranda"),
    new Student("Kari", "Campos"),
    new Student("Muriel", "Middleton"),
    new Student("Georgette", "Jarvis"),
    new Student("Pam", "Boyle"),
    new Student("Deena", "Travis"),
    new Student("Cary", "Totten"),
    new Student("Althea", "Goodwin")
};

现在可以,但前提是你创建将 Add 映射到 Enroll 的扩展方法:Now you can, but only if you create an extension method that maps Add to Enroll:

public static class StudentExtensions
{
    public static void Add(this Enrollment e, Student s) => e.Enroll(s);
}

你对此功能执行的操作是通过创建扩展方法,将把项添加到集合的任何方法映射到一个名为 Add 方法。What you are doing with this feature is to map whatever method adds items to a collection to a method named Add by creating an extension method.

改进了重载解析Improved overload resolution

你可能不会注意到这最后一项功能。This last feature is one you probably won't notice. 在以前的一些构造中,以前版本的 C# 编译器可能会发现涉及 lambda 表达式的一些方法不明确。There were constructs where the previous version of the C# compiler may have found some method calls involving lambda expressions ambiguous. 请考虑此方法:Consider this method:

static Task DoThings() 
{
     return Task.FromResult(0); 
}

在早期版本的 C# 中,使用方法组语法调用该方法将失败:In earlier versions of C#, calling that method using the method group syntax would fail:

Task.Run(DoThings); 

早期的编译器无法正确区分 Task.Run(Action)Task.Run(Func<Task>())The earlier compiler could not distinguish correctly between Task.Run(Action) and Task.Run(Func<Task>()). 在早期版本中,需要使用 lambda 表达式作为参数:In previous versions, you'd need to use a lambda expression as an argument:

Task.Run(() => DoThings());

C# 6 编译器正确地确定 Task.Run(Func<Task>()) 是更好的选择。The C# 6 compiler correctly determines that Task.Run(Func<Task>()) is a better choice.

确定性的编译器选项Deterministic compiler output

-deterministic 选项指示编译器为同一源文件的后续编译生成完全相同的输出程序集。The -deterministic option instructs the compiler to produce a byte-for-byte identical output assembly for successive compilations of the same source files.

默认情况下,每个编译都生成唯一的输出内容。By default, every compilation produces unique output on each compilation. 编译器添加一个时间戳和一个随机生成的 GUID。The compiler adds a timestamp, and a GUID generated from random numbers. 如果想按字节比较输出以确保各项生成之间的一致性,请使用此选项。You use this option if you want to compare the byte-for-byte output to ensure consistency across builds.

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