我不太了解某些编程语言人员对null引用的一致抨击。他们有什么不好?如果我请求对不存在的文件进行读取访问,那么我很高兴得到异常或空引用,但是异常被认为是好的,但空引用被认为是坏的。这背后的原因是什么?
我不太了解某些编程语言人员对null引用的一致抨击。他们有什么不好?如果我请求对不存在的文件进行读取访问,那么我很高兴得到异常或空引用,但是异常被认为是好的,但空引用被认为是坏的。这背后的原因是什么?
Answers:
Null引用不会比异常“回避”,至少我所认识或阅读的任何人都不会。我认为您误会了传统智慧。
不好的是尝试访问空引用(或取消引用空指针等)。这很糟糕,因为它总是表明存在错误;你绝不会做这样的事情有目的的,如果你是有意这样做,那么这更糟糕,因为它使其无法区分错误行为预期行为。
某些边缘群体实际上出于某种原因而真的讨厌无效性的概念,但是正如Ed所指出的那样,如果您没有null
或者nil
只是用其他东西代替它,这可能会导致某些结果。比崩溃更糟糕(例如数据损坏)。
实际上,许多框架都包含这两个概念。例如,在.NET中,您会看到一种常见的模式是一对方法,每个方法都以单词开头Try
(例如TryGetValue
)。在这种Try
情况下,引用设置为其默认值(通常为null
),在另一种情况下,将引发异常。两种方法都没有错。两者都经常在支持它们的环境中使用。
实际上,这完全取决于语义。如果null
是有效的返回值(如搜索集合的一般情况),则返回null
。另一方面,如果它不是有效的返回值(例如,使用来自您自己的数据库的主键查找记录),那么返回null
将是一个坏主意,因为调用者不会期望它,并且可能不会检查。
弄清楚要使用哪种语义非常简单:对于未定义的函数结果是否有意义? 如果是这样,则可以返回空引用。如果不是,则抛出异常。
null
,因此,实际上Haskell与此处无关。
null
忍受这么好这么久了,和广大否则谁说的人似乎很少经验的设计真实世界应用与像不完整的需求或最终一致性约束的学者。
最大的区别是,如果您省去了处理NULL的代码,则代码将在以后继续崩溃,并出现一些不相关的错误消息,其中与异常一样,异常将在失败的初始点引发(打开在您的示例中读取的文件)。
因为null值不是编程语言的必要部分,而是一致的bug源。就像您说的那样,打开文件可能会导致失败,该失败可以作为空返回值或通过异常传达回去。如果不允许使用空值,则存在一致的,唯一的通信失败方式。
另外,这不是null的最常见问题。大多数人记得在调用可能会返回null的函数后检查null。通过在程序执行的各个点允许变量为空,该问题在您自己的设计中更加严重。您可以将代码设计为从不允许使用null值,但是如果在语言级别不允许使用null,则无需这样做。
但是,实际上,您仍然需要某种方式来表示变量是否已初始化。然后,您将遇到某种形式的错误,其中程序不会崩溃,而是继续使用一些可能无效的默认值。老实说,我不知道哪个更好。为了我的钱,我喜欢早早崩溃。
null
返回值时发生了什么:请求的信息不存在。没有区别 无论哪种方式,如果调用者实际上需要出于某些特定目的使用该信息,则它都必须验证返回值。无论是验证null
,null对象还是monad都没有实际区别。
托尼·霍尔(Tony Hoare)首先提出了空引用的想法,但称其为一百万美元的错误。
问题不在于本质上是否为空引用,而是与大多数(其他)类型安全语言中缺乏适当的类型检查有关。
从语言上说,这种缺乏支持意味着错误“ null-bugs”可能在被检测到之前潜伏在程序中很长时间。当然,这就是错误的性质,但是现在已知可以避免“空错误”。
由于(例如)C或C ++会导致“硬”错误(程序崩溃,立即崩溃,没有明显的恢复),因此该问题特别存在于C或C ++中。
在其他语言中,始终存在如何处理它们的问题。
在Java或C#中,如果您尝试在空引用上调用方法,则会出现异常,这可能是可以的。因此,大多数Java或C#程序员都习惯了这一点,并且不明白为什么要这么做(嘲笑C ++)。
在Haskell中,您必须显式地为null情况提供一个操作,因此Haskell程序员会向他们的同事幸灾乐祸,因为他们做对了(对吗?)。
确实,这是旧的错误代码/异常辩论,但是这次用Sentinel值代替错误代码。
与往常一样,最合适的选择实际上取决于情况和要查找的语义。
null
确实是邪恶的,因为它颠覆了类型系统。(当然,该替代方法的冗长性(至少在某些语言中具有缺点)。)
从技术和概念上讲,返回NULL(或数字零或布尔值false)表示错误是错误的。
从技术上讲,你负担的程序员检查返回值马上,在它返回的确切地点。如果您连续打开20个文件,并且通过返回NULL来完成错误信号通知,那么使用代码必须检查每个读取的文件,并打破任何循环和类似结构。这是处理混乱代码的完美方法。但是,如果您选择通过引发异常来发出错误信号,则使用方代码可以选择立即处理异常,或者让其冒泡尽可能多的级别,即使在函数调用之间也是如此。这使得代码更加简洁。
从概念上讲,如果打开文件却出了问题,则返回值(甚至NULL)是错误的。您没有任何要退回的东西,因为您的操作没有完成。返回NULL在概念上等同于“我已经成功读取了文件,这就是它包含的内容-没有内容”。如果这就是您要表达的内容(也就是说,如果将NULL作为所涉及操作的实际结果有意义),则务必返回NULL,但是如果要表示错误,请使用异常。
从历史上看,这种错误是通过这种方式报告的,因为像C这样的编程语言并未在该语言中内置异常处理,并且推荐的方式(使用跳远)有点冗长且有点违反直觉。
这个问题还有一个维护方面:例外,您必须编写额外的代码来处理故障;如果不这样做,该程序将尽早而艰难地失败(这很好)。如果返回NULL表示错误,则程序的默认行为是忽略该错误并继续执行,直到导致其他问题-损坏的数据,segfaults,NullReferenceExceptions,具体取决于语言。为了尽早大声地指出错误,您必须编写额外的代码,然后猜测:这是您在紧迫的期限内遗漏的部分。
正如已经指出的,许多语言不会将对空指针的取消引用转换为可捕获的异常。这样做是一个相对现代的技巧。首次认识到空指针问题时,甚至还没有发明过异常。
如果允许空指针作为有效情况,则为特殊情况。您通常需要在许多不同的地方使用特殊情况的处理逻辑。那是额外的复杂性。
无论它是否与潜在的空指针相关,如果不使用异常引发来处理特殊情况,则必须以其他方式处理这些特殊情况。通常,必须对每个函数调用进行检查以检查那些异常情况,以防止函数调用被不当调用,或者在函数退出时检测失败情况。使用异常可以避免这种额外的复杂性。
更高的复杂度通常意味着更多的错误。
在数据结构中使用空指针的替代方法(例如,标记链接列表的开始/结束)包括使用前哨项。这些可以以更少的复杂性提供相同的功能。但是,可以有其他方法来管理复杂性。一种方法是将可能为空的指针包装在智能指针类中,以便只在一个地方需要空检查。
检测到空指针时该怎么办?如果您无法建立例外情况处理,则始终可以抛出异常,并将该特殊情况处理有效地委派给调用方。这正是某些语言在取消引用空指针时默认执行的操作。
空引用通常非常有用:例如,链表中的元素可以有一些后继,也可以没有。使用null
“无接班人”是完全自然。或者,一个人可以有null
没有配偶- 用于“人没有配偶”是完全自然的,比拥有Person.Spouse
成员可以提及的某些“没有配偶”的特殊价值自然得多。
但是:许多值不是可选的。在典型的OOP程序中,我会说一半以上的引用不能null
在初始化后出现,否则该程序将失败。否则,代码将必须充满if (x != null)
检查。那么,为什么每个引用默认情况下都可以为空?实际上应该是相反的:默认情况下,变量应为不可为空,并且您必须明确地说“哦,该值也可以为null
”。
您的问题措辞混乱。您是说空引用异常(实际上是由于尝试删除参考null)?不想使用此类异常的明显原因是,它不会为您提供任何有关出了什么问题的信息,甚至是什么时候也不提供的信息-该值可能在程序中的任何时候都设置为null。您写道:“如果我请求对不存在的文件进行读取访问,那么我很高兴获得异常或空引用” –但是,您不应该很高兴获得没有给出原因的信息。字符串“尝试取消引用null”中没有任何地方提到读取或不存在的文件。也许您的意思是,您很乐意从读取调用中获取null作为返回值-这是完全不同的事情,但是它仍然没有提供有关读取失败原因的信息;它'