您对编程语言设计的运作方式感到非常困惑。
默认值
(非空)整数的默认值一直是 0
,但对我来说,非空字符串的默认值是 null
毫无意义。为什么会这样选择?完全违反了我们一直所使用的非空原则。我认为非空字符串的默认值应该是 String.Empty
。
变量的默认值是C#语言自始至终基本功能之一。 规范定义了默认值:
对于 value_type 类型的变量,默认值与其 default 构造函数的结果相同([请参见]默认构造函数)。
对于 reference_type 类型的变量,默认值为 null
。
从实践角度来看,这是有道理的。因为默认值的基本用法之一是在声明给定类型的新值数组时。由于此定义,运行时可以将分配的数组中的所有位都清零-值类型的默认构造函数始终是所有字段中的全零值,并且null
表示为空引用的全零引用。这实际上是规范中的下一个行。
通常通过在使用之前内存管理器或垃圾收集器将存储器初始化为所有位零来执行初始化。因此,使用所有位零表示空引用非常方便。
现在具有可空引用类型(NRT)功能是在C#8中发布的。这里的选择不是“尽管有NRT,让我们实现默认值为null”,而是“让我们不要浪费时间和资源来彻底重新设计默认关键字的工作方式,因为我们正在引入NRT”。 NRT是针对程序员的注释,按设计它们对运行时没有任何影响。
我认为,不能为引用类型指定默认值与不能在值类型上定义无参数构造函数的情况类似-运行时需要快速的全零默认值,而null
的值对于引用类型来说是合理的默认值。并非所有类型都会有合理的默认值-对于 TcpClient
来说什么是合理的默认值?
如果您想要自己的自定义默认值,请实现静态的 Default
方法或属性,并记录该方法,以便开发人员可以将其用作该类型的默认值。无需更改语言的基本原则。
我的意思是,在C#的实现中,必须规定整数类型int的默认值为0。我们也可以选择1或2作为默认值,但共识是0。因此,当可为空引用类型功能被激活时,我们是否可以指定字符串类型string的默认值为String.Empty呢?
正如我所说,底层是将一块内存清零非常快速和方便的方法。没有运行时组件负责检查给定类型的默认值,并在创建新数组时重复该值,因为那样会非常低效。
你的提议基本上意味着运行时必须在运行时检查字符串的可空元数据,并将所有零值的非空字符串视为空字符串。这将是一个非常深入入手的更改,仅针对空字符串的这一特殊情况进行操作。使用静态分析器警告您将null分配给非空字符串而不是合理的默认值的成本效益要高得多。幸运的是,我们有这样的分析器,即NRT功能,它始终拒绝编译包含此类定义的类。
string Foo { get; set; }
通过发出警告并强制我更改为:
string Foo { get; set; } = "";
(顺便说一下,我建议打开警告视为错误,但这是个人口味问题。)
再次,这对我来说没有意义,我认为 Foo 的默认值应该是 new Foo()(如果没有无参数构造函数则会导致编译错误)。为什么要将本不应为空的对象默认设置为 null?
这将使您无法声明没有默认构造函数的引用类型的数组。大多数基本集合都使用数组作为底层存储,包括 List<T>
。而且这还需要在创建大小为 N
的数组时分配 N
个默认实例,这非常低效。此外,构造函数可能会产生副作用。我不会再思考这将会破坏多少东西,但可以肯定的是,这并不是一项容易的更改。考虑到 NRT 非常复杂(Roslyn 存储库中的 NullableReferenceTypesTests.cs
文件单独就有约 130,000 行代码),引入这样的更改的成本效益...并不太好。
感叹号操作符(!)和可空值类型
编译器对第一行没有任何警告。最近在使用可空引用类型特性时,我发现了 null!
,我原以为编译器也会对第二行不发出警告,但事实并非如此。现在我真的很困惑为什么 Microsoft 决定实现不同的行为。
null
值仅对引用类型和可空值类型有效。可空类型也是在规范中定义的:
可空类型可以表示其基础类型的所有值以及一个额外的 null
值。可空类型写作 T?
,其中 T
是基础类型。此语法是 System.Nullable<T>
的简写形式,两种形式可互换使用。(...) 可空类型 T?
的实例具有两个公共只读属性:
- 一个
HasValue
类型为 bool
的属性
- 一个
Value
类型为 T
的属性。当 HasValue
为 true
时,该实例被称为非空。非空实例包含一个已知的值,并且 Value
返回该值。
你不能把
null
赋值给
int
的原因非常明显——
int
是一个占用32位并表示整数的值类型。而
null
值是一个特殊的引用值,大小为机器字并表示内存中的位置。将
null
赋值给
int
没有任何有意义的语义。为了允许将
null
分配给值类型以表示“无值”情况,
Nullable<T>
专门存在。但请注意,这样做:
int? x = null
Nullable<T>
只是语法糖。如果 Nullable<T>
的值为全0,则表示“没有值”,因为这意味着 HasValue
为 false
。没有任何魔法的 null
值被分配到任何地方,它与说 = default
是一样的,只是创建一个给定类型 T
的新的全零结构体并将其分配。
所以,答案是 - 没有人有意尝试设计不兼容 NRT 的方式。 Nullable 值类型是自 C#2 引入以来就像这样工作的更基本的特性。您提出的工作方式无法转换为合理的实现 - 您是否希望所有值类型都可以为空?然后所有这些值类型都必须具有占用额外字节并且可能会破坏填充的 HasValue
字段(我认为代表 int
的语言作为40位类型而不是32位类型被认为是异端邪说 :))。
感叹号操作符是用于告诉编译器“我知道我正在取消引用可空类型/将 null 赋值给非可空类型,但我比你聪明,我确信不会出错”。它禁用静态分析警告。但它不能魔法般地扩展基础类型以适应 null
值。
摘要
考虑到它完全无法保护非可空引用类型变量中的 null
,这个新功能似乎没有改变任何事情,也没有改善开发人员的生活(与不可为空值类型相反,不可为 null
,因此不需要进行 null 检查)
因此,最终看起来唯一增加的价值只是在于签名。开发人员现在可以明确方法的返回值是否可以为 null
或不是,或者属性是否可以为 null
或不是(例如在表示数据库表的 C# 中,其中 NULL 是允许在列中出现的值)。
来自NRT 的官方文档:
这个新功能比早期版本 C# 中处理引用变量提供了显著的优势,早期版本中无法从变量声明中确定设计意图。编译器对引用类型的空引用异常没有提供安全性 (...) 这些警告在编译时发出。编译器在可为空上下文中不添加任何空检查或其他运行时结构。在运行时,可为空引用和非可为空引用是等效的。
因此,您的说法是正确的,“唯一增加的价值只是在于签名”以及静态分析,这就是我们首先需要签名的原因。这并不是对开发人员生活的改善?请注意您的这一行:
string nonNullableString = default(string);
会发出警告。如果您没有忽略它(或者更好的做法是将"对待警告作为错误"打开),那么您将得到价值——编译器为您发现了代码中的错误。
它能保护您免受在运行时将null
分配给非空引用类型的影响吗?不行。但它能提高开发人员的生产力,这是肯定的。该功能的强大之处在于编译时发出的警告和可为空性分析。如果您忽略 NRT 发出的警告,那就是自己找麻烦。您可以忽略编译器的帮助,但这并不意味着它无用。毕竟,您也可以将整个代码放入 unsafe
上下文中,并使用 C 语言进行编程,但这并不意味着 C# 是无用的,因为您可以规避其安全保证。
string nonNullableString = default(string);
生成的警告,这就是为什么您看不到任何好处。 您明确地将null
存储到非可空变量中,因此编译器会抱怨并告诉您出了问题。 这就是这个功能的好处,特别是当您将警告视为错误时。 - Panagiotis Kanavos<WarningsAsErrors>CS8600;CS8625</WarningsAsErrors>
。我只是对为什么这种情况可能发生感到非常困惑。我们永远无法确定一个非空引用类型不会为空,但对于值类型,我们可以确定它不会为空... - Jérôme MEVEL