C# 6 中的新增功能

C# 6.0 版本包含许多可提高开发人员工作效率的功能。 这些功能的总体效果是让你编写的代码更简洁、更具可读性。 该语法不像许多常见做法那样繁琐。 可以更轻松地看出设计意图。 好好了解这些功能可以帮助你提高生产力,编写更具可读性的代码。 你可以更专注于功能,而不是语言的构造。

只读自动属性

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

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

FirstNameLastName 属性只能在同一个类的构造函数的主体中设置:

public Student(string firstName, string lastName)
{
    if (IsNullOrWhiteSpace(lastName))
        throw new ArgumentException("不能为空", nameof(lastName));
    FirstName = firstName;
    LastName = lastName;
}

尝试在另一种方法中设置 LastName 会生成 CS0200 编译错误:

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

此功能实现用于创建不可变类型的真正语言支持且使用更简洁和方便的自动属性语法。

自动属性初始化表达式

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

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

Grades 成员在声明它的位置处被初始化。 这样,就能更容易地仅执行一次初始化。 初始化是属性声明的一部分,可更轻松地将存储分配等同于 Student 对象的公用接口。

Expression-bodied 函数成员

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

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

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

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

using static

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

using static System.Math;

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

using static System.String;

在 static using 语句中必须使用完全限定的类名 System.String。 而不能使用 string 关键字。

static using 语句导入时,仅在使用扩展方法调用语法调用扩展方法时,扩展方法才在范围内。 作为静态方法调用时,扩展方法不在范围内。 你在 LINQ 查询中会经常看到这种情况。 可以通过导入 EnumerableQueryable 来导入 LINQ 模式。

using static System.Linq.Enumerable;

通常使用扩展方法调用表达式调用扩展方法。 在使用静态方法调用语法对其进行调用的罕见情况下,添加类名称可以解决歧义。

static using 指令还可以导入任何嵌套的类型。 可以引用任何嵌套的类型,而无需限定。

Null 条件运算符

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

var first = person?.FirstName;

在前面的示例中,如果 Person 对象是 null,则将变量 first 赋值为 null。 否则,将 FirstName 属性的值分配给该变量。 最重要的是,?. 意味着当 person 变量为 null 时,此行代码不会生成 NullReferenceException。 它会短路并返回 null。 还可以将 null 条件运算符用于数组或索引器访问。 将索引表达式中的 [] 替换为 ?[]

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

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

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

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

this.SomethingHappened?.Invoke(this, eventArgs);

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

字符串内插

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

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

本示例使用替代表达式的属性。 可以使用任何表达式。 例如,可以在内插过程中计算学生的成绩平均值:

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

上一行代码将 Grades.Average() 的值格式设置为具有两位小数的浮点数。

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

var grades = 0.123456789;
FormattableString str = $"Average grade is {grades}";
str.ToString(new System.Globalization.CultureInfo("de-DE")).Dump();
// Average grade is 0,123456789

异常筛选器

“异常筛选器”是确定何时应该应用给定的 catch 子句的子句 。 如果用于异常筛选器的表达式计算结果为 true,则 catch 子句将对异常执行正常处理。 如果表达式计算结果为 false,则将跳过 catch 子句。 一种用途是检查有关异常的信息,以确定 catch 子句是否可以处理该异常:

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

nameof 表达式

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

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

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

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

Catch 和 Finally 块中的 Await

C# 5 对于可放置 await 表达式的位置有若干限制。 使用 C# 6,现在可以在 catchfinally 表达式中使用 await。 这通常用于日志记录方案:

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>

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 方法接受多个参数。 新语法支持使用索引分配到集合中:

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 表达式的一些方法不明确。 请考虑此方法:

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

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

Task.Run(DoThings);

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

[!code-csharpLambda]

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

确定性的编译器选项

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

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

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