C# 6.0 中的新增功能
C# 6.0 版本包含许多可提高开发人员工作效率的功能,官方文档见 这里。本文在其基础上添加了一些自己理解及示例代码,如有不正确的地方欢迎指正。
只读自动属性
只读自动属性 提供了更简洁的语法来创建不可变类型。你声明仅具有 get
访问器的自动属性:
public string FirstName { get; }
public string LastName { get; }
只读属性只能在同一个类的构造函数的主体中设置。 尝试在另一种方法中设置只读属性会生成 CS0200 编译错误。
自动属性初始化表达式
自动属性初始值设定项 可让你在属性声明中声明自动属性的初始值。
public ICollection<double> Grades { get; } = new List<double>();
如果设置了 自动属性的初始值 的同时还在 构造函数 中设置了属性值,则构造函数中的赋值会覆盖自动属性的初始值。
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 的代码就可以很清楚的看到属性的赋值过程了。
.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()
通常是理想之选:
public override string ToString() => $"{LastName}, {FirstName}";
也可以将此语法用于只读属性:
public string FullName => $"{FirstName} {LastName}";
在 C# 7.0 中扩展了 expression-bodied 成员,支持在 构造函数(constructor) 、终结器(finalizer) 以及 get 和 set 访问器 上使用 expression-bodied 成员。
using static
using static
增强功能可用于导入单个类的静态方法。指定要使用的类:
using static System.Math;
未使用 using static
时调用需要加上类名:
Console.WriteLine(Math.Abs(-100));
使用 using static
后可以像调用当前类中方法一样调用:
Console.WriteLine(Abs(-100));
Math
不包含任何实例方法。还可以使用 using static
为具有静态和实例方法的类导入类的静态方法。最有用的示例之一是 String
:
using static System.String;
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
方法来访问成员。可以在 委托模式 一文中看到示例。
?.
运算符的规则确保运算符的左侧仅计算一次。它支持许多语法,包括使用事件处理程序的以下示例:
// preferred in C# 6:
this.SomethingHappened?.Invoke(this, eventArgs);
确保左侧只计算一次,这使得你可以在 ?.
的左侧使用任何表达式(包括方法调用)
字符串内插
使用 C# 6,新的字符串内插功能可以在字符串中嵌入表达式。使用 $
作为字符串的开头,并使用 {
和 }
之间的表达式代替序号:
{<interpolationExpression>[,<alignment>][:<formatString>]}
interpolationExpression
生成需要设置格式的结果的表达式。
null
的字符串表示形式为String.Empty
。csharppublic string FullName => $"{FirstName} {LastName}";
alignment
常数表达式,它的值定义表达式结果的字符串表示形式中的最小字符数。 如果值为正,则字符串表示形式为右对齐;如果值为负,则为左对齐。
常数表达式 是一个带符号的整数,指示首选的设置了格式的字段宽度。
如果 alignment 值小于设置了格式的字符串的长度, alignment 将被忽略,并使用设置了格式的字符串的长度作为字段宽度。
如果 alignment 为正数,字段中设置了格式的数据为右对齐;如果 alignment 为负数,字段中的设置了格式的数据为左对齐。
如果需要填充,则使用空白。
如果指定 alignment ,则需要使用逗号。下面的示例定义两个数组,一个包含雇员的姓名,另一个则包含雇员在两周内的工作小时数。复合格式字符串使 20 字符字段中的姓名左对齐,使 5 字符字段中的工作小时数右对齐。请注意*“N1”*标准格式字符串还用于设置带有小数位的小时数格式。
csharpusing 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# 格式字符串 。
csharppublic string GetGradePointPercentage() => $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Average():F2}";
formatString 是适合正在设置格式的对象类型的格式字符串。如果相应对象是数值,指定标准或自定义数字格式字符串;如果相应对象是
DateTime
对象,指定标准或自定义日期和时间格式字符串;或者,如果相应对象是枚举值,指定枚举格式字符串。如果不指定 formatString ,则对数字、日期和时间或者枚举类型使用常规(“G”)格式说明符。如果指定 formatString ,则需要使用冒号。下表列出了 .NET Framework 类库中支持预定义的格式字符串集的类型或类型的类别,并提供指向列出了支持的格式字符串的主题的链接。请注意,字符串格式化是一个可扩展的机制,可使用该机制定义所有现有类型的新的格式字符串,并定义受应用程序定义的类型支持的格式字符串集。有关详细信息,请参阅
IFormattable
和ICustomFormatter
接口主题。
特定区域性设置
通常,可能需要使用 特定区域性设置 生成的字符串的格式。请利用通过字符串内插生成的对象可以隐式转换为 System.FormattableString
这一事实。 FormattableString
实例包含组合格式字符串,以及在将其转换为字符串之前评估表达式的结果。在设置字符串的格式时,可以使用 FormattableString.ToString(IFormatProvider)
方法指定区域性。下面的示例使用德语 (de-DE) 区域性生成字符串。 (德语区域性默认使用 “,” 字符作为小数分隔符,使用 “.” 字符作为千位分隔符。)
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
子句是否可以处理该异常:
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
的其中一个最常见的用途是提供引起异常的符号的名称:
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,现在可以在 catch
或 finally
表达式中使用 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();
}
}
在 catch
和 finally
子句中添加 await
支持的实现细节可确保该行为与同步代码的行为一致。
当在 catch
或 finally
子句中执行的代码引发异常时,执行将在下一个外层块中查找合适的 catch
子句。 如果当前已经存在异常,则该异常将丢失。 catch
和 finally
子句中的 awaited
表达式也会发生同样的情况:搜索合适的 catch
子句,并且当前异常(如果有)将丢失。
备注
鉴于此行为,建议仔细编写
catch
和finally
子句,避免引入新的异常。
使用索引器初始化关联集合
索引初始值设定项 是提高集合初始值设定项与索引用途一致性的两个功能之一。在早期版本的 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 表达式作为参数:
Task.Run(() => DoThings());
C# 6 编译器正确地判断 Task.Run(Func<Task>())
是更好的选择。
确定性的编译器选项
-deterministic 选项指示编译器为同一源文件的后续编译生成完全相同的输出程序集。
默认情况下,每个编译都生成唯一的输出内容。编译器添加一个时间戳和一个随机生成的 GUID 。如果想按字节比较输出以确保各项生成之间的一致性,请使用此选项。
有关详细信息,请参阅 -deterministic 编译器选项 文章。