为什么在抛出异常的时候避免使用空引用呢?


21

我不太了解某些编程语言人员对null引用的一致抨击。他们有什么不好?如果我请求对不存在的文件进行读取访问,那么我很高兴得到异常或空引用,但是异常被认为是好的,但空引用被认为是坏的。这背后的原因是什么?



2
某些语言比其他语言更容易崩溃。对于“托管代码”,例如.Net / Java,空引用只是另一种类型的问题,而其他本机代码可能无法很好地处理此问题(您未提及特定语言)。即使在受管理的环境中,有时您也想编写故障安全代码(嵌入式?,武器?),有时又想尽快地大声疾呼(单元测试)。两种类型的代码都可能调用同一个库-这将是一个问题。总的来说,我认为试图不损害计算机感觉的代码是一个坏主意。无论如何,故障安全是很困难的。
Job

@工作:这是提倡懒惰。如果您知道如何处理异常,则可以对其进行处理。有时,这种处理可能涉及引发另一个异常,但是您绝对不应让空引用异常未经处理。曾经 那是每个维护程序员的噩梦。这是整个树中最无用的异常。只是问堆栈溢出
亚伦诺特2011年

或换一种说法-为什么不返回某种表示错误信息的代码。这种争论将在未来的几年中激怒。
gbjbaanb

Answers:


24

Null引用不会比异常“回避”,至少我所认识或阅读的任何人都不会。我认为您误会了传统智慧。

不好的是尝试访问空引用(或取消引用空指针等)。这很糟糕,因为它总是表明存在错误;你绝不会做这样的事情有目的的,如果你有意这样做,那么这更糟糕,因为它使其无法区分错误行为预期行为。

某些边缘群体实际上出于某种原因而真的讨厌无效性的概念,但是正如Ed所指出的那样,如果您没有null或者nil只是用其他东西代替它,这可能会导致某些结果。比崩溃更糟糕(例如数据损坏)。

实际上,许多框架都包含这两个概念。例如,在.NET中,您会看到一种常见的模式是一对方法,每个方法都以单词开头Try(例如TryGetValue)。在这种Try情况下,引用设置为其默认值(通常为null),在另一种情况下,将引发异常。两种方法都没有错。两者都经常在支持它们的环境中使用。

实际上,这完全取决于语义。如果null是有效的返回值(如搜索集合的一般情况),则返回null。另一方面,如果它不是有效的返回值(例如,使用来自您自己的数据库的主键查找记录),那么返回null将是一个坏主意,因为调用者不会期望它,并且可能不会检查。

弄清楚要使用哪种语义非常简单:对于未定义的函数结果是否有意义? 如果是这样,则可以返回空引用。如果不是,则抛出异常。


5
实际上,有些语言没有null或nil,也不需要“用其他东西代替它”。可空引用意味着可能存在某些东西,也可能没有。如果仅要求用户显式检查那里是否有东西,那么您已经解决了问题。有关实际示例,请参见haskell。
约翰娜·拉尔森

3
@ErikKronberg,是的,“十亿美元的错误”和所有的胡说八道,人们大步向前,声称它是新鲜的和令人着迷的游行队伍永无休止,这就是为什么删除先前的评论主题的原因。人们永远不会提出的这些革命性替代品总是Null Object,Option或Contract的某种变体,它们实际上并没有神奇地消除潜在的逻辑错误,他们只是推迟或推广了该错误。无论如何,这显然是有关确实具有的编程语言的问题null,因此,实际上Haskell与此处无关。
亚罗诺(Aaronaught)2013年

1
您是否在认真地争论不要求对null进行测试与要求进行null一样好?
约翰娜·拉尔森

3
@ErikKronberg:是的,我“严重争论”,必须测试null与(a)必须围绕Null对象的行为设计应用程序的每一层,(b)必须进行模式匹配并没有特别的区别。始终保持选择状态,或者(c)不允许被叫方说“我不知道”并强迫发生异常或崩溃。还有一个原因,为什么null忍受这么好这么久了,和广大否则谁说的人似乎很少经验的设计真实世界应用与像不完整的需求或最终一致性约束的学者。
亚罗诺(Aaronaught)2013年

3
@Aaronaught:有时候我希望有一个downvote按钮来发表评论。没有理由这样大声疾呼。
Michael Shaw 2013年

11

