C# 6 新功能概述

C# 语言版本 6 会持续发展该语言,以减少样板代码、提高明确度和一致性。 更简洁的初始化语法,能够在 catch/finally 块中使用 await,null 条件 ? 运算符特别有用。

注意

有关 C# 语言最新版本(版本 7)的信息,请参阅文章 C# 7.0 的新增功能

本文档介绍了 C# 6 的新功能。 Mono 编译器完全支持此版本,开发人员可以在所有 Xamarin 目标平台上开始使用新功能。

C# 6 中的新增功能视频

使用 C# 6

Visual Studio for Mac 的所有最新版本均使用 C# 6 编译器。 使用命令行编译器的用户应确认 mcs --version 返回 4.0 或更高版本。 Visual Studio for Mac 用户可以通过参考“关于 Visual Studio for Mac > Visual Studio for Mac > 显示详细信息”来检查是否安装了 Mono 4(或更高版本)

更少的样板

using static

枚举和某些类(例如 System.Math)主要是静态值和函数的容器。 在 C# 6 中,可以使用单个 using static 语句导入类型的所有静态成员。 比较 C# 5 和 C# 6 中的典型三角函数:

// Classic C#
class MyClass
{
    public static Tuple<double,double> SolarAngleOld(double latitude, double declination, double hourAngle)
    {
        var tmp = Math.Sin (latitude) * Math.Sin (declination) + Math.Cos (latitude) * Math.Cos (declination) * Math.Cos (hourAngle);
        return Tuple.Create (Math.Asin (tmp), Math.Acos (tmp));
    }
}

// C# 6
using static System.Math;

class MyClass
{
    public static Tuple<double, double> SolarAngleNew(double latitude, double declination, double hourAngle)
    {
        var tmp = Asin (latitude) * Sin (declination) + Cos (latitude) * Cos (declination) * Cos (hourAngle);
        return Tuple.Create (Asin (tmp), Acos (tmp));
    }
}

using static 不会使公共 const 字段(例如 Math.PIMath.E)可直接访问:

for (var angle = 0.0; angle <= Math.PI * 2.0; angle += Math.PI / 8) ... 
//PI is const, not static, so requires Math.PI

将静态方法与扩展方法配合使用

using static 工具的运行方式与扩展方法略有不同。 尽管扩展方法是使用 static 编写的,但如果没有要操作的实例,它们就没有意义。 因此,当 using static 与定义扩展方法的类型配合使用时,扩展方法将在其目标类型(方法的 this 类型)上可用。 例如,using static System.Linq.Enumerable 可用于扩展 IEnumerable<T> 对象的 API,而无需引入所有 LINQ 类型:

using static System.Linq.Enumerable;
using static System.String;

class Program
{
    static void Main()
    {
        var values = new int[] { 1, 2, 3, 4 };
        var evenValues = values.Where (i => i % 2 == 0);
        System.Console.WriteLine (Join(",", evenValues));
    }
}

以上示例演示了行为上的差异:扩展方法 Enumerable.Where 与数组关联,而静态方法 String.Join 可以在不引用 String 类型的情况下调用。

nameof 表达式

有时,你需要引用针对变量或字段指定的名称。 在 C# 6 中,nameof(someVariableOrFieldOrType) 将返回字符串 "someVariableOrFieldOrType"。 例如,当引发 ArgumentException 时,你很可能想要指出哪个参数无效:

throw new ArgumentException ("Problem with " + nameof(myInvalidArgument))

nameof 表达式的主要优点是它们已经过类型检查并且与工具驱动的重构兼容。 在使用 string 动态关联类型的情况下,nameof 表达式的类型检查特别受欢迎。 例如,在 iOS 中,string 用于指定在 UITableView 中创建 UITableViewCell 对象原型时所用的类型。 nameof 可以确保此关联不会由于拼写错误或草率的重构而失败:

public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
    var cell = tableView.DequeueReusableCell (nameof(CellTypeA), indexPath);
    cell.TextLabel.Text = objects [indexPath.Row].ToString ();
    return cell;
}

尽管可以将限定名称传递给 nameof,但只会返回最后一个元素(在最后一个 . 之后)。 例如,可以在 Xamarin.Forms 中添加数据绑定:

