Skip to content

C# 6.0 中的新增功能

🏷️ C# C# 新增功能

C# 6.0 版本包含许多可提高开发人员工作效率的功能,官方文档见 这里。本文在其基础上添加了一些自己理解及示例代码,如有不正确的地方欢迎指正。

只读自动属性

只读自动属性 提供了更简洁的语法来创建不可变类型。你声明仅具有 get 访问器的自动属性:

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

只读属性只能在同一个类的构造函数的主体中设置。 尝试在另一种方法中设置只读属性会生成 CS0200 编译错误。

自动属性初始化表达式

自动属性初始值设定项 可让你在属性声明中声明自动属性的初始值。

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

如果设置了 自动属性的初始值 的同时还在 构造函数 中设置了属性值,则构造函数中的赋值会覆盖自动属性的初始值。

csharp
class Program
{
    static void Main(string[] args)
    {
        var staff = new Staff();
        Console.WriteLine(staff.FirstName); // Output: 佳佳
        Console.WriteLine(staff.LastName); // Output: 刘
        Console.ReadLine();
    }
}

class Staff
{
    public Staff()
    {
        FirstName = "佳佳";
        LastName = "刘";
    }
    public string FirstName { get; set; } = "Jiajia";
    public string LastName { get; set; } = "Liu";
}

我们看一下构造函数的 IL 的代码就可以很清楚的看到属性的赋值过程了。

csharp
.method /*06000003*/ public hidebysig specialname rtspecialname 
        instance void  .ctor() cil managed
// SIG: 20 00 01
{
    // 方法在 RVA 0x208b 处开始
    // 代码大小       55 (0x37)
    .maxstack  8
    IL_0000:  /* 02   |                  */ ldarg.0
    IL_0001:  /* 72   | (70)000001       */ ldstr      "Jiajia" /* 70000001 */
    IL_0006:  /* 7D   | (04)000001       */ stfld      string AutoPropertyInitializers.Staff/*02000003*/::'<FirstName>k__BackingField' /* 04000001 */
    IL_000b:  /* 02   |                  */ ldarg.0
    IL_000c:  /* 72   | (70)00000F       */ ldstr      "Liu" /* 7000000F */
    IL_0011:  /* 7D   | (04)000002       */ stfld      string AutoPropertyInitializers.Staff/*02000003*/::'<LastName>k__BackingField' /* 04000002 */
    IL_0016:  /* 02   |                  */ ldarg.0
    IL_0017:  /* 28   | (0A)00000F       */ call       instance void [System.Runtime/*23000001*/]System.Object/*0100000C*/::.ctor() /* 0A00000F */
    IL_001c:  /* 00   |                  */ nop
    IL_001d:  /* 00   |                  */ nop
    IL_001e:  /* 02   |                  */ ldarg.0
    IL_001f:  /* 72   | (70)000017       */ ldstr      bytearray (73 4F 73 4F )                                     // sOsO /* 70000017 */
    IL_0024:  /* 28   | (06)000005       */ call       instance void AutoPropertyInitializers.Staff/*02000003*/::set_FirstName(string) /* 06000005 */
    IL_0029:  /* 00   |                  */ nop
    IL_002a:  /* 02   |                  */ ldarg.0
    IL_002b:  /* 72   | (70)00001D       */ ldstr      bytearray (18 52 )                                           // .R /* 7000001D */
    IL_0030:  /* 28   | (06)000007       */ call       instance void AutoPropertyInitializers.Staff/*02000003*/::set_LastName(string) /* 06000007 */
    IL_0035:  /* 00   |                  */ nop
    IL_0036:  /* 2A   |                  */ ret
} // end of method Staff::.ctor

从 IL 可以发现 自动属性初始化表达式 会被编译器自动的放到构造函数的顶部执行。

Expression-bodied 函数成员

你编写的许多成员是可以作为单个表达式的单个语句。改为编写 expression-bodied 成员。这适用于方法和只读属性。例如,重写 ToString() 通常是理想之选:

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

也可以将此语法用于只读属性:

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

在 C# 7.0 中扩展了 expression-bodied 成员,支持在 构造函数(constructor)终结器(finalizer) 以及 get 和 set 访问器 上使用 expression-bodied 成员。

using static

using static 增强功能可用于导入单个类的静态方法。指定要使用的类:

csharp
using static System.Math;

未使用 using static 时调用需要加上类名:

csharp
Console.WriteLine(Math.Abs(-100));

