主构造函数
自动属性初始化表达式尤其适合与主构造函数结合使用。主构造函数为降低常见对象模式的繁琐程度提供了一种方法。此功能自五月以来已显著改进。更新包括:
- 主构造函数的可选实现主体:这将支持此前不受支持的主构造函数参数验证和初始化等。
- 取消字段参数:通过主构造函数参数对字段进行声明。(不将此功能按照已定义方式推出是正确的决定,因为它不再按照 C# 之前矛盾的方式强制遵循特定命名约定。)
- 支持表达式主体函数和属性(本文稍后将进行讨论)。
随着 Web 服务、多层应用程序、数据服务、Web API、JSON 及类似技术的普遍使用,类的一个普遍形式是数据传输对象 (DTO)。DTO 通常不会实现太多功能,而是专注于使数据存储简单化。它对于简单性的关注使得主构造函数极具新引力。例如,请看本示例中所示的固定 Pair 数据结构:
struct Pair<T>(T first, T second) { public T First { get; } = first; public T Second { get; } = second; // Equality operator ... }
构造函数定义 Pair(string first, string second) 已合并到类声明中。这会将构造函数参数指定为 first 和 second(均为类型 T)。属性初始化表达式中也引用了这些参数,并将参数分配给其对应的属性。当您发现此类定义的简单性、对不变性的支持以及必不可少的构造函数(所有属性/字段的初始化表达式)时,您将会了解到它是如何帮助您正确编写代码的。这将导致先前需要不必要的详细级别的常见模式得到显著改进。
主构造函数主体指定对主构造函数执行的操作。这将有助于您在主构造函数上实现通常在构造函数上可以实现的等同功能。例如,改进 Pair<T> 数据结构的可靠性的下一个步骤可能是提供属性验证。此类验证可以确保 Pair.First 的 null 值将无效。现在,CTP3 包括一个主构造函数主体(未声明的构造函数主体),如图 4 中所示。
实现主构造函数主体 struct Pair<T>(T first, T second) { { if (first == null) throw new ArgumentNullException("first"); First = first; // NOTE: Not working in CTP3 } public T First { get; }; // NOTE: Results in compile error for CTP3 public T Second { get; } = second; public int CompareTo(T first, T second) { return first.CompareTo(First) + second.CompareTo(Second); } // Equality operator ... }
为清晰起见,我将主构造函数主体置于类的第一个成员中。但这不是 C# 所要求的。主构造函数主体可以按与其他类成员相关的任意顺序显示。
只读属性的另一个功能尽管在 CTP3 中没有发挥作用,但您可以从构造函数内直接分配这些属性(例如,First = first)。这不仅仅限于主构造函数,而且还适用于所有构造函数成员。
支持自动属性初始化表达式的一个有趣的结果是,它解决了早期版本中出现的需要显式字段声明的多种情况。它没有解决一个最明显的问题,即需要对 setter 进行验证的情况。另一方面,几乎已不需要声明只读字段。现在,无论何时声明只读字段,只要需要该封装级别,您都可以将只读自动属性声明为私有。
CompareTo 方法具有参数 first 和 second,这好像与主构造函数的参数名称重复。由于主构造函数名称在自动属性初始化表达式作用域内,因此,first 和 second 似乎并不明确。幸运的是,实际情况并非如此。作用域规则将依据不同维度而定,而您之前在 C# 中并未看到。
在 C# 6.0 之前,作用域始终由代码内的变量声明放置来确定。参数在其帮助声明的方法中绑定,字段在类中绑定,在 if 语句中声明的变量由 if 语句条件主体绑定。
相比之下,主构造函数参数则由时间来绑定。主构造函数参数仅在执行主构造函数时为“活动”状态。此时间范围在主构造函数主体的情况中很明显。可能对于自动属性初始化表达式的情况不太明显。
但是,与作为 C# 1.0+ 中的类初始化表达式的一部分执行的转换为语句的字段初始化表达式类似,自动属性初始化表达式也通过同样的方式实现。换言之,主构造函数参数的作用域与类初始化表达式和主构造函数主体的生命周期绑定。在自动属性初始化表达式或主构造函数主体外部对主构造函数参数进行任何引用都将产生编译错误。
还有其他几个与主构造函数相关的概念需要牢记。只有主构造函数可以调用基构造函数。您可以使用主构造函数声明后跟的基本(上下文)关键字执行此操作:
class UsbConnectionException( string message, Exception innerException, HidDeviceInfo hidDeviceInfo): Exception (message, innerException) { public HidDeviceInfo HidDeviceInfo { get; } = hidDeviceInfo; }
如果指定其他构造函数,则构造函数调用链必须最后调用主构造函数。这意味着主构造函数不能具有此初始化表达式。假定主构造函数也不是默认构造函数,所有其他构造函数必须具有这些初始化表达式:
public class Patent(string title, string yearOfPublication) { public Patent(string title, string yearOfPublication, IEnumerable<string> inventors) ...this(title, yearOfPublication) { Inventors.AddRange(inventors); } }
希望这些示例有助于展示主构造函数简化了 C#。通过主构造函数,还有机会以简单的方式来执行简单的任务,而不是以复杂的方式来执行简单的任务。它偶尔也会让类包含多个主构造函数和调用链,致使代码不易于阅读。如果您遇到主构造函数语法使代码看起来更为复杂而不是简化代码的情况,那么请不要使用主构造函数。对于 C# 6.0 的所有增强功能,如果您有不喜欢的功能,或某个功能使您的代码不易于阅读,请不要使用该功能。
表达式主体函数和表达式属性
表达式主体函数是 C# 6.0 中的另一个语法精简形式。有一些函数不包括语句体,而是以函数声明后跟表达式的形式来实现。
例如,可以这样向 Pair<T> 类添加 ToString 方法的重写:
public override string ToString() => string.Format("{0}, {1}", First, Second);
表达式主体函数没有什么彻底更改。和 C# 6.0 中的大部分功能一样,它们旨在提供简化的语法,用于实现简单的情况。当然,表达式的返回类型必须与函数声明中定义的返回类型相匹配。在这种情况下,ToString 将返回一个字符串,这同函数实现表达式返回的结果一样。返回 void 或 Task 的方法应通过同样不会返回任何结果的表达式实现。
表达式主体简化不仅仅限于函数,您还可以使用表达式实现只读(仅包含 getter)属性——表达式属性。例如,可以将 Text 成员添加到 FingerPrint 类:
public string Text => string.Format("{0}: {1} - {2} ({3})", TimeStamp, Process, Config, User);
其他功能
有一些功能不再计划针对 C# 6.0 实现:
- 索引属性运算符 ($) 不再可用,并且不针对 C# 6.0 实现。
- 尽管索引成员语法预期在 C# 6.0 的更高版本中回归,但它在 CTP3 中不起作用:
var cppHelloWorldProgram = new Dictionary<int, string> { [10] = "main() {", [20] = " printf(\"hello, world\")", [30] = "}" };
主构造函数中的字段参数不再属于 C# 6.0。
- 二进制数字文本和数字文本中的数字分隔符 (‘_’) 在正式发布之前尚不确定。