C#

C# 6.0 如何简化、阐明并压缩您的代码

Mark Michaelis

C# 6.0 并不是对 C# 编程的根本性改变。不同于 C# 2.0、C# 3.0 中泛型的引入及其使用 LINQ 编程集合的突破性方法,也不同于 C# 5.0 中异步编程模式的简化,C# 6.0 将不会转换开发。也就是说,在特定情况下,C# 6.0 将改变您编写 C# 代码的方式,由于这些功能非常高效,您很可能会忘记还有另一种对其进行编码的方法。C# 6.0 引入了一些新的快捷语法,时不时可以减少繁琐程度,最终使编写的 C# 代码更为精简。在本文中,我将深入探讨实现所有上述功能的新 C# 6.0 功能集的详细信息。特别是,我将重点关注图 1 所示的构思图中概述的项目。

C# 6.0 构思图
图 1 C# 6.0 构思图

请注意,本文的很多示例均来自我新一版的《Essential C# 6.0》(C# 6.0 本质论)(Addison-Wesley Professional) 一书。

Using Static

在大多数基本的控制台程序中均可利用 C# 6.0 的许多功能。例如,目前在名为 using static 指令的功能中,特定类支持 using,这在全局作用域内呈现不带任何类型前缀的静态方法,如图 2 所示。由于 System.Console 是一个静态类,它提供了一个如何利用这一功能的极好示例。

图 2 Using Static 指令减少了代码中的干扰信息

using System;
using static System.ConsoleColor;
using static System.IO.Directory;
using static System.Threading.Interlocked;
using static System.Threading.Tasks.Parallel;
public static class Program
{
  // ...
  public static void Main(string[] args)
  {
    // Parameter checking eliminated for elucidation.
    EncryptFiles(args[0], args[1]);
  }
  public static int EncryptFiles(
    string directoryPath, string searchPattern = "*")
  {
    ConsoleColor color = ForegroundColor;
    int fileCount = 0;
    try
    {
      ForegroundColor = Yellow
      WriteLine("Start encryption...");
      string[] files = GetFiles(directoryPath, searchPattern,
        System.IO.SearchOption.AllDirectories);
      ForegroundColor = White
      ForEach(files, (filename) =>
      {
        Encrypt(filename);
        WriteLine("\t'{0}' encrypted", filename);
        Increment(ref fileCount);
      });
      ForegroundColor = Yellow
      WriteLine("Encryption completed");
    }
    finally
    {
      ForegroundColor = color;
    }
    return fileCount;
  }
}

