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

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

这仅仅是因为保守主义-“其他语言有它,我们也必须拥有它...“?

评论

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

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

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

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

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

#1 楼

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

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


强调我的经验。

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


“我们一直在追求C ++程序员。我们设法将其中的许多人拖到了Lisp的中间。”
-Guy Steele,共同作者Java规范


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

当然,C ++具有空值,因为C为null,则无需讨论C的历史影响。 C#取代了J ++,后者是Microsoft的Java实现,并且它也取代了C ++作为Windows开发的首选语言,因此它可以从任何一个中得到。

EDIT这是另一句话来自Hoare的值得考虑的事情:


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

评论


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

–Doval
2014年5月2日在13:38

您实际上并没有回答这个问题;您只提供了一些事后意见的报价,以及一些有关“成本”的额外建议。 (如果null是十亿美元的错误,那么通过实施它,MS和Java所节省的资金难道不可以减少债务吗?)

–DougM
2014年5月2日在18:54

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

–Doval
2014年5月2日在18:59

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

–DougM
2014年5月2日在19:54

这个答案是合理的。我在我的中添加了更多注意事项。我注意到,ICloneable在.NET中同样被破坏。不幸的是,这是无法及时了解Java缺点的地方。

–埃里克·利珀特
2014年5月2日在21:16

#2 楼


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


当然。


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


我希望有所不同!在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>>。你应该能够吗?如果是这样,其期望的语义是什么?使整个类型系统具有仅用于此功能的特殊情况是否值得?

等等。这些决定很复杂。

评论


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

–Doval
2014年5月2日在21:24

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

–mrpyo
14年5月2日在21:31



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

–埃里克·利珀特
2014年5月2日21:37

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

–埃里克·利珀特
2014年5月3日在21:01

@Perseids:现在您可以看到它的运行情况。这只是成千上万个微小的小设计问题,所有这些问题都必须仔细考虑。

–埃里克·利珀特
2014年5月5日13:58

#3 楼

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

我会说我是最了解声音的人,因为空值的滥用以及它们可能造成的所有头痛和痛苦,尤其是在使用时

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

为null进行示例的示例:

死亡通常是可为空的字段。有三种可能的死亡日期。该人已经死亡并且知道日期,或者该人已经死亡并且日期未知,或者该人没有死亡,因此不存在死亡日期。

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

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

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

简单,'1984年3月9日'

人去世了,死亡日期不明

那么什么是最好的呢? Null,“ 0/0/0000”或“ 01/01/1869”(或您的默认值是什么?)

人不是死亡日期不适用死亡日期

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

所以让我们考虑一下每个值...



为空,它包含一些隐患和您需要警惕的问题,无意中首先尝试操纵它而不先确认它不为null,例如会引发异常,但它也最能代表实际情况...如果这个人没有死,那么死亡的日期就不存在了...没什么...它是空的...

0/0/0000,这在某些语言中可能还可以,甚至可以适当表示无日期。不幸的是,某些语言和验证会将其拒绝为无效的日期时间,这使它在许多情况下都不可行。

1/1/1869(或任何默认的日期时间值是),这里的问题是变得棘手。您可以将其用作缺乏价值的东西,除非如果我想筛选出我没有死亡日期的所有记录,该怎么办?我可以轻松地筛选出在那一天实际死亡的人,这些人可能会导致数据完整性问题。

事实是有时您确实不需要表示任何内容,并且确定有时变量类型可以很好地解决这一问题,但通常变量类型必须不能表示任何内容。

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

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

评论


空值是代表缺乏价值的非常有效的目的。 Option或Maybe类型可以达到非常有效的目的,而无需绕过类型系统。

–Doval
2014年5月2日19:45

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

–user7043
2014年5月2日20:05

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

–mrpyo
2014年5月2日在20:13

+1用于区分“无价值”和“未知价值”

–大卫
2014年5月2日在20:57

分离一个人的状态会更有意义吗?即Person类型具有一个State类型的state字段,该状态字段是Alive和Dead(dateOfDeath:Date)的区分联合。

– jon-hanson
2015年11月14日在16:55



#4 楼

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

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

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

评论


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

–user7043
2014年5月2日13:00



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

– Telastyn
2014年5月2日13:06

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

–卡尔·比勒费尔特(Karl Bielefeldt)
2014年5月2日13:28

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

–史蒂文·埃弗斯(Steven Evers)
2014年5月2日在18:25

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

–Doval
2014年5月2日,18:50

#5 楼

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

允许null引用(和指针)只是该概念的一种实现,尽管它已知存在问题,但可能是最受欢迎的一种实现: C,Java,Python,Ruby,PHP,JavaScript等都使用类似的null。为什么?那么,还有什么选择呢?

在诸如Haskell之类的功能语言中,您可以使用OptionMaybe类型。但是,这些是建立在以下基础之上的:


参数类型
代数数据类型

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

就可以了。 null很简单,参数代数数据类型更难。人们选择了最简单的选择。

