问题描述
这不是我关于可空引用类型的第一个问题,因为我已经经历了几个月。但是,我经历的越多,我就越困惑,并且看到该功能所带来的价值也就越少。
以该代码为例
string? nullableString = default(string?);
string nonNullableString = default(string);
int? nullableInt = default(int?);
int nonNullableInt = default(int);
执行该操作会得到:
nullableString
=>null
nonNullableString
=>null
nullableInt
=>null
nonNullableInt
=>0
(不可为空)整数的默认值始终为0
,但
对我来说,不可为空的字符串的默认值为null
没有意义。
为什么选择这个?这与我们一直习惯的不可为空的原则相反。
我认为默认的不可为空的字符串的默认值应该是String.Empty
。
我的意思是必须在C#实现的某个深处指定0
是int
的默认值。我们也可以选择1
或2
,但没有,共识是0
。因此,当可空引用类型功能被激活时,我们不能仅将string
的默认值指定为String.Empty
吗?此外,微软似乎希望在不久的将来通过.NET 5项目默认激活它,因此该功能将成为 normal 行为。
现在使用对象的相同示例:
Foo? nullableFoo = default(Foo?);
Foo nonNullableFoo = default(Foo);
这给出了:
nullableFoo
=>null
nonNullableFoo
=>null
同样,这对我来说没有意义,我认为Foo
的默认值应为new Foo()
(如果没有可用的无参数构造函数,则会产生编译错误)。
为什么默认情况下将不应该为null的对象设置为null?
现在进一步扩展这个问题
string nonNullableString = null;
int nonNullableInt = null;
编译器会针对第一行发出警告,如果在我们的.csproj
文件<WarningsAsErrors>CS8600</WarningsAsErrors>
中进行简单配置,则该行可能会转换为错误。
并按预期给出了第二行的编译错误。
因此,不可为空的值类型与不可为空的引用类型之间的行为是不相同的,但这是可以接受的,因为我可以重写它。
但是这样做的时候:
string nonNullableString = null!;
int nonNullableInt = null!;
第一行编译器就可以了,完全没有警告。
我最近在体验可为空的引用类型功能时发现了null!
,并且我希望编译器也能适用于第二行,但事实并非如此。现在,我对于微软为何决定实施不同的行为感到非常困惑。
考虑到它不能完全防止将null
包含在不可为空的引用类型变量中,似乎此新功能不会改变任何东西,也根本不会改善开发人员的生活(相对不能为空的非空值类型,因此不需要进行空检查)
因此,最后似乎唯一增加的价值仅仅是签名。现在,开发人员可以清楚地知道方法的返回值是否可以为null或属性是否可以为null(例如,在数据库表的C#表示中,其中NULL
是列中的允许值) )。
除了我看不到如何有效使用此新功能外,能否请您给我其他有关如何使用可为空的引用类型的有用示例? 我真的很想充分利用此功能来改善开发人员的生活,但我真的不知道如何...
谢谢
解决方法
再次对我来说这没有意义,我认为Foo的默认值应该是new Foo()(如果没有可用的无参数构造函数,则会给出编译错误)
这是一种意见,但是:这不是实现方式。对于引用类型,default
的意思是null
,即使根据可空性规则它无效。编译器会在Foo nonNullableFoo = default(Foo);
行中发现并警告您:
警告CS8600将空文字或可能的空值转换为不可空类型。
关于string nonNullableString = null!;
和
第一行编译器就可以了,完全没有警告。
您告知以忽略它;这就是!
的意思。如果您告诉编译器不要抱怨某些事情,那么抱怨它没有抱怨是无效的。
因此,最后似乎唯一增加的价值仅仅是签名。
否,它的有效性更高,但是如果您忽略它发出的警告(上面的CS8600),并且您抑制它所做的其他事情,对您来说(!
:是的,它将没有太大用处。所以...不这样做吗?
您对编程语言设计的工作方式感到困惑。
默认值
(不可为空的)整数的默认值始终为
0
,但对我来说,不可为空的字符串的默认值为null
没有意义。为什么选择这个?这完全违反了我们一直习惯的非空原则。我认为默认的不可为空的string
的默认值应该为String.Empty
。
变量的默认值从一开始就是C#语言的基本功能。 The specification defines the default values:
对于 value_type 的变量,默认值与value_type的默认构造函数(请参阅默认构造函数)计算的值相同。 对于 reference_type 的变量,默认值为
null
。
从实际的角度来看,这是有道理的,因为默认值的基本用法之一是在声明给定类型的值的新数组时。通过此定义,运行时可以将分配的数组中的所有位全部置零-值类型的默认构造函数在所有字段中始终为全零值,并且null
表示为全零引用。从字面上看,这是规范的下一行:
初始化默认值通常是通过让内存管理器或垃圾收集器在分配内存之前将内存初始化为全零位来完成的。因此,使用全零位表示空引用很方便。
现在可空引用类型(NRT)随C#8一起发布。这里的选择不是“尽管使用NRT还是将默认值实现为null
”,而是“不要浪费时间和资源来尝试完全改写default
关键字的工作原理,因为我们正在引入NRT ”。 NRT是程序员的注释,设计使它们对运行时的影响为零。
我认为无法为引用类型指定默认值与无法为值类型定义无参数构造函数的情况类似-运行时需要快速的全零默认值和null
值是引用类型的合理默认值。并非所有类型都具有合理的默认值-TcpClient
的合理默认值是什么?
如果要使用自己的自定义默认值,请实现静态的Default
方法或属性并将其记录下来,以便开发人员可以将其用作该类型的默认值。无需更改语言的基础。
我的意思是必须在C#实现的某个深处指定
0
是int
的默认值。我们也可以选择1
或2
,但没有,共识是0
。那么,是否可以在激活Nullable引用类型功能时仅将string
的默认值指定为String.Empty
?
正如我所说,最重要的是将内存范围归零非常快且方便。没有运行时组件负责检查给定类型的默认值是什么,并在创建新值时在数组中重复该值,因为这样做效率极低。
您的建议基本上意味着运行时必须在运行时以某种方式检查string
的可空性元数据,并将全零的不可为空的string
值视为空的{{1} }。对于空string
的一种特殊情况,这将是涉及到运行时的非常复杂的更改。在为string
分配null
而不是明智的默认值时,仅使用静态分析器来警告您会节省更多成本。幸运的是,我们有这样的分析器,即NRT功能,该功能始终拒绝编译包含如下定义的类:
string
通过发出警告并强迫我将其更改为:
string Foo { get; set; }
(我建议您顺便打开“将警告视为错误,但这只是出于个人喜好。)
再次对我来说这没有意义,我认为Foo的默认值应该是new Foo()(如果没有无参数的构造函数,则会产生编译错误)。为什么默认情况下将不应该为null的对象设置为null?
除其他外,这将使您无法在没有默认构造函数的情况下声明引用类型的数组。大多数基本集合都使用数组作为基础存储,包括string Foo { get; set; } = "";
。而且,每当您创建大小为List<T>
的数组时,都将需要分配类型的N
默认实例,这同样非常低效。构造函数也可能有副作用。我不会进一步考虑这会破坏多少东西,足以说这很难做。考虑到实施NRT到底有多复杂(Roslyn回购中的NullableReferenceTypesTests.cs
文件仅具有〜130,000行代码),因此引入这种更改的成本效益不是...很好。
爆炸运算符(!)和可空值类型
第一行编译器就可以了,完全没有警告。我最近在体验可为空的引用类型功能时发现了
N
,并且我希望编译器也能适用于第二行,但事实并非如此。现在,我对于微软为何决定实施不同的行为感到非常困惑。
null!
值仅对引用类型和可为空的值类型有效。可空类型再次定义为in the spec:
可空类型可以表示其基础类型的所有值以及附加的
null
值。可空类型写为null
,其中T?
是基础类型。该语法是T
的简写,并且两种形式可以互换使用。 (...)可为空类型System.Nullable<T>
的实例具有两个公共只读属性:
- 类型为
T?
的{{1}}属性- 类型为
HasValue
的{{1}}属性bool
为Value
的实例被认为是非空的。非null实例包含一个已知值,T
返回该值。
您无法为HasValue
分配true
的原因非常明显-Value
是一个采用32位表示整数的值类型。 null
值是一个特殊的参考值,其值为机器字大小,表示内存中的位置。将int
分配给int
没有明智的语义。 null
的存在是专门用于允许将null
分配给值类型以表示“无值”情况。但请注意,
int
是纯语法糖。 Nullable<T>
的全零值是“无值”情况,因为这意味着null
是int? x = null;
。在任何地方都没有分配魔法值Nullable<T>
,这与说HasValue
一样,它只是创建给定类型false
的新全零结构并将其分配。>
因此,答案再次是-没有人故意设计使其与NRT不兼容的设计。自从C#2引入以来,可空值类型是该语言的一项更基本的功能。而且,您提议它的工作方式不会转化为明智的实现-您是否希望所有值类型都可以为空?那么所有这些人都必须拥有一个null
字段,该字段占用一个额外的字节并可能填充填充(我认为将= default
表示为40位类型而不是32位的语言异端:))。
bang运算符专门用于告诉编译器“我知道我正在引用nullable /将null赋值给non-nullable,但我比您聪明,并且我知道事实上这不会打破任何东西”。它禁用静态分析警告。但这并不能神奇地扩展基础类型以容纳T
值。
摘要
考虑到它并不能完全防止将
HasValue
包含在不可为空的引用类型变量中,因此此新功能似乎并没有改变任何东西,也根本没有改善开发人员的生活(相对不能为int
的非空值类型,因此不需要进行空检查)因此,最后似乎唯一增加的价值仅仅是签名。现在,开发人员可以清楚地知道方法的返回值是否可以为
null
或属性可以为null
(例如,在数据库表的C#表示形式中,允许使用NULL)值)。
此新功能相对于早期版本的C#(无法从变量声明中确定设计意图)中的参考变量的处理而言,具有显着的优势。编译器没有针对引用类型的null引用异常提供安全性(...)这些警告在编译时发出。编译器不会在可为空的上下文中添加任何空检查或其他运行时构造。在运行时,可为空的引用和不可为空的引用是等效的。
所以您说对了,“唯一增加的价值只是签名” 和静态分析,这就是我们首先拥有签名的原因。这不是开发人员生活的改善吗?请注意,您的行
null
发出警告。如果您不忽略它(甚至更好,启用“将警告作为错误对待”),您将获得价值-编译器为您在代码中发现了一个错误。
它是否可以防止您在运行时将null
分配给非空引用类型?否。它会改善开发人员的生活吗?千倍是。该功能的强大之处在于在编译时进行的警告和可为空的分析。如果您忽略NRT发出的警告,则后果自负。您可以忽略编译器的帮助之手的事实并不会使它毫无用处。毕竟,您也可以将整个代码放在null
上下文中并用C编写程序,这并不意味着C#是无用的,因为您可以规避其安全保证。