在本示例中,存在一些适用于 System.ConsoleColor、System.IO.Directory、System.Threading.Interlocked 和 System.Threading.Tasks.Parallel 的 using static 指令。这些指令可以直接调用以下多种方法、属性和枚举:ForegroundColor、WriteLine、GetFiles、Increment、Yellow、White 以及 ForEach。在每种情况下,不再需要限定静态成员的类型。(针对使用 Visual Studio 2015 预览版或更早版本的用户,该语法不包含在 using 后面添加“static”关键字,因此,语法就只是“using System.Console”之类。此外,直到 Visual Studio 2015 预览版之后,using static 指令除可用于静态类之外,还可用于枚举和结构。

大多数情况下,尽管减少了代码,消除类型限定符并没有显著降低代码的清晰度。由于 WriteLine 是对 GetFiles 的调用,它在控制台程序中相当明显。而且,由于在 System.Threading.Tasks.Parallel 上明显有意添加了 using stastic 指令,ForEach 是在 C# 每个版本中定义平行 foreach 循环的一种方法,看上去(如果您刚好眯着眼睛)越来越像是 C# foreach 语句。

使用 using static 指令要特别注意的就是不要牺牲清晰度。例如,考虑图 3 中定义的加密函数。

图 3 模糊的 Exists 调用(带有 nameof 运算符)

private static void Encrypt(string filename)
  {
    if (!Exists(filename)) // LOGIC ERROR: Using Directory rather than File
    {
      throw new ArgumentException("The file does not exist.", 
        nameof(filename));
    }
    // ...
  }

调用 Exists 看上去似乎正是我们所需要的。但更明确地说,如果事实上这里需要 File.Exists,则调用为 Directory.Exists。换言之,尽管此代码可读性好,但至少在这种情况下不正确,避免 using static 语法可能会好一点。

请注意,如果为 System.IO.Directory 和 System.IO.File 指定 using static 指令,在调用 Exists 时,编译器出现错误,则强制使用类型消除歧义前缀来修改代码,以解决歧义。

using static 指令的其他功能是其使用扩展方法的行为。扩展方法不会移动到全局作用域,通常静态方法也不会。例如,using static ParallelEnumerable 指令不会将 Select 方法放入全局作用域,而且您无法调用 Select(files, (filename) => { ...}). 这一限制是设计使然。首先,扩展方法意在作为对象上的实例方法(例如 files.Select((filename)=>{ ... }))呈现,且这不是调用其作为直接来自于这一类型的静态方法的常规模式。其次,诸如 System.Linq 的一些库的类型为 Enumerable 和 ParallelEnumerable 等,这些类型具有重叠的方法名称(如 Select)。要将所有这些类型添加到全局作用域,会导致 IntelliSense 不必要的混乱,很可能引入一个模糊调用(尽管不是在 System.Linq-based 类的情况下)。

尽管扩展方法将不会被放入到全局作用域,C# 6.0 仍允许 using static 指令中存在带有扩展方法的类。using static 指令与 using(命名空间)指令的效果一样,仅 using static 指令作为目标的特定类除外。换言之,using static 允许开发人员将可用的扩展方法限定在标识的特定类,而不是整个命名空间。例如,请考虑使用图 4 中的代码段。

图 4 只有来自于 ParallelEnumerable 的扩展方法在作用域中

using static System.Linq.ParallelEnumerable;
using static System.IO.Console;
using static System.Threading.Interlocked;
// ...
    string[] files = GetFiles(directoryPath, searchPattern,
      System.IO.SearchOption.AllDirectories);
    files.AsParallel().ForAll( (filename) =>
    {
      Encrypt(filename);
      WriteLine($"\t'{filename}' encrypted");
      Increment(ref fileCount);
    });
// ...

注意,这里不存在 using System.Linq 语句。相反,存在 using static System.Linq.ParallelEnumerable 指令,导致只有来自于 ParallelEnumerable 的扩展方法成为作用域中的扩展方法。System.Linq.Enumerable 等一些类上的所有扩展方法将不可用作扩展方法。例如,files.Select(...) 将编译失败,因为 Select 不在字符串数组的作用域内(或者甚至不在 IEnumerable<字符串> 作用域内)。相反,AsParallel 通过 System.Linq.ParallelEnumerable 而处于作用域内。总之,带有扩展方法的类上的 using static 指令会将该类的扩展方法引入作用域作为扩展方法。(通常,将相同类上的非扩展方法引入全局作用域。)

在一般情况下,最佳做法是将 using static 指令的使用限制在几个类,这几个类在整个 System.Console 或 System.Math 等的作用域中反复(不同于“并行”)使用。同样,在针对枚举使用 using static 时,请确保不带有其类型标识符的枚举项很容易理解。例如,或许我最喜欢的做法是,在单元测试文件中指定 using Microsoft.VisualStudio.TestTools.UnitTesting.Assert 以启用 IsTrue、AreEqual<T>、Fail、IsNotNull 等测试声明调用。

nameof 运算符

图 3 包括 C# 6.0 中的另一新增功能 — nameof 运算符。这是新的上下文关键字,用来标识字符串参数,以提取(在编译时)指定为一个参数的任何标识符的非限定名称的常数。在图 3 中,nameof(filename) 返回“文件名”,即加密方法的参数名称。但 nameof 适用于任何编程标识符。例如,图 5 利用 nameof 将属性名称传递到 INotifyPropertyChanged.PropertyChanged。(顺便说一下,使用 CallerMemberName 属性来检索 PropertyChanged 调用的属性名称仍是检索属性名称的有效方法,请参阅 itl.tc/?p=11661。)

图 5 针对 INotifyPropertyChanged.PropertyChanged 使用 nameof 运算符

public class Person : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  public Person(string name)
  {
    Name = name;
  }
  private string _Name;
  public string Name
  {
    get{ return _Name; }
    set
    {
      if (_Name != value)
      {
        _Name = value;
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
          propertyChanged(this,
            new PropertyChangedEventArgs(nameof(Name)));
        }
      }
    }
  }
  // ...
}
[TestClass]
public class PersonTests
{
  [TestMethod]
  public void PropertyName()
  {
    bool called = false;
    Person person = new Person("Inigo Montoya");
    person.PropertyChanged += (sender, eventArgs) =>
    {
      AreEqual(nameof(CSharp6.Person.Name), eventArgs.PropertyName);
      called = true;
    };
    person.Name = "Princess Buttercup";
    IsTrue(called);
  }   
}