使用 using static 后可以像调用当前类中方法一样调用:

csharp
Console.WriteLine(Abs(-100));

Math 不包含任何实例方法。还可以使用 using static 为具有静态和实例方法的类导入类的静态方法。最有用的示例之一是 String

csharp
using static System.String;

Null 条件运算符

Null 条件运算符使 null 检查更轻松、更流畅。将成员访问 . 替换为 ?.

csharp
var first = person?.FirstName;

在前面的示例中,如果 Person 对象是 null ,则将变量 first 赋值为 null 。否则,将 FirstName 属性的值分配给该变量。最重要的是,?. 意味着当 person 变量为 null 时,此行代码不会生成 NullReferenceException 。它会短路并返回 null

还可以将 null 条件运算符用于数组或索引器访问。将索引表达式中的 [] 替换为 ?[]

无论 person 的值是什么,以下表达式均返回 string 。通常,将此构造与 “null 合并”运算符 一起使用,以在其中一个属性为 null 时分配默认值。表达式短路时,键入返回的 null 值以匹配整个表达式。

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

还可以将 ?. 用于有条件地调用方法。具有 null 条件运算符的成员函数的最常见用法是用于安全地调用可能为 null 的委托(或事件处理程序)。通过使用 ?. 运算符调用该委托的 Invoke 方法来访问成员。可以在 委托模式 一文中看到示例。

?. 运算符的规则确保运算符的左侧仅计算一次。它支持许多语法,包括使用事件处理程序的以下示例:

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

确保左侧只计算一次,这使得你可以在 ?. 的左侧使用任何表达式(包括方法调用)

字符串内插

使用 C# 6,新的字符串内插功能可以在字符串中嵌入表达式。使用 $ 作为字符串的开头,并使用 {} 之间的表达式代替序号:

csharp
{<interpolationExpression>[,<alignment>][:<formatString>]}
  • interpolationExpression

    生成需要设置格式的结果的表达式。 null 的字符串表示形式为 String.Empty

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

    常数表达式,它的值定义表达式结果的字符串表示形式中的最小字符数。 如果值为正,则字符串表示形式为右对齐;如果值为负,则为左对齐。

    常数表达式 是一个带符号的整数,指示首选的设置了格式的字段宽度。
    如果 alignment 值小于设置了格式的字符串的长度, alignment 将被忽略,并使用设置了格式的字符串的长度作为字段宽度。
    如果 alignment 为正数,字段中设置了格式的数据为右对齐;如果 alignment 为负数,字段中的设置了格式的数据为左对齐。
    如果需要填充,则使用空白。
    如果指定 alignment ,则需要使用逗号。

    下面的示例定义两个数组,一个包含雇员的姓名,另一个则包含雇员在两周内的工作小时数。复合格式字符串使 20 字符字段中的姓名左对齐,使 5 字符字段中的工作小时数右对齐。请注意*“N1”*标准格式字符串还用于设置带有小数位的小时数格式。

    csharp
    using System;
    
    public class Example
    {
        public static void Main()
        {
            string[] names = { "Adam", "Bridgette", "Carla", "Daniel",
                                "Ebenezer", "Francine", "George" };
            decimal[] hours = { 40, 6.667m, 40.39m, 82, 40.333m, 80, 16.75m };
    
            Console.WriteLine("{0,-20} {1,5}\n", "Name", "Hours");
            for (int ctr = 0; ctr < names.Length; ctr++)
                Console.WriteLine("{0,-20} {1,5:N1}", names[ctr], hours[ctr]);
        }
    }
    // The example displays the following output:
    //       Name                 Hours
    //
    //       Adam                  40.0
    //       Bridgette              6.7
    //       Carla                 40.4
    //       Daniel                82.0
    //       Ebenezer              40.3
    //       Francine              80.0
    //       George                16.8
  • formatString;

    受表达式结果类型支持的格式字符串。有关更多信息,请参阅 格式字符串组件 或者 我整理的另一篇博客 C# 格式字符串

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

    formatString 是适合正在设置格式的对象类型的格式字符串。如果相应对象是数值,指定标准或自定义数字格式字符串;如果相应对象是 DateTime 对象,指定标准或自定义日期和时间格式字符串;或者,如果相应对象是枚举值,指定枚举格式字符串。如果不指定 formatString ,则对数字、日期和时间或者枚举类型使用常规(“G”)格式说明符。如果指定 formatString ,则需要使用冒号。

    下表列出了 .NET Framework 类库中支持预定义的格式字符串集的类型或类型的类别,并提供指向列出了支持的格式字符串的主题的链接。请注意,字符串格式化是一个可扩展的机制,可使用该机制定义所有现有类型的新的格式字符串,并定义受应用程序定义的类型支持的格式字符串集。有关详细信息,请参阅 IFormattableICustomFormatter 接口主题。