最大的区别是,如果您省去了处理NULL的代码,则代码将在以后继续崩溃,并出现一些不相关的错误消息,其中与异常一样,异常将在失败的初始点引发(打开在您的示例中读取的文件)。


4
如果无法处理NULL,则可能会故意忽略您的代码与返回的代码之间的接口。会犯此错误的开发人员会使用不会执行NULL的语言使其他人陷入困境。
Blrfl 2011年

@Blrfl,理想的方法是很短的,因此很容易弄清楚问题出在哪里。一个好的调试器通常可以很好地捕获一个空引用异常,即使代码很长。如果我试图从注册表中读取关键设置,那该怎么办?我的注册表搞砸了,与静默地重新创建节点并将其设置为默认值相比,让用户失败和惹恼用户更好。如果病毒这样做了怎么办?所以,如果我得到一个null,我应该抛出一个专门的异常还是让它撕裂?用简短的方法有什么大不同?
Job

@Job:如果没有调试器怎么办?您是否意识到99.99%的时间您的应用程序将在发布环境中运行?发生这种情况时,您将希望使用更有意义的异常。您的应用程序可能仍然必须失败并使用户烦恼,但至少它将输出调试信息,使您能够迅速找到问题所在,从而将烦恼降至最低。
亚伦诺特2011年

@Birfl,有时我不想处理自然会返回null的情况。例如,假设我有一个将键映射到值的容器。如果我的逻辑保证我永远不会尝试读取未曾存储的值,那么我永远都不会返回null。在那种情况下,我宁愿有一个异常,该异常会提供尽可能多的信息来指示出了什么问题,而不是返回null以使程序中的其他地方神秘地失败。
温斯顿·埃韦特

换句话说,除了例外,我必须明确处理异常情况,否则我的程序立即死亡。如果代码没有明确处理异常情况,则使用null引用时,它将尝试li行。我认为最好以失败告终。
温斯顿·埃韦特

8

因为null值不是编程语言的必要部分,而是一致的bug源。就像您说的那样,打开文件可能会导致失败,该失败可以作为空返回值或通过异常传达回去。如果不允许使用空值,则存在一致的,唯一的通信失败方式。

另外,这不是null的最常见问题。大多数人记得在调用可能会返回null的函数后检查null。通过在程序执行的各个点允许变量为空,该问题在您自己的设计中更加严重。您可以将代码设计为从不允许使用null值,但是如果在语言级别不允许使用null,则无需这样做。

但是,实际上,您仍然需要某种方式来表示变量是否已初始化。然后,您将遇到某种形式的错误,其中程序不会崩溃,而是继续使用一些可能无效的默认值。老实说,我不知道哪个更好。为了我的钱,我喜欢早早崩溃。


考虑带有标识字符串的未初始化子类,其所有方法均引发异常。如果其中之一出现,您就会知道发生了什么。在内存有限的嵌入式系统上,未初始化工厂的生产版本只能返回null。
Jim Balter

3
@吉姆·巴尔特(Jim Balter):我想我对如何在实践中真正起到帮助感到困惑。在任何不平凡的程序中,您都必须在某些时候处理可能未初始化的值。因此,必须有某种方式来表示默认值。因此,您仍然必须在继续之前进行检查。因此,您现在可以处理无效数据,而不是潜在的崩溃。
Ed S.

1
您还知道在获得null返回值时发生了什么:请求的信息不存在。没有区别 无论哪种方式,如果调用者实际上需要出于某些特定目的使用该信息,则它都必须验证返回值。无论是验证null,null对象还是monad都没有实际区别。
亚伦诺特2011年

1
@吉姆·巴尔特(Jim Balter):是的,我仍然没有看到实际的区别,也没有看到它如何使人们在学术界之外编写真正的程序变得更加轻松。这并不意味着没有好处,只是对我而言似乎并不明显。
Ed S.

1
我已经用Uninitialized类解释了两次实际的区别-它标识了null项的起源-使得在取消引用null时可以查明错误。至于围绕无空范式设计的语言,它们避免了从一开始就出现的问题,它们提供了替代的编程方式。如果您不熟悉它们,可能很难理解。结构化编程也避免了很多错误,也曾经被认为是“学术性的”。
Jim Balter

5

托尼·霍尔(Tony Hoare)首先提出了空引用的想法,但称其为一百万美元的错误

问题不在于本质上是否为空引用,而是与大多数(其他)类型安全语言中缺乏适当的类型检查有关。