注意,无论是仅提供非限定“名称”(因为该名称在作用域内),还是在测试中使用完全限定的 CSharp6.Person.Name 标识符,这一结果只能是最终的标识符(包含以点分隔的名称的最后一个元素)。

通过利用 nameof 运算符,能够消除绝大多数的“奇妙”字符串(只要它们在作用域中就会引用代码标识符)。这不仅消除了由于奇妙字符串内拼写错误导致的运行时错误(编译器从不对其进行验证),而且还使重命名等重构工具将所有引用更新到名称更改标识符。另外,如果不使用重构工具来更改名称,编译器将发出一个错误来指示不再存在标识符。

字符串插值

不仅可以通过指定异常消息来指明未发现文件,还可以通过显示文件名本身来改进图 3 中的问题。在 C# 6.0 之前的版本中,您会使用 string.Format 来实现此操作,从而将文件名嵌入字符串参数。但复合格式设置可读性不是最好的。例如,对 Person 名称进行格式设置,需要基于图 6 消息分配所示的参数顺序来替代占位符。

图 6 复合字符串格式设置与字符串插值

[TestMethod]
public void InterpolateString()
{
  Person person = new Person("Inigo", "Montoya") { Age = 42 };
  string message =
    string.Format("Hello!  My name is {0} {1} and I am {2} years old.",
    person.FirstName, person.LastName, person.Age);
  AreEqual<string>
    ("Hello!  My name is Inigo Montoya and I am 42 years old.", message);
  string messageInterpolated =
    $"Hello!  My name is {person.FirstName} {person.LastName} and I am
    {person.Age} years old.";
  AreEqual<string>(message, messageInterpolated);
}

注意通过分配到 messageInterpolated 来复合格式设置的替代方法。在此示例中,分配到 messageInterpolated 的表达式是一个带有“$”前缀的字符串参数,大括号标识字符串内联嵌入的代码。在这种情况下,Person 的属性用于使此字符串比复合字符串明显更便于阅读。另外,字符串插值语法减少了如下错误:由参数遵循顺序不当的格式字符串所致的错误,或者由参数完全缺失并引发异常所致的错误。(在 Visual Studio 2015 预览版中,不存在 $ 字符,而每个左大括号前面都需要一条斜线。而 Visual Studio 2015 预览版的后续版本都更新为在字符串参数语法前面使用 $。)

在编译时,转换字符串插值来调用等效的 string.Format 调用。这像以前一样为本地化保留了位置支持(尽管仍使用传统的格式字符串),并没有通过字符串引入任何后编译代码注入。

图 7 还显示字符串插值的两个示例。

图 7 使用字符串插值替代 string.Format

public Person(string firstName, string lastName, int? age=null)
{
  Name = $"{firstName} {lastName}";
  Age = age;
}
private static void Encrypt(string filename)
{
  if (!System.IO.File.Exists(filename))
  {
    throw new ArgumentException(
      $"The file, '{filename}', does not exist.", nameof(filename));
  }
  // ...
}

注意,在第二种情况下,利用了 throw 语句,以及字符串插值和 nameof 运算符。字符串插值是导致 ArgumentException 消息包含文件名(即,“此文件 ‘c:\data\missingfile.txt’ 不存在”)的原因。nameof 运算符用于识别加密参数的名称(“filename”)以及 ArgumentException 构造函数的第二个参数。Visual Studio 2015 完全了解字符串插值语法,为嵌入内插字符串的代码块提供颜色编码和 IntelliSense。

Null 条件运算符