var myReactiveInstance = new ReactiveType ();
var myLabelOld.BindingContext = myReactiveInstance;
var myLabelNew.BindingContext = myReactiveInstance;
var myLabelOld.SetBinding (Label.TextProperty, "StringField");
var myLabelNew.SetBinding (Label.TextProperty, nameof(ReactiveType.StringField));

SetBinding 的两次调用传递相同的值:nameof(ReactiveType.StringField)"StringField",而不是最初预期的 "ReactiveType.StringField"

Null 条件运算符

C# 的早期更新引入了可为 null 的类型和 null 合并运算符 ?? 的概念,以减少处理可为 null 的值时的样板代码量。 C# 6 通过“null 条件运算符”?. 延续了这一主题。 对表达式右侧的对象使用时,如果对象不是 null,则 null 条件运算符将返回成员值,否则返回 null

var ss = new string[] { "Foo", null };
var length0 = ss [0]?.Length; // 3
var length1 = ss [1]?.Length; // null
var lengths = ss.Select (s => s?.Length ?? 0); //[3, 0]

length0length1 都推理为类型 int?

以上示例中的最后一行显示了 ? null 条件运算符与 ?? null 合并运算符的组合。 新的 C# 6 null 条件运算符在数组中的第二个元素上返回 null,此时 null 合并运算符启动并向 lengths 数组提供 0(当然,这是否合适取决于具体的问题)。

null 条件运算符可以大幅减少许多应用程序中所需的样板 null 检查量。

由于歧义,null 条件运算符存在一些限制。 你不能像使用 delegate 那样紧接着在 ? 后面添加带括号的参数列表:

SomeDelegate?("Some Argument") // Not allowed

不过,Invoke 可用于将 ? 从参数列表中分离,并且相对于样板文件的 null 检查块仍然有显著的改进:

public event EventHandler HandoffOccurred;
public override bool ContinueUserActivity (UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
    HandoffOccurred?.Invoke (this, userActivity.UserInfo);
    return true;
}

字符串内插

