如果null不好,为什么现代语言会实现它呢?[关闭]


82

我确定像Java或C#这样的语言的设计师都知道与空引用存在有关的问题(请参阅空引用真的是一件坏事吗?)。另外,实现选项类型实际上并不比空引用复杂得多。

他们为什么仍然决定将其包括在内?我确信缺少空引用会鼓励(甚至强迫)语言创建者和用户使用质量更高的代码(尤其是更好的库设计)。

仅仅是因为保守主义吗?“其他语言也有,我们也必须有……”?


99
null很棒。我喜欢它,每天都在使用。
Pieter B

17
@PieterB但是,您是否将其用于大多数引用,还是希望大多数引用不为null?论据不是说不应该有可为空的数据,而应该是明确的和选择加入的。

11
@PieterB但是,当多数不应该为可为空值时,将可为空值作为例外而不是默认值是不是有意义?请注意,虽然选项类型的通常设计是强制显式检查是否存在缺失和拆包,但也可以具有众所周知的Java / C#/ ...语义,用于选择可为空的引用(使用时似乎不是可为空的,则可能会爆炸如果为null)。它至少可以防止某些错误,并使静态分析抱怨缺少空检查更为实用。

20
WTF跟你们在一起吗?在软件可能会发生的所有事情中,尝试取消引用null完全没有问题。它总是生成AV /段故障,因此得到修复。是否有如此多的bug短缺,您必须为此担心?如果是这样,我有很多备用资源,而且没有一个会引用空引用/指针的问题。
马丁·詹姆斯

13
@MartinJames“它总是会生成AV / segfault,因此会得到修复”-不,不,它不会。
2014年

Answers:


97

免责声明:由于我个人不认识任何语言设计师,因此我给您的任何答案都是推测性的。

托尼·霍尔自己:

我称之为我的十亿美元错误。这是在1965年发明空引用的。那时,我正在设计第一个全面的类型系统,用于面向对象语言(ALGOL W)的引用。我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但是我忍不住要插入一个空引用的诱惑,仅仅是因为它是如此容易实现。这导致了无数错误,漏洞和系统崩溃,在最近四十年中可能造成十亿美元的痛苦和破坏。

强调我的。

自然,在当时对他来说这并不是一个坏主意。出于相同的原因,它可能被永久保留了-如果它对图灵奖得主Quicksort的发明者来说似乎是个好主意,那么很多人仍然不理解为什么它是邪恶的就不足为奇了。也可能部分是因为出于营销和学习上的原因,新语言与旧语言的相似性很方便。例子:

“我们追求的是C ++程序员。我们设法将其中很多拖到Lisp的中间。” -Java规范的合著者Guy Steele

