it-swarm.cn

空引用真的是一件坏事吗?

我听说它说在编程语言中包含空引用是“十亿美元的错误”。但为什么?当然,它们会导致NullReferenceExceptions,但是那又如何呢?如果使用不当,该语言的任何元素都可能成为错误的来源。

还有什么选择?我想不是这样说:

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

您可以这样说:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

但是,如何更好呢?无论哪种方式,如果您忘记检查客户是否存在,都会遇到异常。

我想,CustomerNotFoundException比NullReferenceException更具调试性,因为它更具描述性。这就是全部吗?

165
Tim Goodman

空是邪恶

在InfoQ上有一个有关此主题的演示:空引用:十亿美元的错误作者Tony Hoare

选项类型

函数式编程的替代方法是使用 选项类型 ,其中可以包含SOME valueNONE

一篇不错的文章“选项”模式讨论了Option类型并提供了Java的实现。

我还发现了有关此问题的Java)错误报告: 向Java防止NullPointerExceptions 添加尼斯选项类型。 请求的功能 在Java 8。

93
Jonas

问题在于,因为从理论上讲任何对象都可以为null并在尝试使用它时抛出异常,所以面向对象的代码基本上是未爆炸炸弹的集合。

没错,优雅的错误处理在功能上可以与对if语句进行空检查相同。但是,当您说服自己无法做到时,会发生什么? 可能 为空实际上是空吗? Kerboom。不管接下来发生什么,我都敢打赌1)它不会很优美; 2)您不会喜欢它。

并且不要忽略“易于调试”的价值。成熟的生产规范是一种疯狂的,无所适从的生物。任何可以让您更深入了解问题出在哪里以及可以节省大量挖掘时间的事情。

129
BlairHippo

在代码中使用空引用存在几个问题。

首先,它通常用于指示特殊状态。通常不像专门化那样为每个状态定义新的类或常量,而是使用空引用使用有损,大规模泛化的类型/值

其次,当出现空引用并且您试图确定生成它的原因时,即使您可以跟踪其上游执行路径,哪种状态有效及其原因,调试代码也会变得更加困难。

第三,空引用引入要测试的其他代码路径

第四,一旦将空引用用作参数以及返回值的有效状态,防御性编程(针对由设计引起的状态)就需要为了在各种情况下进行更多的空引用检查..。

第五,在对象的方法表上执行选择器查找时,该语言的运行时已在执行类型检查。因此,通过检查对象的类型是否有效来重复工作,然后让运行时检查有效对象的类型以调用其方法。