String.Format 函数在传统上使用索引作为格式字符串中的占位符,例如 String.Format("Expected: {0} Received: {1}.", expected, received。 当然,添加新值始终涉及到一个烦人的小任务,即统计参数、对占位符重新编号,以及将新参数按正确的顺序插入到参数列表中。

C# 6 的新字符串内插功能在 String.Format 的基础上得到了极大改进。 现在,可以直接在前缀为 $ 的字符串中命名变量。 例如:

$"Expected: {expected} Received: {received}."

当然,会检查变量,拼写错误或不可用的变量将导致编译器错误。

占位符不需要是简单变量,它们可以是任意表达式。 在这些占位符中,可以使用引号且无需转义这些引号。 例如,请注意以下内容中的 "s"

var s = $"Timestamp: {DateTime.Now.ToString ("s", System.Globalization.CultureInfo.InvariantCulture )}"

字符串内插支持 String.Format 的对齐和格式语法。 在前面你编写的是 {index, alignment:format},而在 C# 6 中可以编写 {placeholder, alignment:format}

using static System.Linq.Enumerable;
using System;

class Program
{
    static void Main ()
    {
        var values = new int[] { 1, 2, 3, 4, 12, 123456 };
        foreach (var s in values.Select (i => $"The value is { i,10:N2}.")) {
            Console.WriteLine (s);
        }
    Console.WriteLine ($"Minimum is { values.Min(i => i):N2}.");
    }
}

这会生成:

The value is       1.00.
The value is       2.00.
The value is       3.00.
The value is       4.00.
The value is      12.00.
The value is 123,456.00.
Minimum is 1.00.

字符串内插是 String.Format 的语法糖:它不能与 @"" 字符串字面量一起使用,并且与 const 不兼容,即使不使用占位符也是如此:

const string s = $"Foo"; //Error : const requires value

在使用字符串内插生成函数参数的常见用例中,仍然需要小心转义、编码和区域性问题。 当然,SQL 和 URL 查询对于清理至关重要。 与 String.Format 一样,字符串内插使用 CultureInfo.CurrentCulture。 使用 CultureInfo.InvariantCulture 会有点繁琐:

Thread.CurrentThread.CurrentCulture  = new CultureInfo ("de");
Console.WriteLine ($"Today is: {DateTime.Now}"); //"21.05.2015 13:52:51"
Console.WriteLine ($"Today is: {DateTime.Now.ToString(CultureInfo.InvariantCulture)}"); //"05/21/2015 13:52:51"

初始化

C# 6 提供了许多简洁的方法来指定属性、字段和成员。

自动属性初始化

现在能够像初始化字段那样以简洁的方式初始化自动属性。 不可变的自动属性只能用 getter 来编写:

class ToDo
{
    public DateTime Due { get; set; } = DateTime.Now.AddDays(1);
    public DateTime Created { get; } = DateTime.Now;

在构造函数中,可以设置仅限 getter 的自动属性的值:

class ToDo
{
    public DateTime Due { get; set; } = DateTime.Now.AddDays(1);
    public DateTime Created { get; } = DateTime.Now;
    public string Description { get; }

    public ToDo (string description)
    {
        this.Description = description; //Can assign (only in constructor!)
    }

自动属性的这种初始化既是一种节省空间的通用功能,也是希望强调其对象不可变性的开发人员的福音。

索引初始值设定项

C# 6 引入了索引初始化表达式,它允许在具有索引器的类型中设置键和值。 通常,这适用于 Dictionary 样式的数据结构:

partial void ActivateHandoffClicked (WatchKit.WKInterfaceButton sender)
{
    var userInfo = new NSMutableDictionary {
        ["Created"] = NSDate.Now,
        ["Due"] = NSDate.Now.AddSeconds(60 * 60 * 24),
        ["Task"] = Description
    };
    UpdateUserActivity ("com.xamarin.ToDo.edit", userInfo, null);
    statusLabel.SetText ("Check phone");
}

表达式为主体的函数成员

Lambda 函数具有多种优势,其中之一就是节省空间。 同样,表达式为主体的类成员允许以比 C# 6 早期版本更简洁的方式表达小函数。

表达式为主体的函数成员使用 lambda arrow 语法而不是传统的块语法:

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

请注意,lambda-arrow 语法不使用显式的 return。 对于返回 void 的函数,表达式也必须是语句:

public void Log(string message) => System.Console.WriteLine($"{DateTime.Now.ToString ("s", System.Globalization.CultureInfo.InvariantCulture )}: {message}");

表达式为主体的成员仍然遵守方法支持 async、但属性不支持的规则:

//A method, so async is valid
public async Task DelayInSeconds(int seconds) => await Task.Delay(seconds * 1000);
//The following property will not compile
public async Task<int> LeisureHours => await Task.FromResult<char> (DateTime.Now.DayOfWeek.ToString().First()) == 'S' ? 16 : 5;

异常

对此没有办法:异常处理很难正确执行。 C# 6 中的新功能使异常处理变得更加灵活且一致。

异常筛选器

根据定义,异常在不寻常的情况下发生,并且很难对特定类型的异常的所有可能发生方式进行推理和编码。 C# 6 引入了使用运行时评估筛选器来保护执行处理程序的功能。 这是通过在正常的 catch(ExceptionType) 声明后添加 when (bool) 模式来实现的。 在下面,筛选器区分与 date 参数相关的分析错误和其他分析错误。

public void ExceptionFilters(string aFloat, string date, string anInt)
{
    try
    {
        var f = Double.Parse(aFloat);
        var d = DateTime.Parse(date);
        var n = Int32.Parse(anInt);
    } catch (FormatException e) when (e.Message.IndexOf("DateTime") > -1) {
        Console.WriteLine ($"Problem parsing \"{nameof(date)}\" argument");
    } catch (FormatException x) {
        Console.WriteLine ("Problem parsing some other argument");
    }
}

await in catch...finally…

C# 5 中引入的 async 功能彻底改变了该语言的游戏规则。 在 C# 5 中,catchfinally 块中不允许使用 await,考虑到 async/await 功能的值,这很麻烦。 C# 6 消除了此限制,允许通过程序一致地等待异步结果,如以下代码片段中所示:

async void SomeMethod()
{
    try {
        //...etc...
    } catch (Exception x) {
        var diagnosticData = await GenerateDiagnosticsAsync (x);
        Logger.log (diagnosticData);
    } finally {
        await someObject.FinalizeAsync ();
    }
}

总结

C# 语言不断发展,以提高开发人员的工作效率,同时促进良好实践和支持工具。 本文档概述了 C# 6 中的新语言功能,并简要演示了如何使用它们。