从语言上说,这种缺乏支持意味着错误“ null-bugs”可能在被检测到之前潜伏在程序中很长时间。当然,这就是错误的性质,但是现在已知可以避免“空错误”。

由于(例如)C或C ++会导致“硬”错误(程序崩溃,立即崩溃,没有明显的恢复),因此该问题特别存在于C或C ++中。

在其他语言中,始终存在如何处理它们的问题。

在Java或C#中,如果您尝试在空引用上调用方法,则会出现异常,这可能是可以的。因此,大多数Java或C#程序员都习惯了这一点,并且不明白为什么要这么做(嘲笑C ++)。

在Haskell中,您必须显式地为null情况提供一个操作,因此Haskell程序员会向他们的同事幸灾乐祸,因为他们做对了(对吗?)。

确实,这是旧的错误代码/异常辩论,但是这次用Sentinel值代替错误代码。

与往常一样,最合适的选择实际上取决于情况和要查找的语义。


精确地 null确实是邪恶的,因为它颠覆了类型系统。(当然,该替代方法的冗长性(至少在某些语言中具有缺点)。)
机械蜗牛

2

您不能将人类可读的错误消息附加到空指针。

(但是,您可以在日志文件中留下错误消息。)

在一些语言/环境,其允许指针的算术运算中,如果指针参数之一为空并且它允许进入的计算,其结果将是一个无效的非空指针。(*)给您更多的力量。

(*)这在COM编程中经常发生,在这种情况下,如果您尝试调用接口方法,但接口指针为null,则将导致对无效地址的调用,该地址几乎但不完全与零不同。


2

从技术和概念上讲,返回NULL(或数字零或布尔值false)表示错误是错误的。

从技术上讲,你负担的程序员检查返回值马上,在它返回的确切地点。如果您连续打开20个文件,并且通过返回NULL来完成错误信号通知,那么使用代码必须检查每个读取的文件,并打破任何循环和类似结构。这是处理混乱代码的完美方法。但是,如果您选择通过引发异常来发出错误信号,则使用方代码可以选择立即处理异常,或者让其冒泡尽可能多的级别,即使在函数调用之间也是如此。这使得代码更加简洁。

从概念上讲,如果打开文件却出了问题,则返回值(甚至NULL)是错误的。您没有任何要退回的东西,因为您的操作没有完成。返回NULL在概念上等同于“我已经成功读取了文件,这就是它包含的内容-没有内容”。如果这就是您要表达的内容(也就是说,如果将NULL作为所涉及操作的实际结果有意义),则务必返回NULL,但是如果要表示错误,请使用异常。

从历史上看,这种错误是通过这种方式报告的,因为像C这样的编程语言并未在该语言中内置异常处理,并且推荐的方式(使用跳远)有点冗长且有点违反直觉。

这个问题还有一个维护方面:例外,您必须编写额外的代码来处理故障;如果不这样做,该程序将尽早而艰难地失败(这很好)。如果返回NULL表示错误,则程序的默认行为是忽略该错误并继续执行,直到导致其他问题-损坏的数据,segfaults,NullReferenceExceptions,具体取决于语言。为了尽早大声地指出错误,您必须编写额外的代码,然后猜测:这是您在紧迫的期限内遗漏的部分。


1

正如已经指出的,许多语言不会将对空指针的取消引用转换为可捕获的异常。这样做是一个相对现代的技巧。首次认识到空指针问题时,甚至还没有发明过异常。

如果允许空指针作为有效情况,则为特殊情况。您通常需要在许多不同的地方使用特殊情况的处理逻辑。那是额外的复杂性。

无论它是否与潜在的空指针相关,如果使用异常引发来处理特殊情况,则必须以其他方式处理这些特殊情况。通常,必须对每个函数调用进行检查以检查那些异常情况,以防止函数调用被不当调用,或者在函数退出时检测失败情况。使用异常可以避免这种额外的复杂性。

更高的复杂度通常意味着更多的错误。

在数据结构中使用空指针的替代方法(例如,标记链接列表的开始/结束)包括使用前哨项。这些可以以更少的复杂性提供相同的功能。但是,可以有其他方法来管理复杂性。一种方法是将可能为空的指针包装在智能指针类中,以便只在一个地方需要空检查。