为什么不使用 NullObject模式 进行利用运行时检查的好处使其调用 [〜#〜] nop [〜#〜] 方法特定于该状态(符合常规状态的接口),同时还消除了整个代码库中对空引用的所有额外检查?

通过为要表示特殊状态的每个接口创建一个NullObject类,它涉及更多工作。但是至少对每个特殊状态专业化是孤立的,而不是可能存在该状态的代码。 IOW,因为您的方法中有较少的替代执行路径,所以减少了测试数量。

50
Huperniketes

空值还算不错,除非您不期望它们。您应该需要在代码中明确指定您期望为null的代码,这是一种语言设计问题。考虑一下:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

如果尝试在Customer?上调用方法,则应收到编译时错误。更多语言不执行此操作(IMO)的原因是,它们不提供根据其作用域来更改变量的类型的方法。如果语言可以处理该问题,则可以完全解决该问题。类型系统。

还有使用选项类型和Maybe处理此问题的功能方法,但我对此并不熟悉。我更喜欢这种方式,因为理论上正确的代码只需要添加一个字符即可正确编译。

48

有很多很好的答案可以解决null的不幸症状,所以我想提出一个替代的论点:Null是类型系统中的缺陷。

类型系统的目的是确保程序的不同组件正确地“组合”在一起;一个类型正确的程序不能“脱离轨道”进入未定义的行为。

考虑一个假设的Java语言,或者您偏爱的静态类型语言是什么,您可以在其中将字符串"Hello, world!"分配给任何类型的任何变量:

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

您可以像这样检查变量:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

这没有什么不可能的-如果有人愿意,可以设计出这种语言。特殊值不必是"Hello, world!"-可能是数字42,元组(1, 4, 9)或_null。但是,为什么要这样做呢? Foo类型的变量应仅包含Foos-这就是类型系统的重点! null不是Foo,而不是"Hello, world!"。更糟糕的是,null不是any类型的值,因此您无能为力!

程序员永远无法确定变量实际上是否包含Foo,程序也不能保证;为了避免未定义的行为,它必须先检查"Hello, world!"的变量,然后再将它们用作Foos。请注意,在前面的代码段中执行字符串检查不会传播foo1实际上是Foo-bar的事实,为了安全起见,也可能会进行自己的检查。

与使用具有模式匹配的Maybe/Option类型进行比较:

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

Just foo子句中,您和程序都肯定知道我们的Maybe Foo变量确实确实包含Foo值-信息在调用链中传播,而bar不需要进行任何检查。因为Maybe Foo是与Foo不同的类型,所以您不得不处理它可能包含Nothing的可能性,因此您永远不能被NullPointerException蒙蔽。您可以更轻松地对程序进行推理,并且编译器可以知道所有Foo类型的变量确实包含Foos而省略空检查。每个人都赢。

20
Doval

null指针是一种工具

不是敌人您只应该使用'em right'。

请稍等一下,与查找和修复空指针错误相比,查找和修复典型的(无效指针)错误需要多长时间。检查指向null的指针很容易。验证非空指针是否指向有效数据是一种PITA。

如果仍然不满意,请向您的方案中添加大量的多线程,然后再考虑一下。

故事的寓意:不要把孩子们扔到水里。而且不要怪事故的工具。很久以前发明零数字的那个人非常聪明,因为那天你可以命名为“ nothing”。 null指针与该指针相距不远。


EDIT:尽管NullObject模式似乎是比可能为空的引用更好的解决方案,但它本身会带来问题:

  • 当调用方法时,持有NullObject的引用应该(根据理论)不执行任何操作。因此,由于错误地分配了错误的引用,可能会引入细微的错误,这些引用现在可以保证为非null(yehaa!),但会执行不必​​要的操作:什么也不做。使用NPE,问题的根源就显而易见了。对于以某种方式表现(但不正确)的NullObject,我们会引入错误(过早)被发现的风险。这并非偶然地类似于无效指针问题,并且具有类似的后果:该引用指向的东西看起来像有效的数据,但事实并非如此,它引入了数据错误并且可能难以跟踪。老实说,在这些情况下,我会眨眼间更喜欢NPE发生故障立即,现在,而不是逻辑/数据错误,后者后来突然击中了我。还记得IBM关于错误成本的研究,该成本取决于在哪个阶段发现错误?

  • NullObject上调用方法时,在调用属性getter或函数或方法通过out返回值时不执行任何操作的概念不成立。假设返回值是int,我们决定“不执行任何操作”表示“返回0”。但是我们如何确定0是正确的“无”(不是)返回呢?毕竟,NullObject不应执行任何操作,但是当要求输入值时,它必须以某种方式做出反应。失败不是一种选择:我们使用NullObject来防止NPE,我们肯定不会将它与另一个异常(未实现,除以零,...)进行交易,对吗?那么,当您必须退货时,如何正确地什么也不退货呢?

我无济于事,但是当有人尝试将NullObject模式应用于每个问题时,它看起来更像是尝试通过解决另一个错误来修复一个错误。在某些情况下,毫无疑问,这是一个很好且有用的解决方案,但它肯定不是所有情况下的灵丹妙药。

15
JensG

(把我的帽子扔给一个老问题;)

Null的特定问题在于它会破坏静态类型。

如果我有_Thing t_,则编译器可以保证我可以调用t.doSomething()。好吧,除非运行时UNLESS t为空。现在所有赌注都关闭了。编译器说还可以,但后来我发现t实际上不_doSomething()。因此,我不能等待编译器捕获类型错误,而必须等到运行时才能捕获它们。我不妨只使用Python!

因此,从某种意义上说,null将动态类型引入到静态类型的系统中,并具有预期的结果。

除以零或对数为负等之间的区别是,当i = 0时,它仍然是整数。编译器仍然可以保证其类型。问题是,逻辑以逻辑不允许的方式错误地应用了该值...但是,如果逻辑做到了,那几乎就是错误的定义。

(顺便说一句,编译器可以解决其中的一些问题,就像在编译时标记诸如_i = 1 / 0_这样的表达式。但是您真的不能指望编译器遵循函数并确保参数都与函数的逻辑一致)

实际的问题是您需要做很多额外的工作,并添加空检查以在运行时保护自己,但是如果您忘记了一个检查该怎么办?编译器阻止您分配:

字符串s = new Integer(1234);

那么,为什么要允许分配一个值(null)来破坏对s的取消引用呢?

通过在代码中混合“无值”和“类型化”引用,您将给程序员增加负担。而且,当NullPointerExceptions发生时,对其进行跟踪可能会更加耗时。而不是依靠静态类型说“此is对期望的东西的引用”,而是让语言说“此可能是对期望的东西的引用”。 ”

14
Rob

还有什么选择?

可选类型和模式匹配。由于我不了解C#,因此以下是一种虚构的语言,称为Scala#:-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}
8
fredoverflow

空引用是一个错误,因为它们允许使用非意义的代码:

foo = null
foo.bar()

如果您使用类型系统,则还有其他选择:

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

通常的想法是将变量放在一个盒子中,唯一可以做的就是将其拆箱,最好像为Eiffel建议的那样争取编译器帮助。

Haskell从头开始(Maybe),在C++中可以使用boost::optional<T>,但您仍然可以得到不确定的行为...

8
Matthieu M.

空值的问题在于允许它们使用的语言在很大程度上迫使您针对它进行防御性编程。需要花费大量的精力(远远超过尝试使用防御性的if-block)来确保

  1. 您期望它们not为null的对象确实永远不会为null,并且
  2. 您的防御机制确实可以有效应对所有潜在的NPE。

因此,实际上,空值最终是一件昂贵的事情。

6
luis.espinal

这个问题是您的编程语言在运行程序之前尝试证明其正确性的程度。用静态类型语言证明您具有正确的类型。通过使用非空引用的默认值(带有可选的可空引用),您可以消除传递null而不应该传递null的许多情况。问题是,在程序正确性方面,处理非空引用的额外努力是否值得受益。

4
Winston Ewert

问题不仅仅在于null,还在于您无法在许多现代语言中指定非null引用类型。

例如,您的代码可能看起来像

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (Egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    Egg.Crack();
    MixingBowl.Mix(Egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (Egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

当类引用被传递时,防御性编程会鼓励您再次检查所有参数是否为空,即使您只是事先检查了它们是否为空,尤其是在构建被测试的可重用单元时。在整个代码库中,可以轻松地对同一引用进行10次空值检查!

如果在得到东西时可以有一个普通的可为空的类型,然后在那儿处理null,然后将其作为不可为空的类型传递给所有小辅助函数和类,而不检查所有的null,那会更好吗?时间?

这就是Option解决方案的重点-不允许您打开错误状态,而是允许设计隐式不能接受空参数的函数。类型的“默认”实现是可为空还是不可为空,与使两个工具都可用的灵活性相比,重要性不那么重要。

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

这也被列为C#的第二大投票功能-> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c

4
Michael Parker

空是邪恶的。但是,缺少null可能是更大的弊端。

问题在于,在现实世界中,您经常会遇到没有数据的情况。您的示例中没有空值的版本仍然会爆炸-您在逻辑上的错误并忘记了检查Goodman,或者在检查和查找之间,Goodman结婚了。 (如果您认为Moriarty正在观察代码的每一部分并试图使您绊倒,则有助于评估逻辑。)

当古德曼不在的时候,她会做什么?没有空值,您必须返回某种默认客户-现在您要向该默认客户销售商品。

从根本上讲,归根结底,无论代码如何工作还是正确工作,对代码而言是否更为重要。如果不当行为比无行为更可取,那么您就不希望使用null。 (有关如何正确选择此示例的示例,请考虑首次发射ArianeV。未捕获的/ 0错误导致火箭硬转,并且因此而自行解散时发射自毁式火箭。)它试图计算出助推器被点燃后实际上不再具有任何作用-尽管例行返回垃圾,它仍会进入轨道。)

在100中至少有99次,我会选择使用null。不过,我会为自己的版本感到高兴。

3
Loren Pechtel

针对最常见的情况进行优化。

一直需要检查是否为空是很乏味的-您希望能够仅持有Customer对象并使用它。

在正常情况下,这应该可以正常工作-您进行查找,获取对象并使用它。

例外情况中,如果您(随机)按名称查找客户,而又不知道该记录/对象是否存在,则需要某种指示,以表明失败。在这种情况下,答案是抛出RecordNotFound异常(或让下面的SQL提供程序为您执行此操作)。

如果您不知道是否可以信任传入的数据(参数)(可能是因为它是由用户输入的),则还可以提供“ TryGetCustomer(name,out customer)”。图案。与int.Parse和int.TryParse的交叉引用。

1
JBRWilkinson

没有例外!

他们在那里是有原因的。如果您的代码运行不正常,则有一个叫做exception的天才模式,它告诉您某些事情是错误的。

通过避免使用空对象,您可以隐藏这些异常的一部分。我没有在OP示例中大惊小怪,他将空指针异常转换为类型良好的异常,这实际上可能是一件好事,因为它提高了可读性。就像@Jonas指出的那样,当采用选项类型解决方案时,您将隐藏异常。

与在生产应用程序中相比,单击按钮选择空选项时不会引发异常,什么也没有发生。代替抛出空指针,将抛出异常,我们可能会得到该异常的报告(就像在许多生产应用程序中,我们都具有这种机制),然后我们才可以实际对其进行修复。

通过避免异常使代码防弹是一个坏主意,通过修复异常使代码防弹,这就是我会选择的路径。

0
Ilya Gazman

没有。

语言无法解决null这样的特殊值是该语言的问题。如果您查看 KotlinSwift 之类的语言,它们会迫使编写者在每次遇到时都明确地处理空值的可能性,那么没有危险。

在上述语言中,该语言允许null为任何值-ish 类型,但不提供任何构造以供日后使用,以免意外(尤其是从其他地方传递给您)时导致null这是邪恶的:让您使用null而不用处理它。

0
Ben Leggiero

Nulls是有问题的,因为必须对其进行显式检查,但是编译器无法警告您忘记检查它们。只有费时的静态分析才能告诉您这一点。幸运的是,有几种不错的选择。

将变量移出范围。经常(当程序员过早声明变量或将变量保留太长时间时,null用作占位符。最好的方法就是简单地摆脱变量。在您输入有效值之前,请勿声明它。这并不是您想像的那样困难的限制,它使您的代码更加简洁。

使用空对象模式。有时,缺少的值是您系统的有效状态。空对象模式是一个很好的解决方案。它避免了需要进行持续的显式检查,但仍允许您表示一个空状态。正如某些人所声称的那样,它不是“在错误条件下书写”,因为在这种情况下,空状态是有效的语义状态。话虽如此,当空状态不是有效的语义状态时,您不应使用此模式。您应该只将变量超出范围。

使用Maybe/Option。首先,这使编译器警告您需要检查缺少的值,但它不仅可以将一种显式检查替换为另一种。使用Options,您可以链接您的代码,并继续进行操作,就好像您的值存在一样,而无需真正检查直到最后一刻。在Scala中,示例代码如下所示:

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

在第二行中,我们正在构建令人敬畏的消息,我们没有明确检查是否找到了客户,美丽之处在于我们不需要。直到最后一行,可能是以后的很多行,甚至是在不同的模块中,我们都明确地考虑到OptionNone会发生什么。不仅如此,如果我们忘记这样做,就不会进行类型检查。即使这样,它也可以以非常自然的方式完成,而无需显式的if语句。

null进行对比,它在其中进行类型检查就很好了,但是如果您在用例中很幸运,那么您必须在其中进行每个步骤的显式检查,否则整个应用程序在运行时都会崩溃。您的单元测试练习。只是不值得麻烦。

0
Karl Bielefeldt

NULL的中心问题是它使系统不可靠。 1980年,托尼·霍尔(Tony Hoare)在致力于图灵奖的论文中写道:

因此,我对ADA的创建者和设计师的最佳建议被忽略了。 …。请勿在可靠性至关重要的应用中使用该语言的当前状态,例如核电站,巡航导弹,预警系统,弹道导弹防御系统。下一个由于编程语言错误而误入歧途的火箭可能并不是探空太空火箭在无害前往金星的途中:它可能是一个核弹头爆炸了我们一个城市。与不安全的汽车,有毒的农药或核电站的事故相比,不可靠的编程语言生成不可靠的程序对我们的环境和社会构成更大的风险。保持警惕以减少风险,而不是增加风险。

从那以后,ADA语言发生了很大的变化,但是Java,C#和许多其他流行语言仍然存在此类问题。

开发人员有责任在客户和供应商之间创建合同。例如,在C#中,就像在Java中一样,您可以使用Generics通过创建只读NullableClass<T>(两个选项)来最大程度地降低Null引用的影响:

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

然后将其用作

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

或通过C#扩展方法使用两个选项样式:

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

差异是显着的。作为方法的客户,您知道会发生什么。团队可以有以下规则:

如果一个类不是NullableClass类型,那么它的实例必须不为null

团队可以使用 按合同设计 并在编译时进行静态检查来增强此想法,例如,前提是:

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

或一个字符串

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

这些方法可以大大提高应用可靠性和软件质量。按合同设计是 Hoare逻辑 的一种情况,1988年,Bertrand Meyer在他着名的《面向对象的软件构造》一书和Eiffel语言中对此进行了填充,但是在现代软件制作中并未无效使用它。

0
Artru