尽管为了清楚起见,在图 2 中消除了参数的 null 检查,但几乎每个接受参数的 Main 方法都要求在调用 Length 成员之前先检查参数是否为 nul,以确定所传递参数的数量。更广泛地说,这是在调用成员之前检查 null 的一个非常通用的模式,以避免 System.NullReferenceException(几乎总是指示编程逻辑中的错误)。由于这一模式的频率很高,C# 6.0 引入了称为 null 条件运算符的“?.”运算符:

public static void Main(string[] args)
{
  switch (args?.Length)
  {
  // ...
  }
}

无论在调用方法或属性(在此情况下为 Length)前此操作数是否为 null,null 条件运算符都会转换为检查。逻辑上等效的显式代码可能是(尽管在 C# 6.0 语法中,args 的值只被计算一次):

(args != null) ? (int?)args.Length : null

Null 条件运算符特别方便之处在于它可以链在一起。例如,如果您调用 string[] names = person?.Name?.Split(' '),则在 Person 和 person.Name 均不为 null 的情况下,将只调用 Split。在链起来后,如果第一个操作数为 null,则不进行表达式计算,在表达式调用链中也不会出现进一步调用。然而,请注意,不要无意间忽视其他 null 条件运算符。例如,看一看 names = person?.Name.Split(' ')。如果存在 Person 实例,但 Name 为 null,则在调用 Split 时会出现 NullReferenceException。这并不意味着您必须使用 null 条件运算符链,而意味着您应多加注意逻辑。例如,在 Person 案例中,如果 Name 经过验证且永不为 null,则无需其他 null 条件运算符。

有关 null 条件运算符需要着重注意的是,在使用一个返回值类型的成员时,它始终返回该类型可以为空的版本。例如,args?.Length 返回 int? 而不仅仅是 int。尽管可能有一些奇怪(与其他运算符的行为比较而言),但只在调用链的尾部出现所返回的可以为空的值类型。结果是,在 Length 上调用点 (“.”) 运算符只允许对 int(而不是 int?)成员的调用。但是,将 args?.Length 封装在括号中(通过括号运算符优先级来强制 int? 结果),将调用 int? 返回并使 Nullable<T> 特定成员(HasValue 和 Value)可用。

Null 条件运算符本身就是一个强大的功能。但将其与委托调用一起使用,就可以解决自 C# 1.0 起就存在的一个 C# 难题。请注意,在图 5 中,在检查值是否为 null 之前,我将 PropertyChange 事件处理程序分配到本地副本 (propertyChanged),然后最终触发此事件。这是调用事件最简单的线程安全的方式,而不会在进行 null 检查与触发事件期间发生取消订阅事件的风险。遗憾的是,这并不直观,我经常遇到不遵循该模式的代码,导致不一致的 NullReferenceExceptions。幸运的是,随着 C# 6.0 中引入了 null 条件运算符,这一问题得到了解决。

使用 C# 6.0,代码段从以下形式:

PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
  propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
}

更改为简单的:

PropertyChanged?.Invoke(propertyChanged(
  this, new PropertyChangedEventArgs(nameof(Name)));

而且,由于事件只是一个委托,总是可以通过 null 条件运算符和 Invoke 来以相同的模式调用委托。此功能可能比 C# 6.0 中的其他任何功能更能改变您在未来编写 C# 代码的方式。在您对委托使用 null 条件运算符之后,您可能不会再使用旧的编码方式(当然,除非您陷在 C# 6.0 之前版本的世界里)。

Null 条件运算符还可以与索引运算符一起使用。例如,在您将它们与 Newtonsoft.JObject 一起使用时,则可以遍历 JSON 对象来检索特定元素,如图 8 中所示。

图 8 控制台颜色配置示例

string jsonText =
    @"{
      'ForegroundColor':  {
        'Error':  'Red',
        'Warning':  'Red',
        'Normal':  'Yellow',
        'Verbose':  'White'
      }
    }";
  JObject consoleColorConfiguration = JObject.Parse(jsonText);
  string colorText = consoleColorConfiguration[
    "ForegroundColor"]?["Normal"]?.Value<string>();
  ConsoleColor color;
  if (Enum.TryParse<ConsoleColor>(colorText, out color))
  {
    Console.ForegroundColor = colorText;
  }