检测到空指针时该怎么办?如果您无法建立例外情况处理,则始终可以抛出异常,并将该特殊情况处理有效地委派给调用方。这正是某些语言在取消引用空指针时默认执行的操作。


1

特定于C ++,但此处避免使用空引用,因为C ++中的空语义与指针类型相关联。文件打开函数失败并返回空指针是很合理的。实际上,该fopen()功能正是这样做的。


确实,如果您发现自己在C ++中具有空引用,那是因为您的程序已经损坏
哈兹巨龙

1

这取决于语言。

例如,Objective-C允许您毫无问题地将消息发送到空(nil)对象。调用nil也会返回nil,并且被认为是语言功能。

我个人喜欢它,因为您可以依靠这种行为并避免所有那些复杂的嵌套if(obj == null)结构。

例如:

if (myObject != nil && [myObject doSomething])
{
    ...
}

可以缩短为:

if ([myObject doSomething])
{
    ...
}

简而言之,它使您的代码更具可读性。



-1

因为缺少程序逻辑中的某些内容,所以通常会出现Null引用,即:您进入了一行代码,而没有经过该代码块所需的设置。

另一方面,如果对某事抛出异常,则意味着您认识到该程序的正常运行中可能会发生某种特殊情况,并且正在处理该情况。


2
空引用可能由于多种原因而“出现”,其中很少涉及到任何遗漏的内容。也许您还会将其与null引用异常混淆。
亚伦诺特2011年

-1

空引用通常非常有用:例如,链表中的元素可以有一些后继,也可以没有。使用null“无接班人”是完全自然。或者,一个人可以有null没有配偶- 用于“人没有配偶”是完全自然的,比拥有Person.Spouse成员可以提及的某些“没有配偶”的特殊价值自然得多。

但是:许多值不是可选的。在典型的OOP程序中,我会说一半以上的引用不能null在初始化后出现,否则该程序将失败。否则,代码将必须充满if (x != null)检查。那么,为什么每个引用默认情况下都可以为空?实际上应该是相反的:默认情况下,变量应为不可为空,并且您必须明确地说“哦,该值也可以为null”。


您想将讨论中的任何内容添加回您的答案吗?这个注释线程有点发热,我们想清理一下。任何扩展的讨论都应该用来聊天

在所有这些争吵中,可能会有一两个实际兴趣点。不幸的是,直到我删除了扩展讨论之后,我才看到Mark的评论。以后,如果您想保留评论,直到有时间对其进行审核并适当地编辑您的答案,请标记您的答案以引起主持人的注意。
乔什·K

-1

您的问题措辞混乱。您是说空引用异常(实际上是由于尝试删除参考null)?不想使用此类异常的明显原因是,它不会为您提供任何有关出了什么问题的信息,甚至是什么时候也不提供的信息-该值可能在程序中的任何时候都设置为null。您写道:“如果我请求对不存在的文件进行读取访问,那么我很高兴获得异常或空引用” –但是,您不应该很高兴获得没有给出原因的信息。字符串“尝试取消引用null”中没有任何地方提到读取或不存在的文件。也许您的意思是,您很乐意从读取调用中获取null作为返回值-这是完全不同的事情,但是它仍然没有提供有关读取失败原因的信息;它'


不,我不是说空引用异常。在所有语言中,我都知道未初始化但已声明的变量是某种形式的null。
davidk01 2011年

但是您的问题不是关于未初始化的变量。而且有些语言不对未初始化的变量使用null,而是使用可以选择包含值的包装对象-例如Scala和Haskell。
Jim Balter

1
“ ...而是使用可以选择包含值的包装对象。” 显然,空的类型不同
亚伦诺特2011年

1
在haskell中,没有未初始化的变量。您可能会声明一个IORef并以None作为初始值将其作为种子,但这几乎类似于在其他语言中声明变量并将其保持未初始化状态,这带来了同样的问题。在IO monad haskell之外的纯功能内核中工作,程序员无法求助于引用类型,因此不存在空引用问题。
davidk01 2011年

1
如果您具有非凡的“丢失”值,则与非零的“空”值完全相同- 如果您拥有相同的工具来处理它。那是一个很大的“如果”。无论哪种方式,您都需要额外的复杂性来处理这种情况。在Haskell中,模式匹配和类型系统提供了一种帮助管理这种复杂性的方法。但是,还有其他工具可以管理其他语言的复杂性。例外就是这样一种工具。
Steve314 2011年
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.