(来源:http : //www.paulgraham.com/icad.html

而且,当然,C ++为空,因为C为空,因此无需考虑C的历史影响。C#取代了J ++,后者是Microsoft对Java的实现,它也取代了C ++作为Windows开发的首选语言,因此它可以从任何一个中获得。

编辑这是Hoare的另一句话,值得考虑:

总体而言,编程语言比以前复杂得多:从连贯且科学基础的学科或正确性理论的角度来看,仍然没有真正考虑过面向对象,继承和其他功能。 。我一生一直追求的最初假设是,人们将正确性标准作为一种融合体面的编程语言设计的手段-一种不会为用户设置陷阱的方法,而程序的不同组件清楚地对应于其规范的不同组件,因此您可以对其进行组合推理。[...]包括编译器在内的工具必须基于编写正确程序的含义的某种理论。 -2002年7月17日在英国剑桥接受Philip L. Frana的口述历史采访;明尼苏达大学查尔斯·巴贝奇研究所。[ http://www.cbi.umn.edu/oh/display.phtml?id=343]

再次强调我的。Sun / Oracle和Microsoft是公司,任何公司的底线都是金钱。他们的好处null可能胜过弊端,或者他们的期限太紧,无法充分考虑这个问题。例如,由于截止日期可能发生的另一种语言错误:

可克隆性被破坏很遗憾,但是它确实发生了。最初的Java API在紧迫的期限内很快就完成了,以适应市场的逐渐关闭。最初的Java团队做得非常出色,但是并非所有的API都是完美的。可克隆性是一个薄弱环节,我认为人们应该意识到它的局限性。-乔什·布洛赫(Josh Bloch)

(来源:http : //www.artima.com/intv/bloch13.html


32
亲爱的唐纳德:我如何改善答案?
2014年

6
您实际上并没有回答这个问题;您只提供了一些事后意见的报价和一些有关“成本”的建议。(如果null为十亿美元的错误,那么MS和Java通过实施它所节省的钱不应该减少债务吗?)
DougM 2014年

29
@DougM您希望我做什么,请过去50年中的每位语言设计师加入我们的行列,并问他为什么采用null他的语言?除非来自语言设计者,否则对这个问题的任何回答都是推测性的。除了埃里克·利珀特(Eric Lippert)之外,我不知道这个站点上有什么其他的站点。最后一部分是红色鲱鱼,其原因很多。在MS和Java API之上编写的第三方代码数量显然超过了API本身中的代码数量。因此,如果您的客户想要null,可以给他们null。您还假设他们已经接受null,使他们付出了金钱。
2014年

3
如果您只能给出推测性的答案,请在开始的段落中明确说明。(您问过如何改善答案,我回答了。任何括号只是您可以随意忽略的注释;毕竟,括号是英语的意思。)
DougM 2014年

7
这个答案是合理的。我在我的中添加了更多注意事项。我注意到,ICloneable.NET同样也有问题。不幸的是,这是无法及时了解Java缺点的地方。
埃里克·利珀特

121

我确定像Java或C#这样的语言的设计师都知道与空引用存在有关的问题

当然。

另外,实现选项类型实际上并不比空引用复杂得多。

我不敢苟同!在C#2中,可为空的值类型所涉及的设计注意事项是复杂,有争议且困难的。他们花了几个月的时间讨论语言,运行时的设计团队以及原型的实现等问题,实际上,可空框的语义已非常接近于发行C#2.0,这引起了很大争议。

他们为什么仍然决定将其包括在内?

所有设计都是从许多微妙和完全不兼容的目标中进行选择的过程。我只能简要介绍一下将要考虑的一些因素:

  • 语言特征的正交性通常被认为是一件好事。C#具有可空值类型,不可空值类型和可空引用类型。不可为空的引用类型不存在,这导致类型系统为非正交类型。

  • 熟悉C,C ++和Java的现有用户很重要。

  • 与COM的轻松互操作性很重要。

  • 与所有其他.NET语言的轻松互操作性很重要。

  • 与数据库的轻松互操作性很重要。

  • 语义的一致性很重要;如果我们引用TheKingOfFrance等于null总是表示“现在没有法国国王”,或者它也意味着“肯定有法国国王;我只是不知道现在是谁”?还是意味着“在法国拥有国王这一概念是荒谬的,所以甚至不要问这个问题!”?在C#中,Null可能意味着所有这些含义,而所有这些概念都很有用。

  • 性能成本很重要。

  • 进行静态分析很重要。

  • 类型系统的一致性很重要;我们是否总能知道在任何情况下都不能将非空引用视为无效?在引用类型为非空字段的对象的构造函数中该怎么办?在这样的对象的终结器中,由于要填充引用的代码引发了异常,该对象被终结了,该怎么办?对于您而言,保证其安全性的类型系统很危险。

  • 语义的一致性又如何呢?使用时会传播空,但使用时空引用会引发异常。那是不一致的。这种不一致是否可以通过某些好处得到证明?

  • 我们可以在不破坏其他功能的情况下实现该功能吗?该功能还排除了哪些其他将来可能的功能?

  • 您是与自己的军队而不是自己想要的人打仗。请记住,C#1.0没有泛型,因此Maybe<T>作为替代方案谈论是完全不入门的。在运行时团队添加泛型以消除空引用的同时,.NET是否应该滑倒两年?

  • 类型系统的一致性如何?您可以说Nullable<T>任何值类型-不,等等,这是一个谎言。你不能说Nullable<Nullable<T>>。你应该能够吗?如果是这样,其期望的语义是什么?是否值得为此整个类型系统有一个特殊情况?

等等。这些决定很复杂。


12
+1一切,但特别是泛型。很容易忘记,在Java和C#的历史中都有一段时间不存在泛型。
2014年

2
可能是一个愚蠢的问题(我只是IT本科生)-但无法在语法级别(CLR不了解任何内容)将选项类型实现为常规的可为空的引用,该引用需要在使用之前进行“具有值”检查码?我相信选项类型在运行时不需要任何检查。
mrpyo 2014年

2
@mrpyo:当然,这是一个可能的实现选择。其他设计选择都不会消失,而实现选择则有很多优点和缺点。
埃里克·利珀特

1
@mrpyo我认为强制执行“具有值”检查不是一个好主意。从理论上讲,这是一个很好的主意,但是在实践中,IMO会带来各种各样的空检查,只是为了满足编译器的需要-就像Java中的检查异常和catches无所事事的人一样。我认为最好是让系统崩溃,而不是在可能无效的状态下继续运行。
NothingsIm不可能

2
@voo:由于许多原因,非空引用类型的数组很难。有许多可能的解决方案,并且所有解决方案都会为不同的操作带来成本。Supercat的建议是跟踪元素在分配之前是否可以合法读取,这会增加成本。您应该确保在数组可见之前,每个元素上都运行一个初始化程序,这会带来不同的成本。这就是难题:无论人们选择哪种技术,都会有人抱怨说这对他们的宠物情境并不有效。这是对该功能的严重要点。
埃里克·利珀特

28

空值是代表缺乏价值的非常有效的目的。

我会说我是最了解声音的人,尤其是在自由使用时,会滥用null以及它们可能引起的所有头痛和痛苦。

我的个人立场是,只有在人们认为有必要和适当时才可以使用null 。

对齐null的示例:

死亡日期通常是一个可为空的字段。死亡日期可能有三种情况。该人已死亡且已知日期,该人已死亡且未知日期,或者该人尚未死亡,因此不存在死亡日期。

死亡日期也是DateTime字段,没有“未知”或“空”值。当您确定一个新的日期时间时,它确实具有默认日期,该日期根据所使用的语言而异,但是从技术上讲,这个人实际上确实在那个时间死亡,如果您愿意,它将标记为“空值”使用默认日期。

数据将需要正确地表示情况。

人已死已知死亡日期(3/9/1984)

简单,“ 3/9/1984”

人死了死亡日期不详

那什么是最好的?Null,'0/0/0000'或'01 / 01/1869'(或无论您的默认值是什么?)

人未死亡死亡日期不适用

那什么是最好的?Null,'0/0/0000'或'01 / 01/1869'(或无论您的默认值是什么?)

因此,让我们考虑每个值...

  • Null,它有一些含义和隐患,您需要警惕,不小心首先尝试操作它而不先确认它不是null,例如会引发异常,但它也最能代表实际情况...如果该人没有死死亡日期不存在...什么都没有...它是空的...
  • 0/0/0000,这在某些语言中可能还可以,甚至可以适当地表示没有日期。不幸的是,某些语言和验证会将其拒绝为无效的日期时间,因此在很多情况下都不可行。
  • 1/1/1869(或任何默认的datetime值),这里的问题是处理起来很棘手。您可以将其用作缺乏价值的东西,但如果我想过滤掉我没有死亡日期的所有记录会发生什么呢?我可以轻松地过滤掉那一天实际死亡的人,这可能会导致数据完整性问题。

事实是,有时你不要需要代表什么,并确保有时变量类型可很好地工作,但往往变量类型必须能够代表什么。

如果我没有苹果,我有0个苹果,但是如果我不知道我有多少个苹果,该怎么办?

绝对会滥用null并有潜在危险,但是有时这是必要的。在许多情况下,它只是默认值,因为在我提供值之前,缺少值,需要一些东西来表示它。(空值)


37
Null serves a very valid purpose of representing a lack of value.一个OptionMaybe类型可以达到非常有效的目的,而无需绕过类型系统。
2014年

34
没有人在争论不应该存在价值缺失的价值,他们在争论应该将可能缺失的价值明确标明,而不是每个潜在缺失的价值。

2
我猜RualStorge是在谈论SQL,因为有一些阵营指出每个列都应标记为NOT NULL。我的问题与RDBMS无关...
mrpyo 2014年

5
+1用于区分“
大卫

2
分离一个人的状态会更有意义吗?即,A Person型具有state类型的字段State,这是一个识别联合AliveDead(dateOfDeath : Date)
jon-hanson 2015年

10

我不会说“其他语言也必须,我们也必须……”,就像跟琼斯一样。任何新语言的一个关键功能是能够与其他语言的现有库进行互操作(阅读:C)。由于C具有空指针,所以互操作性层必然需要空的概念(或使用它时会炸毁的其他“不存在”等价物)。

语言设计者可以选择使用选项类型,并迫使您在任何可能为空的地方都处理空路径。而且几乎可以肯定会导致更少的错误。

但是(特别是对于Java和C#,由于它们的引入时间和目标受众),如果不对它们的采用大打折扣,则将选项类型用于此互操作性层可能会受到损害。选项类型一直向上传递,使90年代中期至后期的C ++程序员烦恼-或互操作性层在遇到null时会抛出异常,使90年代中期至90年代后期的C ++程序员烦恼。 ..


3
第一段对我来说没有意义。Java没有您建议的形状的C互操作(有JNI,但对于与引用有关的所有内容,它已经跳了十几个圈;而且在实践中很少使用),与其他“现代”语言相同。

@delnan-抱歉,我更熟悉C#,它确实具有这种互操作性。我宁愿假设许多基础Java库在底部也使用JNI。
Telastyn 2014年

6
您为允许null提出了很好的论据,但是您仍然可以在鼓励使用 null的情况下允许它。Scala就是一个很好的例子。它可以与使用null的Java api无缝地互操作,但是我们建议您将其包装Option在Scala 中以供在Scala中使用,这非常简单val x = Option(possiblyNullReference)。在实践中,人们很快就可以看到的好处Option
Karl Bielefeldt

1
选项类型与(经过静态验证的)模式匹配紧密结合,不幸的是C#没有。F#确实可以,这很棒。
史蒂文·埃弗斯

1
@SteveEvers可以使用带有私有构造函数的抽象基类,密封内部类以及Match将委托作为参数的方法来伪造它。然后将lambda表达式传递给Match(使用命名参数的加分点)并Match调用正确的表达式。
2014年

7

首先,我认为我们都可以同意零概念是必要的。在某些情况下,我们需要代表缺乏信息。

允许null引用(和指针)只是该概念的一种实现,尽管它已知存在问题,但可能是最受欢迎的实现:C,Java,Python,Ruby,PHP,JavaScript等都使用相似的null

为什么呢 好吧,还有什么选择?

在Haskell等功能语言中,您具有Optionor或Maybe;但是这些是建立在:

  • 参数类型
  • 代数数据类型

现在,原始的C,Java,Python,Ruby或PHP是否支持这些功能?否。Java的有缺陷的泛型是该语言历史上的最新成果,我不知怎地怀疑其他语言甚至根本没有实现它们。

你有它。null很简单,参数代数数据类型更难。人们选择了最简单的选择。


+1表示“ null容易,参数代数数据类型更难”。但是我认为这并不是参数化类型和ADT更加困难的问题。只是他们没有被认为是必要的。另一方面,如果Java出厂时没有目标系统,那它就会失败。OOP是一项“炫耀性”功能,因为如果您没有它,那么没人会感兴趣。
2014年

@Doval:好吧,对于Java来说可能需要OOP,但对于C语言则不是:)但是Java的目标确实是简单。不幸的是,人们似乎认为简单的语言会导致简单的程序,这有点奇怪(Brainfuck是一种非常简单的语言...),但是我们当然同意,即使即使复杂的语言(C ++ ...)也不是万能药它们可能非常有用。
Matthieu M.

1
@MatthieuM .:实际系统很复杂。一种精心设计的语言,其复杂性与要建模的现实世界系统相匹配,可以使用简单的代码对复杂的系统进行建模。试图过度简化一种语言只会将复杂性推给使用它的程序员。
2014年

@supercat:我完全同意。或像爱因斯坦所说:“让一切尽可能简单,但不要简单”。
Matthieu M. 2014年

@MatthieuM .:爱因斯坦在很多方面都是明智的。试图假设“一切都是对象,可能会存储Object对它的引用”的语言无法认识到实际应用需要不可共享的可变对象和可共享的不可变对象(两者的行为都应类似于值),以及可共享和不可共享实体。Object对所有事物使用单一类型并不能消除对此类区别的需求。只是增加了正确使用它们的难度。
超级猫

5

Null / nil / none本身并不邪恶。

如果您看到他的名字令人误解的著名演讲“十亿美元的错误”,Tony Hoare谈到了如何让任何变量能够保持null是一个巨大的错误。备选方案-使用选项- 实际上并没有消除空引用。相反,它允许您指定允许哪些变量保留为null,哪些不允许。

事实上,对于实现适当的异常处理的现代语言而言,空解除引用错误与任何其他异常没有什么不同-您可以找到它,然后对其进行修复。空引用的某些替代方法(例如,“空对象”模式)会隐藏错误,从而使事情无声地失败,直到很久以后。我认为,快速失败更好。

那么问题来了,为什么语言无法实现选项?实际上,可以说是有史以来C ++最受欢迎的语言,它能够定义无法分配的对象变量NULL。这是Tony Hoare在讲话中提到的“零问题”的解决方案。为什么第二流行的打字语言Java没有呢?有人可能会问,为什么它通常有这么多缺陷,尤其是在类型系统上。我认为您无法真正说出语言是系统地犯此错误的。有些会,有些不会。


1
从实现的角度来看,Java的最大优点之一,从语言的角度来看,缺点是,只有一种非基本类型:混杂对象引用。这极大地简化了运行时,从而使某些极其轻量的JVM实现成为可能。但是,这种设计意味着每种类型都必须具有默认值,并且对于混杂对象引用,唯一可能的默认值为null
超级猫

嗯,无论如何,一种非原始类型。为什么从语言角度来看这是一个弱点?我不明白为什么这个事实要求每个类型都具有默认值(或者相反,为什么多个根类型将允许类型不具有默认值),也为什么这是一个缺点。
英国电信

字段或数组元素还可以保留其他哪种非基本元素?缺点是某些引用用于封装身份,而另一些则用于封装包含在由此标识的对象中的值。对于用于封装身份的引用类型变量,null是唯一明智的默认设置。但是,用于封装值的引用在类型将具有或可以构造明智的默认实例的情况下可能具有明智的默认行为。引用应如何行为的许多方面取决于它们是否以及如何封装值,但是……
超级猫

... Java类型系统无法表达这一点。如果foo拥有一个唯一的参考int[]{1,2,3}和代码要foo举行一个参考int[]{2,2,3},以最快的方式来实现,这将是递增foo[0]。如果代码想让一个方法知道foohold {1,2,3},则另一个方法将不会修改数组,也不会保留引用超出foo要修改数组的范围,最快的方法是将引用传递给数组。如果Java具有“临时只读引用”类型,则...
超级猫

...该数组可以安全地作为临时引用传递,而想要保留其值的方法将知道需要对其进行复制。在没有这种类型的情况下,安全公开数组内容的唯一方法是对其进行复制或将其封装在为此目的而创建的对象中。
超级猫2014年

4

因为编程语言通常被设计为实用而不是技术上正确的。事实是,null由于数据错误或丢失或状态尚未确定,状态是常见的情况。技术上优越的解决方案比简单地允许空状态和吸收程序员犯错误的事实更加笨拙。

例如,如果我想编写一个适用于文件的简单脚本,则可以编写如下的伪代码:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

如果joebloggs.txt不存在,它将只会失败。事实是,对于简单的脚本来说可能还可以,并且在更复杂的代码中的许多情况下,我知道它存在并且不会发生故障,因此迫使我进行检查浪费了我的时间。更安全的替代方案通过迫使我正确处理潜在的故障状态来实现其安全,但是通常我不想这样做,我只是想继续前进。


13
在这里,您举了一个关于null到底有什么问题的示例。正确实现的“ openfile”功能应引发异常(针对丢失的文件),该异常将在此处停止执行,并详细说明发生的情况。相反,如果它返回null,它将进一步传播到,for line in file并抛出无意义的null引用异常,这种情况对于这样一个简单的程序是可以的,但会在更复杂的系统中引起真正的调试问题。如果不存在空值,则“ openfile”的设计者将无法犯此错误。
mrpyo 2014年

2
为“ +1,因为编程语言通常被设计为实用而不是技术上正确的”
Martin Ba

2
我知道的每种选项类型都允许您通过一个简短的额外方法调用来执行null失败(锈示例:)let file = something(...).unwrap()。根据您的POV,这是不处理错误或不会断言为空的简洁断言的简便方法。浪费的时间极少,您可以在其他地方节省时间,因为您不必弄清楚某些内容是否可以为null。另一个优点(可能值得额外付费)是您显式忽略了错误情况;当它失败时,毫无疑问,哪里出了问题以及需要修复的地方。

4
@mrpyo并非所有语言都支持异常和/或异常处理(尝试/捕获)。异常也可以被滥用-“异常作为流控制”是一种常见的反模式。这种情况-文件不存在-是AFAIK最常引用的反模式示例。看来您正在用另一种不好的做法代替。
大卫

8
@mrpyo if file exists { open file }患有种族状况。知道打开文件是否成功的唯一可靠方法是尝试打开它。

4

指针NULL(或nil,或Nil,或null,或Nothing它在您的首选语言中所说的任何东西)有明显的实际用途。

对于没有异常系统的语言(例如C),应返回空指针时,可以将空指针用作错误标记。例如:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

在这里,从NULL返回的malloc(3)值用作失败的标记。

在方法/函数参数中使用时,它可以指示使用默认参数或忽略输出参数。下面的例子。

即使对于具有异常机制的语言,空指针也可以用作软错误(即,可恢复的错误)的指示,尤其是在异常处理成本很高(例如Objective-C)的情况下:

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

在这里,如果没有捕获到软错误,则不会导致程序崩溃。这样就消除了Java那样的疯狂try-catch,并且由于软错误不会中断(并且一些剩余的硬异常通常不可恢复且未被捕获),因此可以更好地控制程序流程。


5
问题在于无法将不应包含的变量与应包含的变量区分开null。例如,如果我想要一个Java中包含5个值的新类型,我可以使用一个枚举,但是得到的是一个可以容纳6个值的类型(我想要的5个值+ null)。这是类型系统中的缺陷。
2014年

@Doval如果是这种情况,只需给NULL赋予含义(或者,如果您具有默认值,则将其视为默认值的同义词)或使用NULL(从不应该出现在首位)作为软错误的标记(即错误,但至少还没有崩溃)
Maxthon Chan

1
Null仅当类型的值不携带数据时(例如,枚举值),才可以为@MaxtonChan 分配含义。一旦您的值变得更复杂(例如,结构),null就无法为其分配对该类型有意义的含义。无法将a null用作结构或列表。而且,再次将其null用作错误信号的问题是,我们无法确定可能返回null或接受null的内容。程序中的任何变量都可能是变量,null除非您非常谨慎地null在每次使用前都要检查每个变量,而没人会这样做。
2014年

1
@Doval:将不可变的引用类型null视为可用的默认值(例如,使默认值string表现为空字符串,就像在先前的公共对象模型下一样)不会有特别的固有困难。所需要的全部只是语言的使用,call而不是callvirt在调用非虚拟成员时。
超级猫2014年

@supercat很好,但是现在您不需要添加支持以区分不可变类型和非不变类型吗?我不确定添加到语言中的琐碎程度。
2014年

4

有两个相关但略有不同的问题:

  1. 应该null存在吗?还是应该始终Maybe<T>在null有用的地方使用?
  2. 是否所有引用都可以为空?如果不是,那应该是默认值?

    必须显式声明可为空的引用类型string?或类似的引用类型,可以避免大多数(但不是全部)null导致问题的原因,而与程序员习惯的用法没有太大不同。

我至少同意您的观点,并非所有引用都应该为空。但是避免null并非没有其复杂性:

.NET将所有字段初始化为,default<T>然后才能首先由托管代码访问它们。这意味着对于引用类型,您需要null或等同的名称,并且无需运行代码即可将值类型初始化为某种。尽管这两种方法都有严重的缺点,但default初始化的简便性可能胜过了这些缺点。

  • 对于实例字段,您可以通过在将this指针暴露给托管代码之前要求对字段进行初始化来解决此问题。Spec#走这条路,与C#相比,使用了与构造函数链接不同的语法。

  • 对于静态字段,确保这比较困难,除非您对字段初始化程序中可能运行的代码类型设置严格的限制,因为您不能简单地隐藏this指针。

  • 如何初始化引用类型的数组?考虑一个List<T>由容量大于长度的数组支持的。其余元素需要具有一定的价值。

另一个问题是,它不允许像bool TryGetValue<T>(key, out T value)这样的方法返回default(T)value好像它们什么都没有找到一样。尽管在这种情况下,很容易就争辩说out参数最初是不好的设计,并且此方法应该返回一个区分联合或一个may

所有这些问题都可以解决,但这并不像“禁止null并且一切都很好”那样容易。


List<T>恕我直言,这是最好的示例,因为它要么要求每个T都具有默认值,要么要求后备存储中的每个项目都Maybe<T>带有一个额外的“ isValid”字段,即使当T是时也是如此Maybe<U>,或者List<T>行为的代码取决于T本身是否为可空类型。我认为将T[]元素初始化为默认值是这些选择中最少的弊端,但这当然意味着元素需要具有默认值。
2014年

Rust遵循第1点-完全没有null。锡兰遵循第2点-默认为非空。可以为null的引用用包含引用或null的并集类型显式声明,但是null永远不能是纯引用的值。结果,该语言是完全安全的,并且没有NullPointerException,因为它在语义上是不可能的。
Jim Balter 2015年

2

最有用的编程语言允许以任意顺序写入和读取数据项,因此通常无法在程序运行之前静态确定读取和写入的顺序。实际上,在许多情况下,代码实际上会在读取之前将有用的数据存储到每个插槽中,但是很难证明这一点。因此,通常有必要运行程序,其中至少在理论上代码可能会尝试读取尚未以有用值编写的内容。不管代码是否合法,都没有阻止这种尝试的通用方法。唯一的问题是,当发生这种情况时会发生什么。

不同的语言和系统采用不同的方法。

  • 一种方法是说,任何尝试读取未写入内容的尝试都会触发立即错误。

  • 第二种方法是要求代码在有可能读取之前,在每个位置都提供一些值,即使没有办法使所存储的值在语义上有用。

  • 第三种方法是简单地忽略问题,而让任何事情“自然地”发生。

  • 第四种方法是说每种类型都必须具有默认值,并且任何未用其他任何东西写入的插槽都将默认为该值。

方法4比方法3安全得多,并且通常比方法1和2便宜。然后,剩下的问题是引用类型的默认值应该是什么。对于不可变的引用类型,在许多情况下定义默认实例是有意义的,并且说该类型的任何变量的默认值都应该是对该实例的引用。但是,对于可变引用类型,那将不是很有帮助。如果在写入之前尝试使用可变引用类型,则通常没有任何安全的措施,只能在尝试使用时进行陷阱。

从语义上讲,如果一个数组customers具有type Customer[20],并且在Customer[4].GiveMoney(23)没有将任何内容存储到的情况下进行尝试,则将不得不Customer[4]执行陷阱。可能有人争辩说,尝试读取Customer[4]应该立即捕获,而不是等到代码尝试读取之后GiveMoney,但是在很多情况下,读取插槽,发现其不具有值然后再利用该值很有用。信息,使读取尝试本身失败通常会是一个主要的麻烦。

某些语言允许人们指定某些变量永远不应包含null,并且任何存储null的尝试都应触发立即陷阱。这是一个有用的功能。通常,尽管如此,任何允许程序员创建引用数组的语言都必​​须允许存在空数组元素,或者强制将数组元素初始化为可能没有意义的数据。


会不会一Maybe/ Option型解决问题#2,因为如果你没有供你参考值,但将有一个在未来,你可以存储Nothing在一个Maybe <Ref type>
2014年

@Doval:不,它不能解决问题-至少,如果不重新引入null引用的话。“什么都不应该”表现为这种成员吗?如果是这样,哪一个?还是应该抛出异常?在这种情况下,与简单地null正确/明智地使用相比,您有什么更好的收获?
cHao 2014年

@Doval:a的后缀类型应该List<T>是a T[]还是a Maybe<T>?那么a的支持类型List<Maybe<T>>呢?
2014年

@supercat我不确定后备类型是否Maybe有意义,List因为它Maybe拥有单个值。你是说Maybe<T>[]
德瓦尔(Doval)

@cHao Nothing只能分配给type的值Maybe,所以它与分配不太一样nullMaybe<T>T是两种不同的类型。
2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.