值得注意的是,与 MSCORLIB 中的大多数集合不同的是,如果索引无效,JObject 就不会引发异常。例如,如果 ForegroundColor 不存在,JObject 就会返回 null,而不是引发异常。这很重要,因为大多数情况下,在引发 IndexOutOfRangeException 的集合上几乎无需使用 null 条件运算符,且可能在不存在这类安全性时暗示安全。回到显示 Main 和 args 示例的代码段,请考虑以下因素:

public static void Main(string[] args)
{
  string directoryPath = args?[0];
  string searchPattern = args?[1];
  // ...
}

Null 条件运算符可能给您一种安全的错觉,暗示如果 args 不为 null 则此元素必须存在,这使得该示例很危险。当然,事实并非如此,因为即使 args 不为 null,此元素也可能不存在。由于使用 args?.Length 检查元素计数已经验证了 args 不为 null,则在检查 Length 后对集合执行索引时,您确实无需再使用 null 条件运算符。总之,如果索引运算符针对不存在的索引引发 IndexOutOfRangeException,则避免将 null 条件运算符与索引运算符一起使用。这样做会导致代码合法性的错觉。

结构中的默认构造函数

另一个要了解的 C# 6.0 功能是支持值类型上的默认(无参数)构造函数。这在以前不允许,因为在初始化数组、默认设置类型结构的字段或使用默认运算符初始化实例时,不会调用构造函数。但在 C# 6.0 中,在使用新运算符实例化该值类型时,现允许默认构造函数,请注意只对它们进行调用。数组初始化和默认值的显式赋值(或结构字段类型的隐式初始化)将避开默认构造函数。

要了解如何利用默认构造函数,请参阅图 9 中所示的 ConsoleConfiguration 类的示例。通过 CreateUsingNewIsInitialized 方式中所示的新运算符来对构造函数及其调用赋值,则结构将被完全初始化。正如您所预期的以及图 9 中所演示的,完全支持构造函数链,即一个构造函数可以使用构造函数声明后跟的“this”关键字调用另一个构造函数。

图 9 在值类型上声明默认构造函数

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
public struct ConsoleConfiguration
{
  public ConsoleConfiguration() :
    this(ConsoleColor.Red, ConsoleColor.Yellow, ConsoleColor.White)
  {
    Initialize(this);
  }
  public ConsoleConfiguration(ConsoleColor foregroundColorError,
    ConsoleColor foregroundColorInformation,
    ConsoleColor foregroundColorVerbose)
  {
    // All auto-properties and fields must be set before
    // accessing expression bodied members
    ForegroundColorError = foregroundColorError;
    ForegroundColorInformation = foregroundColorInformation;
    ForegroundColorVerbose = foregroundColorVerbose;
  }
   private static void Initialize(ConsoleConfiguration configuration)
  {
    // Load configuration from App.json.config file ...
  }
  public ConsoleColor ForegroundColorVerbose { get; }
  public ConsoleColor ForegroundColorInformation { get; }
  public ConsoleColor ForegroundColorError { get; }
  // ...
  // Equality implementation excluded for elucidation
}
[TestClass]
public class ConsoleConfigurationTests
{
  [TestMethod]
  public void DefaultObjectIsNotInitialized()
  {
    ConsoleConfiguration configuration = default(ConsoleConfiguration);
    AreEqual<ConsoleColor>(0, configuration.ForegroundColorError);
    ConsoleConfiguration[] configurations = new ConsoleConfiguration[42];
    foreach(ConsoleConfiguration item in configurations)
    {
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorError);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorInformation);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorVerbose);
    }
  }
  [TestMethod]
  public void CreateUsingNewIsInitialized()
  {
    ConsoleConfiguration configuration = new ConsoleConfiguration();
    AreEqual<ConsoleColor>(ConsoleColor.Red,
      configuration.ForegroundColorError);
    AreEqual<ConsoleColor>(ConsoleColor.Yellow,
      configuration.ForegroundColorInformation);
    AreEqual<ConsoleColor>(ConsoleColor.White,
      configuration.ForegroundColorVerbose);
  }
}

关于结构,需要记住的要点是:在调用任何其他实例成员之前,必须完全初始化所有的实例字段和自动属性(因为他们拥有支持字段)。因此,在图 9 中的示例中,在分配所有字段和自动属性之后,构造函数才能调用 Initialize 方法。幸运的是,如果链接的构造函数处理所有必不可少的初始化,并通过“this”调用进行调用,则如图 9 中所示,编译器足够聪明,会检测到不必再次从这个非 this 调用的构造函数主体来初始化数据。

