我曾经读过,拥有可空类型是一种绝对的邪恶。 我相信这是由创建它们的人写的一篇文章(在Ada中?)我相信这是文章
无论如何,那么如果默认情况下像C#这样的语言使用非可空类型呢? 如何替换C#或Ruby中的一些常用习语或null是可接受值的任何其他常用语言?
-
不管怎么说,你有原始问题吗?如果您有double x;如何判断x是否已初始化?
-
@KLee是的我知道,但忽略C#细节。我问这个问题的原因是因为我正在设计自己的语言,并且正在考虑不可空的辩论。
-
据我所知,这并非特定于C#。我认为double是大多数语言中不可空的类型。您必须处理不为null的对象,就像处理决定double是否已初始化一样。
-
托尼霍尔:无效的参考,十亿美元的错误。还有一个演示视频。 (以及关于这个话题的访谈等。)
-
但是,int可能是比double更好的例子。通常的double的IEEE浮点实现提供NaN值,这通常可以合理地替代null值。
-
@bcat:你仍然可以在NaN上操作并获得无用的结果,你不能在null上。
-
@bltxd:是的,尽管如果您的语言支持信令NaN,这可以在某种程度上得到缓解。
-
可能重复的不包括语言中的NULL的含义?,关于非可空类型的争论
-
NaN不是null ......这意味着你有一个值,但它在当前的上下文中没有意义; null意味着你几乎什么都没有。
我认为,大多数语言都将可空性嫁接到各种类型上,而这两种概念应该是正交的,而不是直接宣称可以为空的类型是邪恶的。
例如,所有非原始Java类型(以及所有C#引用类型)都可以为空。为什么?我们可以来回走动,但最终我敢打赌答案归结为"很容易"。 Java语言没有任何内在要求广泛的可空性。 C ++参考提供了一个很好的例子,说明如何在编译器级别驱除空值。当然,C ++有一些非常丑陋的语法,而Java明确地试图缩减它,所以一些好的功能最终会出现在切割底线上。
C#2.0中的可空值类型向正确的方向迈出了一步 - 将可空性与无关类型语义分离,或者更糟糕的是,CLR实现细节 - 但它仍然缺少与引用类型相反的方法。 (代码合同很棒,但它们并没有像我们在这里讨论的那样嵌入到类型系统中。)
大量功能性或其他模糊语言从一开始就使这些概念"直接"......但如果它们被广泛使用,我们就不会进行这样的讨论......
要回答你的问题:禁止现代语言中的空白批发,就像所谓的"十亿美元错误"一样愚蠢。有一些有效的编程结构,其中空值很好:可选参数,任何类型的默认/回退计算,其中coalesce运算符导致简洁代码,与关系数据库的交互等。强制自己使用sentinel值,NaN等将是比疾病更糟糕的"治愈"。
也就是说,我暂时会同意引文中表达的观点,只要我可以详细说明以适合我自己的经验:
需要空值的情况比大多数人想象的要小
一旦你将空值引入库或代码路径中,除了添加它们之外,要摆脱它们要困难得多。 (所以不要让初级程序员一时兴起!)
可空的bug以可变的生命周期进行扩展
与#3相关:早期崩溃
-
在没有很好的方法来定义可选参数的语言中,您可能(或者您可能不会,取决于您想要的创意)需要NULL。但通常,NULL不是实现可选参数所必需的。
-
真正。只是因为nulls可用于实现可选参数并不能使它们成为最好的方法 - 特别是如果我们开始讨论可能有自己的自定义语法糖的特定语言。
-
我听说过C#中的Nullable值类型。 Nullable使用起来有点笨拙,但这只是一个语法问题。 (我宁愿只用var本身访问var的值,而不是var.Value。)创建一些东西去另一种方式 - 默认情况下是引用/可空的类型,但可以选择转换为值/不可空 - 肯定需要的不仅仅是通用类和语法糖。
-
有两种方法可以使类型不可为空:(1)允许特定的固定默认值; (2)要求创建一个对象或数组槽,其中包含该类型字段的结果,调用特定于类型的构造函数,该构造函数必须在包含对象暴露在任何地方之前完成。第一种选择在某些情况下可能很有用,但在许多情况下,最合理的默认值是陷阱表示。第二种选择有时可能是有用的,但在许多情况下会减慢常见情况(数组中的每个插槽在读取之前都会被重写)。
我们在(很少)允许空值实际需要的地方使用选项类型,并且我们有更少的模糊错误,因为任何对象引用都将保证指向适当类型的有效实例。
-
选项类型在概念上与可空类型有何不同?
-
不是。只是默认情况下不允许为null,并且在需要的情况下明确表示允许null。
-
@Gabe选项类型是可以为空的类型。但是,具有不同类型的语言通常具有类型不可为空的属性。某些类型(例如float或list)可能在其域中内置了null类型。选项类型为我们提供了一种将可空性引入需要的其他类型的方法。问题不在于存在可空性;它无处不在。如果您构建的语言不可为空,则选项类型允许您在适当的时候重新引入可空性。
-
[编辑:ninja'd :)]据我所知,它确实不是,除了选项类型明确表示值可以为null / unset,而在C#,Java等中实现的可空类型可以是默认情况下为null / unset,没有任何显式声明。
-
bcat:在C#中,值类型可以是可空的(例如int或int?),但引用类型总是可以为空。
-
@Gabe选项类型与可空类型不同,因为编译器在不先检查null的情况下不允许使用该值。在F#中,它看起来像match opt with | Some(value) -> do_something_with value | None -> oops_its_null。如果您尝试执行do_something_with opt,则会出现编译时类型错误。
-
这是正确的答案。这是对SO投票人口的悲伤评论,它没有获得更多的投票。
-
@Gabe:选项类型和可空类型之间存在一些差异。一个是你可以有一个选项选项(即一个值为None,Some(None)和Some(x)的类型),这在具有参数多态的语言中尤为重要。另一个不同之处在于,学习(对于程序员)或实现(对于实现者)来说,这是一个较少的概念:选项只是众多数据结构中的一个(0或1 x),如list(任意数量的x顺序),数组(某个固定数量的x),对(x和ay),...
Haskell是一种强大的语言,没有无效的概念。基本上,每个变量必须初始化为非空值。如果要表示"可选"变量(变量可能有值,但可能没有),可以使用特殊的"Maybe"类型。
在Haskell中实现这个系统比在C#中更容易,因为数据在Haskell中是不可变的,所以有一个你以后填充的空引用真的没有意义。但是,在C#中,链接列表中的最后一个链接可能具有指向下一个链接的空指针,该链接在列表展开时填充。我不知道没有null类型的过程语言会是什么样子。
此外,请注意上面的许多人似乎建议用特定于类型的逻辑"无"值(999-999-9999,"NULL"等)替换空值。这些值并没有真正解决任何问题,因为人们对空值的问题在于它们是一种特殊情况,但人们忘记为特殊情况编码。对于特定于类型的逻辑无关值,人们仍然忘记为特殊情况编写代码,但他们避免了捕获此错误的错误,这是一件坏事。
您可以采用一个简单的规则:将所有变量初始化(默认情况下,这可以被覆盖)为由变量类定义的不可变值。对于标量,这通常是某种形式的零。对于引用,每个类将定义其"null"值是什么,并且将使用指向此值的指针初始化引用。
这实际上是NullObject模式的语言范围实现:http://en.wikipedia.org/wiki/Null_Object_pattern
因此它并没有真正摆脱空对象,它只是使它们不能成为必须如此处理的特殊情况。
-
不,它们仍然是必须如此处理的特殊情况。您最终会得到更难以调试的错误,因为它们会被忽略而不是立即引发异常。
-
如果(foo == 0)不比If更优雅(foo == null)。在许多情况下,它比首先允许空值更有问题。我知道你只是回答这个问题,而不是捍卫Hoare的立场本身,但我无法阻止自己发表评论......
-
@Gabe,如果null值旨在表示"什么都不做"以外的其他内容,则需要测试null值。我认为这些测试的地方都来自测试和设计,但我从未使用过这种方式的语言(嘿,也许有一个原因!),所以我不知道它在实践中有多好用。
-
@Richard,我没有看到你的观点。我们的想法是将引用初始化为指向一个空对象,该对象基本上不会为为其定义的操作执行任何操作,从而避免在大多数情况下检查确实该引用确实指向空值对象。您仍然可以通过比较引用来区分空对象引用和非空对象引用,例如,如果myclassobject == myclass.null,但在大多数情况下您不需要。
-
我理解这种模式。我同意,如果你的语言特别没有表现,那么它有时是唯一的方法。我只是觉得情况会更糟。至少一个nullref会提前崩溃并提供堆栈跟踪。一个不合适的MyClass.Null可能无限期地被发现。它们可能在数量上更少,但调试听起来更加阴险。
-
当null是一个sentinel值时,编译器可以告诉我每次需要检查它或插入自己的引发异常的检查。使用null对象模式,我只需要猜测我需要在检查中放置的位置,并且历史记录显示程序员不善于进行此类检查。
-
没有正确初始化的int与值为零的int之间存在差异。而且,没有任何价值可以正常运作。我们理想地喜欢<未初始化> + 3 == <未初始化>,但实际上它将具有不同的值,并且我们将丢失初始化信息。 (这适用于IEEE浮点类型,假设您可以将它们初始化为某种形式的NaN。)换句话说,使用此方案,您必须在可能未初始化的任何时候显式测试int,并且我不要看看这对任何事情都有所改善。
-
@David,从程序员的角度来看,这个方案没有未初始化的变量。有些语言可以保证标量默认初始化为零,并且它适用于所有标量类型,包括浮点数。我不知道是否有任何初始化对实际对象的引用,这将是不同的。
-
@ergosys:然而,"初始化"并不意味着在这样的环境中有很多。初始化为任意值的变量并不比垃圾更有用,因为它是未初始化的 - 除非您可以并且测试初始化??。我宁愿坚持有关未初始化变量的编译器警告。
我想你指的是这个话题:"空参考:十亿美元的错误"
-
是的,刚刚通过(非常小的)[non-nullable]标签找到了
Null不是问题,它是允许您编写访问可能为null的值的代码的语言。
如果语言只需要检查任何指针访问权限或首先将其转换为非可空类型,则99%的null相关错误将消失。例如。在C ++中
1 2 3 4 5 6 7 8 9 10 11 12
| void fun(foo *f)
{
f->x; // error: possibly null
if (f)
{
f->x; // ok
foo &r = *f; // ok, convert to non-nullable type
if (...) f = bar; // possibly null again
f->x; // error
r.x; // ok
}
} |
遗憾的是,这不能对大多数语言进行改装,因为它会破坏大量代码,但对于新语言来说却是非常合理的。
Tcl是一种语言,不仅没有null的概念,而且null本身的概念与语言的核心不一致。在tcl中我们说:'一切都是字符串'。它真正意味着什么是tcl有一个严格的值语义(它恰好默认为字符串)。
那么tcl程序员用什么来表示"无数据"呢?大部分都是空字符串。在某些情况下,空字符串可以表示数据,那么它通常是以下之一:
无论如何都要使用空字符串 - 大多数时候它对最终用户没有任何影响。
使用您知道在数据流中不存在的值 - 例如字符串"_NULL_"或数字9999999或我最喜欢的NUL字节"\0"。
使用围绕值的数据结构 - 最简单的是列表(其他语言称为数组)。一个元素的列表表示该值存在,零元素表示null。
测试变量的存在 - [info exists variable_name]。
值得注意的是,Tcl并不是唯一具有严格值语义的语言。 C也有严格的值语义,但值的默认语义恰好是整数而不是字符串。
哦,差点忘了另一个:
有些库使用数字2的变体,允许用户指定"无数据"的占位符。基本上它允许您指定默认值(如果不是,则默认值通常默认为空字符串)。
-
是的,所以你在Tcl中,基本上有一些非常类似于null的东西,但最终会变得更糟。使用魔术值比使用空值更糟糕。
没有NULL我们会做什么?发明吧! :-)如果你正在寻找带内指针值来表达实际上不是指针,你不必成为火箭科学家使用0。
实际上,在任何首先允许指针或对象引用的强大编程语言中,都会出现代码能够访问没有运行任何初始化代码的指针的情况。有可能保证将这些指针初始化为某个静态值,但这似乎并不十分有用。如果一台机器有一般捕获未初始化变量(无论是指针还是别的东西)的方法,那比特殊套管空指针更好,但是我看到的最大的与空相关的错误发生在允许使用空指针进行算术的实现中。将5添加到(char *)0不应该产生到地址5的字符指针;它应该触发一个错误(如果它适合创建指向绝对地址的指针,那么应该有一些其他的方法)。
我们会创建各种奇怪的结构来传达对象"无效"或"不存在"的信息,如其他答案所示。 null可以传达的消息。
-
正如我在这里解释的那样,Null对象模式有其缺点。
-
特定于域的空值。这迫使你检查魔术数字,这是不好的。
-
集合包装器,其中空集合意味着"没有价值"。可以为空的包装器会更好,但这与检查null或使用Null对象模式没有太大区别。
就个人而言,我会编写一些允许我使用null的C#预处理器。然后,这将映射到某个dynamic对象,只要在其上调用方法,就会抛出NullReferenceException。
早在1965年,空引用可能看起来像是一个错误。但是现在,随着各种代码分析工具向我们发出关于空引用的警告,我们不必担心这么多。从编程角度来看,null是一个非常有价值的关键字。
-
参数选项类型是特定于域的,可由编译器和读者轻松检查。除了低级实现之外,Null没有合法用途。
我们使用其中之一
鉴。一个额外的属性或标志或指示符,表示值为"null"且必须被忽略。
特定领域的空白。特定值 - 在允许的域内 - 被解释为"忽略此值"。例如,社会安全号码999-99-9999可能是特定于域的空值,表示SSN未知或不适用。