乍一看,提案#2145 似乎是 C# 8 可空引用类型特性的逻辑扩展。其基本思想是,开发人员不需要再显式地向接受非空参数的方法添加参数空值检查。然而,人们对于这个特性的争议很大。
本文试图说明这些选项以及它们的利弊,以便读者能够得出自己的看法。但在此之前,本文将简要说明为什么这在 C# 8 中仍然很重要。
目前,可空引用类型特性只是提供信息。它会警告开发人员在处理空值时的常见错误,但仅在编译时发出警告。当应用程序运行时,所有这些编译时检查都不存在。
此外,在使用反射或 dynamic 时,编译检查根本不起作用。
特定语法:感叹号操作符
原来的建议是使用感叹号操作符! 告诉编译器应添加参数空值检查。
复制代码
// 输入的代码
void Insert(string value!)
{
...
}
// 编译后的代码
void Insert(string value)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
...
}
这个选项的理由是它破坏性小。它只需要对 C#编译器做一个小的修改,并且新语法完全向后兼容。
反对这一选项的理由是:
它是一种适用面很窄的新语法;
在阅读代码时很容易忽略它;
很容易忘;
声明参数不可为空是多余的。
另一个问题是,value! 可能意味着“请检查这个值是否为空”或“不需要检查,我知道它不是空的”,这取决于上下文。为了解决后一个问题,这个提案的一个变体是使用双感叹号操作符(string value!!)。
新属性
另一个选项是增加编译器可以识别的新属性,而不是新语法。
复制代码
void Insert([NotNull] string value)
就 C#而言,影响编译代码的属性并不是什么新东西,所以这可以与现有的模式保持一致。如果我们以前有声明性参数验证,它也会是这样的。
反对这一选项的理由是:
与正在考虑的其他选项相比,它非常啰嗦;
声明参数不可为空是多余的。
编译标识
下一个要考虑的选项是全局编译器标识。当该标识启用时,将检查所有非空参数。
这个选项的好处是你不需要考虑它。一旦启用,检查就会自动添加,这样就不会忘记,也不需要学习特殊的语法。
对此,第一个反对意见是,这可能存在性能方面的考虑。该选项的支持者认为,性能成本微不足道,该特性可以选择性地仅应用于公共方法,但可以对任何方法调用执行空值检查。
反对此特性的另一个理由是,开发人员可能希望抛出不同的异常。与此相反的观点是,除了 ArgumentNullException 之外,他们不应该抛出任何东西。此外,编译器指令可以在需要特殊处理时仅针对一个文件或方法禁用该特性。
最后一个观点最有说服力。这将是编译器标识第二次改变代码的语义。诸如“nullable”之类的编译器标识实际上并不会改变代码的行为方式,它只是一个编译时特性。
这个规则有个例外,就是’checked’编译器标识,它会改变整数溢出的行为。在 C#语言设计人员中,这被认为是一个错误,因为如果不知道编译器级如何设置标识,你就无法判断给定代码段的操作方式。
反对的观点并没有反驳这一点,但是,保持这项更改是使可空引用类型特性接近完成的必要步骤。对此,一些人坚持认为,NRT 从来就不是一个完备的解决方案,为了向后兼容,它不应该影响运行时行为。
外部 AOP 和 IL 织入
术语“IL 织入(IL weaving)”指的是在编译器完成后修改程序集的后处理步骤。这用于面向方面的编程工具,如 PostSharp 和已取消的 Code Contracts 项目。
争论中提到了具体的工具 Fody NullGuard 。 Fody 是遵循 MIT 许可协议的 IL 织入器,以 Mono.Cecil 为基础构建。
反对 IL 织入的理由是它需要第三方工具,不能很好地与 IDE 中运行的静态分析工具协同,会破坏 Edit-and-Continue,降低构建速度。
内部 AOP 或宏系统
有一些关于某种内部 AOP 或宏指令的讨论。这将允许开发人员扩展语言本身,而不是等待 C#的增强。
这次这个选项并没有得到太多的支持。内部 AOP 或宏系统会造成整个工具链的重大变化。此外,它可能会让开发人员创建自己的 C#方言,造成语言割裂。
什么也不做
最后一个选项是什么也不做。最有力的论据是,这仅仅是一个“生活质量”特性,并没有为开发者提供任何新的东西。虽然减少样板文件这点值得赞赏,但它们的负面影响超过了其所带来的好处。
而且,在任何给定的函数中,只需要少量代码来执行空检查。
反驳的观点是,这是 C#中最常见的样板代码示例之一,在示例和生产代码中经常缺失。对此,这一选项的支持者给出的回复是,静态分析工具将检测出大部分(尽管不是全部)空值检查的情况。启用 NRT 后,静态分析检查可以变得更加准确。