自动属性改进

另请注意,在图 9 中,这三个属性(没有显式字段)全部声明为自动属性(不包括主体)和一个 getter。这些只有 getter 的自动属性是 C# 6.0 的一个功能,声明受只读字段(内部)支持的只读属性。因此,只能在构造函数中修改这些属性。

只有 getter 的自动属性在结构和类声明中均可用,但由于结构固定不变的最佳做法指导原则,它们对结构而言尤其重要。C# 6.0 之前的版本需要大约 6 行声明来声明只读属性并对其进行初始化,现在 C# 6.0 与之不同,所需的就是构造函数中的单行声明和分配。因此,固定不变的结构的声明现在不仅仅是结构的正确编程模式,还是更简洁的模式—是对以前语法所做出的备受赞赏的更改,在进行正确编码时,以前的语法更为费力。

C# 6.0 中引入的第二个自动属性功能是支持初始值设定项。例如,我可以将带有初始值设定项的静态 DefaultConfig 自动属性添加到 ConsoleConfiguration:

// Instance property initialization not allowed on structs.
static private Lazy<ConsoleConfiguration> DefaultConfig{ get; } =
  new Lazy<ConsoleConfiguration>(() => new ConsoleConfiguration());

这种属性将提供单个实例工厂模式来访问默认 ConsoleConfigurtion 实例。请注意,此示例在声明期间利用 System.Lazy<T> 并将实例化为初始值设定项,而不是从构造函数内分配只有 getter 的自动属性。其结果是,在构造函数完成后,Lazy<ConsoleConfiguration> 实例将固定不变,DefaultConfig 调用将始终返回 ConsoleConfiguration 的相同实例。

请注意,在结构实例成员上不允许存在自动属性初始值设定项(尽管在类上确实允许它们)。

表达式主体方法和自动属性

在 C# 6.0 中引入的另一个功能是表达式主体成员。这一功能适用于属性和方法,并能使用箭头操作符 (=>) 将表达式分配到一个属性或方法来替代语句体。例如,由于在前面的示例中,DefaultConfig 属性为私有且类型为 Lazy<T>,因此检索 ConsoleConfiguration 的实际默认实例需要 GetDefault 方法:

static public ConsoleConfiguration GetDefault() => DefaultConfig.Value;

但在这个代码段中,请注意,不存在语句块类型方法主体。相反,此方法只通过带有 Lambda 箭头操作符前缀的表达式(而不是语句)来实现。这旨在提供简单的单行实现,而无需任何繁琐程序,在方法签名中带参数或不带参数实现功能的是:

private static void LogExceptions(ReadOnlyCollection<Exception> innerExceptions) =>
  LogExceptionsAsync(innerExceptions).Wait();

请注意,对于属性,表达式主体只适用于只读(只有 getter)属性。事实上,语法与表达式主体方法几乎完全相同,标识符后面没有括号的表达式主体方法除外。回到之前的 Person 示例,我可以使用表达式主体实现只读 FirstName 和 LastName 属性,如图 10 中所示。

图 10 表达式主体自动属性

public class Person
{
  public Person(string name)
  {
    Name = name;
  }
  public Person(string firstName, string lastName)
  {
    Name = $"{firstName} {lastName}";
    Age = age;
  }
  // Validation ommitted for elucidation
  public string Name {get; set; }
  public string FirstName => Name.Split(' ')[0];
  public string LastName => Name.Split(' ')[1];
  public override string ToString() => "\{Name}(\{Age}";
}

另外,表达式主体属性还可用于索引成员,例如,从内部集合返回项。

字典初始值设定项

对于定义名称值对,字典类型集合意义重大。遗憾的是,语法初始化有点不理想:

{ {"First", "Value1"}, {"Second", "Value2"}, {"Third", "Value3"} }

为了改进这一点,C# 6.0 引入了新的字典赋值类型语法:

Dictionary<string, Action<ConsoleColor>> colorMap =
  new Dictionary<string, Action<ConsoleColor>>
{
  ["Error"] =               ConsoleColor.Red,
  ["Information"] =        ConsoleColor.Yellow,
  ["Verbose"] =            ConsoleColor.White
};