特定区域性设置

通常,可能需要使用 特定区域性设置 生成的字符串的格式。请利用通过字符串内插生成的对象可以隐式转换为 System.FormattableString 这一事实。 FormattableString 实例包含组合格式字符串,以及在将其转换为字符串之前评估表达式的结果。在设置字符串的格式时,可以使用 FormattableString.ToString(IFormatProvider) 方法指定区域性。下面的示例使用德语 (de-DE) 区域性生成字符串。 (德语区域性默认使用 “,” 字符作为小数分隔符,使用 “.” 字符作为千位分隔符。)

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

要开始使用字符串内插,请参阅 C# 中的字符串内插 交互式教程、字符串内插 一文和 C# 中字符串内插 教程。

异常筛选器

“异常筛选器” 是确定何时应该应用给定的 catch 子句的子句。

如果用于异常筛选器的表达式计算结果为 true ,则 catch 子句将对异常执行正常处理。
如果表达式计算结果为 false ,则将跳过 catch 子句

一种用途是检查有关异常的信息,以确定 catch 子句是否可以处理该异常:

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

nameof 表达式

nameof 表达式的计算结果为符号的名称。每当需要变量、属性或成员字段的名称时,这是让工具正常运行的好办法。 nameof 的其中一个最常见的用途是提供引起异常的符号的名称:

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

另一个用途是用于实现 INotifyPropertyChanged 接口的基于 XAML 的应用程序:

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

CatchFinally 块中的 Await

C# 5 对于可放置 await 表达式的位置有若干限制。使用 C# 6,现在可以在 catchfinally 表达式中使用 await

这通常用于日志记录方案:

csharp
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 支持的实现细节可确保该行为与同步代码的行为一致。

当在 catchfinally 子句中执行的代码引发异常时,执行将在下一个外层块中查找合适的 catch 子句。 如果当前已经存在异常,则该异常将丢失。 catchfinally 子句中的 awaited 表达式也会发生同样的情况:搜索合适的 catch 子句,并且当前异常(如果有)将丢失。

备注

鉴于此行为,建议仔细编写 catchfinally 子句,避免引入新的异常。

使用索引器初始化关联集合

索引初始值设定项 是提高集合初始值设定项与索引用途一致性的两个功能之一。在早期版本的 C# 中,可以将 集合初始值设定项 用于序列样式集合,包括在键值对周围添加括号而得到 Dictionary<TKey,TValue>

csharp
private Dictionary<int, string> messages = 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."}
};

可以将 集合初始值设定项Dictionary<TKey,TValue> 集合和其他类型一起使用,在这种情况下,可访问的 Add 方法接受多个参数。新语法支持使用索引分配到集合中:

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

此功能意味着,可以使用与多个版本中已有的序列容器语法类似的语法初始化关联容器。

集合初始值设定项中的扩展 Add 方法

使集合初始化更容易的另一个功能是对 Add 方法使用扩展方法。添加此功能的目的是进行 Visual Basic 的奇偶校验。

如果自定义集合类的方法具有通过语义方式添加新项的名称,则此功能非常有用。

改进了重载解析

你可能不会注意到这最后一项功能。在以前的一些构造中,以前版本的 C# 编译器可能会发现涉及 lambda 表达式的一些方法不明确。请考虑此方法:

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

在早期版本的 C# 中,使用方法组语法调用该方法将失败:

csharp
Task.Run(DoThings);

早期的编译器无法正确区分 Task.Run(Action)Task.Run(Func<Task>())。在早期版本中,需要使用 lambda 表达式作为参数:

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

C# 6 编译器正确地判断 Task.Run(Func<Task>()) 是更好的选择。

确定性的编译器选项

-deterministic 选项指示编译器为同一源文件的后续编译生成完全相同的输出程序集。

默认情况下,每个编译都生成唯一的输出内容。编译器添加一个时间戳和一个随机生成的 GUID 。如果想按字节比较输出以确保各项生成之间的一致性,请使用此选项。

有关详细信息,请参阅 -deterministic 编译器选项 文章。