评论


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

–Doval
2014年5月3日在16:07

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

– Matthieu M.
2014年5月3日17:39

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

–超级猫
2014年5月8日13:43

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

– Matthieu M.
2014年5月8日14:23

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

–超级猫
2014年5月8日15:07

#6 楼

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

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

事实上,对于实现适当异常处理的现代语言而言,null取消引用错误不存在与任何其他例外不同-您找到了,就解决了。空引用的某些替代方法(例如,空对象模式)会隐藏错误,从而使事情无声地失败,直到以后。我认为,快速失败会更好。

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

评论


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

–超级猫
2014年5月5日,下午3:05

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

– B T
2014年5月5日7:33

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

–超级猫
2014年5月5日14:56



... Java类型系统无法表达这一点。如果foo拥有对包含{1,2,3}的int []的唯一引用,并且代码希望foo保留对包含{2,2,3}的int []的引用,则最快的方法是递增foo [0]。如果代码想让一个方法知道foo拥有{1,2,3},则另一个方法将不会修改数组,也不会保留引用超出foo想要对其进行修改的范围,这是最快的实现方法将引用传递给数组。如果Java具有“临时只读引用”类型,则...

–超级猫
2014年5月5日15:05

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

–超级猫
2014年5月5日15:22

#7 楼

因为编程语言通常被设计为实用而不是技术上正确的。事实是,由于错误或丢失的数据或尚未确定的状态,null状态很常见。技术上优越的解决方案比简单地允许空状态和吸收程序员犯错误的事实更加笨拙。例如,如果我想编写一个与文件一起使用的简单脚本,我可以编写伪代码,例如:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}


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

评论


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

–mrpyo
2014年5月2日17:41



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

–马丁·巴
2014年5月2日17:53

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

–user7043
2014年5月2日在18:14



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

–大卫
2014年5月2日在20:22

@mrpyo(如果文件存在){打开文件}处于竞争状态。知道打开文件是否成功的唯一可靠方法是尝试打开它。

–user7043
2014年5月2日在22:34

#8 楼

NULL(或nilNilnullNothing或您喜欢的语言中调用的任何东西)指针有明显的实际用途。

对于那些没有例外的语言在系统(例如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,并在程序流中具有更好的控制,因为软错误不会中断(并且少数硬异常通常无法恢复并且未被捕获)

评论


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

–Doval
2014年5月2日在17:51

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

–陈Max
2014年5月2日在17:59

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

–Doval
2014年5月2日在18:06



@Doval:将不可变的引用类型视为null可用的默认值不会有特别的固有困难(例如,将string的默认值表现为空字符串,就像在先前的Common Object Model下所做的那样)。语言需要在调用非虚拟成员时使用call而不是callvirt。

–超级猫
2014年5月3日15:58

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

–Doval
2014年5月3日16:04

#9 楼

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



应该根本存在null吗?还是应该始终在可能有用null的地方使用Maybe<T>

所有引用是否都可以为空?如果不是,应该使用默认值吗?

必须显式地将可空引用类型声明为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都具有默认值,后备存储中的每个项目都必须是带有额外“ isValid”字段的Maybe ,即使T是可能是,还是List 的代码的行为不同,具体取决于T本身是否为可为空的类型。我认为将T []元素初始化为默认值是这些选择中最少的选择,但这当然意味着元素需要具有默认值。

–超级猫
2014年5月3日15:51

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

–吉姆·巴尔特(Jim Balter)
15年4月27日在1:36

#10 楼

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

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


一种方法是说任何阅读尝试
第二种方法是要求代码在有可能读取之前,在每个位置提供一些值,即使无法存储值也是如此。在语义上是有用的。
第三种方法是简单地忽略该问题,而让发生的任何事情“自然地”发生。
第四种方法是说每个类型都必须具有默认值,而任何类型都必须具有默认值未用其他任何东西写入的插槽将默认为该值。

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

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

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

评论


Maybe / Option类型不能解决#2的问题,因为如果您还没有供参考的值,但将来还会有一个值,您可以将Nothing存储在Maybe 中吗?

–Doval
2014年5月3日,下午3:51

@Doval:不,它不会解决问题-至少,除非没有再次引入null引用,否则不会解决。 “什么都不应该”表现为这种成员吗?如果是这样,哪一个?还是应该抛出异常?在这种情况下,与仅正确/明智地使用null相比,您还有什么更好的选择?

– cHao
2014年5月3日14:02



@Doval:List 的后备类型应该是T []还是Maybe ? List >的后备类型呢?

–超级猫
2014年5月3日14:22

@supercat我不确定Maybe的后备类型对List的意义如何,因为Maybe拥有单个值。您是说Maybe []吗?

–Doval
2014年5月3日15:19

@cHao Nothing只能分配给Maybe类型的值,因此与分配null不太一样。也许和T是两种不同的类型。

–Doval
2014年5月3日15:21