要改进此语法,语言团队引入了赋值运算符作为关联一对使查找(名称)值配对或映射的项的方法。该查找就是字典所声明的索引值(和数据类型)。

异常改进

在 C# 6.0 中,异常也相应地有一些小的语言调整。首先,现在可以在 catch 和 finally 块中使用 await 子句,如图 11 中所示。

图 11 在 Catch 和 Finally 块中使用 Await

public static async Task<int> EncryptFilesAsync(string directoryPath, string searchPattern = "*")
{
  ConsoleColor color = Console.ForegroundColor;
  try
  {
  // ...
  }
  catch (System.ComponentModel.Win32Exception exception)
    if (exception.NativeErrorCode == 0x00042)
  {
    // ...
  }
  catch (AggregateException exception)
  {
    await LogExceptionsAsync(exception.InnerExceptions);
  }
  finally
  {
    Console.ForegroundColor = color;
    await RemoveTemporaryFilesAsync();
  }
}

自从在 C# 5.0 中引入了 await,在 catch 和 finally 块中支持 await 的重要性超出了原先的预期。例如,从 catch 或 finally 块中调用异步方法的模式相当普遍,尤其是在进行清理或日志记录期间。现在,在 C# 6.0 中,这最终成为可能。

第二个异常相关的功能(自 1.0 起,在 Visual Basic 中可用)是支持掌握按照特定异常类型进行筛选的异常筛选器,现在这一功能可以指定 if 子句来进一步限制异常是否将被 catch 块捕获。(有时还可以利用此功能的副作用,如将异常记录为由实际上不执行任何异常处理的“飞逝”的异常。)有关此功能值得注意的一点就是,如果您的应用程序有任何机会可能进行本地化,请避免通过异常消息执行 catch 条件表达式,因为在本地化后不做任何更改的话,它们将不再工作。

总结

关于 C# 6.0 的所有功能,我要提到的最后一点是,尽管这些功能显然需要 C# 6.0 编译器,包括 Visual Studio 2015 或更高版本,但它们不需要 Microsoft .NET Framework 的更新版本。因此,例如,即使您正在对 .NET Framework 4 进行编译,也可以使用 C# 6.0 的功能。上述情况可能发生的原因就是编译器中实现了所有的功能,且不具备任何 .NET Framework 的依赖关系。

通过这一简要概述,我总结一下我对 C# 6.0 的见解。唯一尚未讨论的两个剩余功能是:支持定义自定义的 Add 扩展方法以帮助集合初始值,以及一些细微但改进的重载解析。总之,C# 6.0 没有从根本上更改您的代码,至少没有以泛型或 LINQ 那样的方式来改变您的代码。C# 6.0 所做的只是让正确的编码模式更为简单。委托的 null 条件运算符大概就是上述说法的最佳示例,但同时许多其他功能也都让编码模式更加简单,包括字符串插值、nameof 运算符和(尤其针对只读属性的)自动属性改进。

有关其他信息,这里有一些其他参考:

此外,尽管要到 2015 年第二季度才能面市,请届时查看我新一版的《Essential C# 6.0》(C# 6.0 本质论)一书 (intellitect.com/EssentialCSharp)。

当您阅读本文时,C# 6.0 功能讨论可能已经结束了。但是,有一点毋庸置疑,新的 Microsoft 正在兴起,即一个致力于使用开放源代码最佳做法(允许开发社区在创建优秀软件上进行共享)投资跨平台开发的 Microsoft。因此,您很快就能了解到 C# 7.0 的早期设计讨论,因为这次讨论将在开放源代码论坛中进行。


Mark Michaelis (itl.tc/Mark) 是 IntelliTect 的创始人。他还担任首席技术架构师和培训师。自 1996 年以来,他就成为 Microsoft C#、Visual Studio Team System (VSTS) 和 Windows SDK 方面的 MVP。他在 2007 年就任 Microsoft 区域总监。另外,他还是多个 Microsoft 软件设计评审团队(包括 C#、连通系统部门和 VSTS)的成员。Michaelis 在开发人员大会上发表讲话,还撰写了许多文章和书籍,目前正在撰写新一版的《Essential C#》(C# 本质论)(Addison-Wesley Professional)。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Mads Torgersen