C#
C# 6.0 如何简化、阐明并压缩您的代码
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 所示的构思图中概述的项目。
图 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 运算符和(尤其针对只读属性的)自动属性改进。
有关其他信息,这里有一些其他参考:
- Mads Torgersen 的“C# 6.0 的新增功能”(视频):bit.ly/CSharp6Mads
- 在我撰写本文时,Mark Michaelis 介绍 6.0 更新的 C# 博客:itl.tc/csharp
- C# 6.0 语言讨论:roslyn.codeplex.com/discussions
此外,尽管要到 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