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


161

我听说它说在编程语言中包含空引用是“十亿美元的错误”。但为什么?当然,它们会导致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!"); }

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

我想,由于更具描述性,所以与NullReferenceException相比,CustomerNotFoundException的调试要容易一些。这就是全部吗?


54
我会警惕“如果客户存在/然后获取客户”的方法,一旦您编写了两行这样的代码,就为竞争环境打开了大门。获取,然后检查null / catch异常更为安全。
Carson63000

26
只要我们能消除所有运行时错误的源头,我们的代码将被保证是完美的!如果C#中的NULL引用使您
不知所措

但是在某些语言中存在空合并和安全导航运算符
jk。

35
null本身并不坏。在T类型和T + null类型之间没有区别的类型系统是不好的。
Ingo 2014年

27
几年后回到我自己的问题,我现在完全是Option / Maybe方法的转换。
Tim Goodman 2014年

Answers:


91

空是邪恶

有相关的介绍关于这个主题:空引用:数十亿美元的错误托尼·霍尔

选项类型

函数式编程的替代方法是使用Option类型,该类型可以包含SOME valueNONE

一篇不错的文章“ Option” Pattern,它讨论了Option类型并提供了Java的实现。

我还发现了有关此问题的Java错误报告:向Java添加Nice Option类型以防止NullPointerExceptions。所请求的功能是Java 8中引入的。


5
C#中的Nullable <x>是一个类似的概念,但与选项模式的实现相去甚远。主要原因是在.NET System.Nullable中,它仅限于值类型。
MattDavey

27
+1对于选项类型。在熟悉Haskell的Maybe之后,空值开始显得很奇怪……
Andres F.

5
开发人员可以在任何地方出错,因此您将获得空选项异常,而不是空指针异常。后者比前者好吗?
greenoldman

7
@greenoldman没有“空选项异常”之类的东西。尝试将NULL值插入定义为NOT NULL的数据库列中。
乔纳斯(Jonas)

36
@greenoldman空值的问题是任何东西都可以为空,作为开发人员,您必须格外小心。一String类是不是真的一个字符串。这是一种String or Null类型。完全遵循选项类型概念的语言在其类型系统中不允许使用null值,从而保证了,如果定义的类型签名需要一个String(或其他)类型,则必须具有一个值。这里的Option类型用于提供可能具有或不具有值的事物的类型级别表示,因此您知道必须在何处显式处理这些情况。
KChaloux 2014年

127

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

没错,优雅的错误处理在功能上可以与null检查if语句相同。但是,当你的东西说服自己不可能发生的事情可能是一个空的,其实空?Kerboom。不管接下来发生什么,我都敢打赌1)它不会很优美; 2)您不会喜欢它。

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


27
+1通常需要尽快识别生产代码中的错误,以便可以将其纠正(通常是数据错误)。调试越容易,越好。

4
如果将其应用于OP的任何一个示例,则答案是相同的。您测试错误条件还是不进行测试,要么优雅地处理它们,要么不进行处理。换句话说,从语言中删除空引用不会改变您将遇到的错误类别,只会巧妙地更改这些错误的表现。
dash-tom-bang 2010年

19
+1表示未爆炸的炸弹。对于空引用,说的public Foo doStuff()意思不是“做某事并返回Foo结果”,而是指“做某事并返回Foo结果,或者返回null,但是您不知道这是否会发生。” 结果是您必须对所有内容进行空检查才能确定。还可以删除空值,并使用特殊的类型或模式(例如值类型)来指示无意义的值。
Matt Olenik

12
@greenoldman,您的示例在证明我们的观点上是双重有效的,因为使用原始类型(无论是null还是整数)作为语义模型是一种不好的做法。如果您有一个负数不是有效答案的类型,则应创建一个新的类型类来实现具有该含义的语义模型。现在,用于处理该非负类型的建模的所有代码都定位于其类,与系统的其余部分隔离开来,并且可以立即捕获为该类创建负值的任何尝试。
2013年

9
@greenoldman,您将继续证明原始类型不足以进行建模。首先是负数。现在是除数为零的除法运算符。如果您知道不能将其除以零,那么您可以预测结果不会很漂亮,并负责确保进行测试,并为该情况提供适当的“有效”值。软件开发人员负责了解其代码在其中运行的参数,并进行适当的编码。这是工程与修补之间的区别。
2013年

50

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

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

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

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

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

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

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

通过为要表示特殊状态的每个接口创建NullObject类,需要进行更多工作。但是至少专业化是隔离到每个特殊状态的,而不是可能存在该状态的代码。IOW,减少了测试数量,因为方法中的替代执行路径更少


7
...因为弄清楚谁生成了(或者更不用说更改或初始化)填充了您认为有用的对象的垃圾数据比跟踪NULL引用容易得多?我不买它,尽管我大多被困在静态类型的土地上。
dash-tom-bang

但是,垃圾数据比空引用少得多。特别是在托管语言中。
Dean Harding

5
-1为空对象。
DeadMG

4
点(4)和(5)是可靠的,但NullObject不是补救措施。您将陷入更糟糕的陷阱-逻辑(工作流)错误。当您完全了解当前情况时,可以肯定会有所帮助,但是盲目使用(而不是null)会花费更多。
greenoldman

1
@ gnasher729,尽管Objective-C的运行时不会像Java和其他系统那样抛出空指针异常,但由于我在回答中概述的原因,它并不能更好地使用空引用。例如,如果在[self.navigationController.presentingViewController dismissViewControllerAnimated: YES completion: nil];运行时中没有navigationController 将其视为无操作序列,则视图不会从显示中删除,并且您可能坐在Xcode前面数小时试图找出原因。
Huperniketes

48

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

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,但是我对此并不熟悉。我更喜欢这种方式,因为理论上正确的代码只需要添加一个字符即可正确编译。


11
哇,真酷。除了在编译时捕获更多错误外,它使我不必检查文档以找出是否GetByLastName返回null(如果未找到)(与引发异常相反),我可以看看它是否返回Customer?or Customer
Tim Goodman 2010年

14
@JBRWilkinson:这只是一个展示想法的成熟示例,而不是实际实现的示例。实际上,我认为这是一个很好的主意。
Dean Harding

1
您说对了,但这不是空对象模式。
Dean Harding

13
这看起来像什么哈斯克尔称之为Maybe-一个Maybe Customer或者是Just c(这里cCustomer)或Nothing。在其他语言中,它称为Option-查看有关此问题的其他一些答案。
MatrixFrog 2011年

2
@SimonBarker:你可以肯定,如果Customer.FirstName是类型String(而不是String?)。这才是真正的好处-只有变量/有一个有意义的性质没有什么价值应该被声明为空。
2014年

20

有很多很好的答案可以解决的不幸症状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它不是任何类型的值,因此您无能为力!

程序员永远无法确定变量实际上是否包含Foo,程序也不能保证;为了避免未定义的行为,它必须在将变量"Hello, world!"用作Foos 之前检查变量。请注意,在前面的代码段中执行字符串检查不会传播foo1确实是a的事实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而省略空检查。每个人都赢。


在Perl 6中,类似空值的值呢?它们始终为null,除非始终输入。难道还要你可以写一个问题my Int $variable = Int,这里Int的类型为空值Int
Konrad Borowski14 2014年

@xfix我对Perl不熟悉,但是只要您没有this type only contains integers相对于的强制执行方法,this type contains integers and this other value每次需要整数时都会遇到问题。
2014年

1
+1用于模式匹配。通过上面提到的有关选项类型的大量答案,您可能会认为有人会提到这样一个事实,即简单的语言功能(例如模式匹配器)可以使操作它们比使用null更直观。
2015年

1
我认为单子绑定比模式匹配甚至更有用(尽管Maybe绑定当然是使用模式匹配实现的)。我认为看到C#之类的语言为很多事物(???.等等)添加特殊语法,就像haskell之类的语言实现为库函数一样,这很有趣。我认为这更整洁。null/ Nothing传播在任何方面都不是“特殊的”,那么为什么我们必须学习更多的语法呢?
萨拉

14

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

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

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

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

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

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

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

字符串s = new Integer(1234);

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

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


3
在C语言中,类型变量*int可以标识intNULL。同样在C ++中,类型变量*Widget可以标识a Widget,其类型源自WidgetNULL。Java和C#等派生类的问题在于它们使用存储位置Widget来表示*Widget。解决方案不是假装a *Widget不能持有NULL,而是要拥有适当的Widget存储位置类型。
2014年

14

null指针是一个工具

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

请稍等一下,以查找和修复典型的无效指针错误所需的时间,而不是查找和修复空指针错误。检查指向的指针很容易null。验证非空指针是否指向有效数据是一种PITA。

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

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


编辑:虽然NullObject模式似乎比可能为null的引用是一个更好的解决方案,但它本身会引入问题:

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

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

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


您能提出一个为什么存在的理由null吗?可以通过“这不是问题,只要不要犯错误”就几乎可以纠正任何语言缺陷。
2014年

您将无效的指针设置为什么值?
JensG 2014年

好吧,您不要将它们设置为任何值,因为您根本不应该允许它们。而是使用其他类型的变量(可能称为)Maybe T,该变量可以包含NothingT值。这里的关键是,您被迫在允许使用前检查是否Maybe确实包含T,并提供处理措施,以防万一。而且由于您不允许使用无效的指针,所以现在您不必检查任何非的内容Maybe
2014年

1
@JensG在您的最新示例中,编译器是否使您在每次调用之前进行空检查t.Something()?还是通过某种方式知道您已经进行了检查,而又不让您再次进行检查?如果在传递t给此方法之前进行了检查怎么办?使用Option类型,编译器通过查看类型可以知道您是否已经完成检查t。如果它是一个Option,则您没有选中它,而如果它是未包装的值,那么您已经选中了它。
Tim Goodman 2014年

1
当询问值时,空对象返回什么?
JensG 2014年

8

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

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>但仍然可以得到未定义的行为...


6
-1为“杠杆”!
Mark C 2010年

8
但是可以肯定,许多常见的编程构造都允许使用荒谬的代码,不是吗?除以零(可以说)是毫无意义的,但我们仍然允许除法,并希望程序员记得记住除数是否为非零(或处理异常)。
Tim Goodman 2010年

3
我不确定您是“从头开始” Maybe是什么意思,但不是内置在核心语言中,其定义恰好在标准的前奏中:data Maybe t = Nothing | Just t
fredoverflow

4
@FredOverflow:由于默认情况下会加载前奏,所以我认为它是“从头开始”的,Haskell具有足够惊人的类型系统,可以用语言的一般规则定义此(和其他)细节,这只是一个奖励:)
Matthieu M.

2
@TimGoodman虽然用零除可能不是所有数值类型的最佳示例(IEEE 754定义了用零除的结果),但这种情况可能会引发异常,而不是用类型的值T除以其他类型的值T,则将操作限制为type T的值除以type 的值NonZero<T>
kbolino '16

8

还有什么选择?

可选类型和模式匹配。由于我不了解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!");
}

6

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

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

因此,实际上,空值最终成为昂贵的东西。


1
如果您提供了一个示例,该示例需要花费更多的精力来处理null,则可能会有所帮助。在我公认的过于简化的示例中,两种方法的工作量几乎相同。我不同意您的看法,我只是发现特定的(伪)代码示例比一般的声明更清晰。
Tim Goodman 2010年

2
啊,我没有清楚地说明自己。并不是说处理空值更加困难。要保证对所有可能为空的引用的所有访问都已经受到安全保护,这是极其困难的(如果不是不可能的话)。也就是说,在允许空引用的语言中,要保证任意大小的代码没有空指针异常是不可能的,这要经过一些重大的困难和努力。这就是使null引用成为昂贵的事情的原因。要保证完全没有空指针异常是非常昂贵的。
luis.espinal,2010年

4

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


4

问题不仅仅在于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


我认为Option <T>的另一个重要点是它是一个monad(因此也是一个endofunctor),因此您可以在包含可选类型的值流上进行复杂的计算,而无需明确检查方法的None每个步骤。
2016年

3

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

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

当古德曼不在时,查找该怎么办?没有空值,您必须返回某种默认客户-现在您要向该默认客户销售商品。

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

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


1
这是一个很好的答案。编程中的null构造不是问题,这是所有null值的实例。如果您创建一个默认对象,最终所有的空值都将被这些默认值替换,并且您的UI,数据库以及介于两者之间的所有地方都将被这些默认值填充,这可能不再有用。但是您可能会在晚上入睡,因为至少我的程序不会崩溃。
克里斯·麦考尔

2
您假定这null是对缺少值进行建模的唯一(甚至是更可取的)方法。
2016年

1

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

一直需要检查是否为空是一件很麻烦的事情-您希望能够只掌握Customer对象并使用它。

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

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

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


我会断言,您永远都不知道是否可以信任传入的数据,因为它传入的是函数并且是可为空的类型。可以重构代码以使输入完全不同。因此,如果可能为空,请务必进行检查。因此,您最终得到了一个系统,在该系统中,每次访问变量之前,都要对每个变量进行null检查。不检查它的情况将成为程序的失败百分比。
克里斯·麦考尔

0

没有例外!

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

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

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

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


1
与几年前发布问题相比,我现在对Option类型了解更多,因为我现在经常在Scala中使用它们。Option的要点在于,它会将分配None给非Option的东西分配给编译时错误,并且Option类似地使用好像它具有一个值,而无需先检查它是否是编译时错误。编译错误肯定比运行时异常好。
Tim Goodman 2014年

例如:您不能说s: String = None,您必须说s: Option[String] = None。如果s是Option,则不能说s.length,但是您可以通过模式匹配来检查它是否具有值并同时提取值:s match { case Some(str) => str.length; case None => throw RuntimeException("Where's my value?") }
Tim Goodman 2014年

不幸的是,在Scala中您仍然可以s: String = null,但这就是与Java互操作性的代价。我们通常在我们的Scala代码中不允许这种事情。
Tim Goodman 2014年

2
但是,我同意这样的一般性评论:异常(正确使用)不是一件坏事,而吞咽它来“修复”异常通常是一个糟糕的主意。但是对于Option类型,目标是将异常转化为编译时错误,这是更好的选择。
Tim Goodman

@TimGoodman是仅Scala战术,还是可以在Java和ActionScript 3等其他语言中使用它?如果您可以用完整的示例编辑答案,那也将是很好的。
Ilya Gazman 2014年

0

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

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

使用空对象模式。 有时,缺少值是您系统的有效状态。空对象模式是一个很好的解决方案。它避免了进行常量显式检查的需要,但仍允许您表示一个空状态。正如某些人所声称的那样,它不是“在错误条件下书写”,因为在这种情况下,空状态是有效的语义状态。话虽如此,当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)

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

将其与进行对比null,在其中进行类型检查就很好,但是如果在用例中很幸运的话,您必须对每个步骤进行显式检查,或者整个应用程序在运行时崩溃,在这种情况下,您可以进行单元测试。只是不值得麻烦。


0

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

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

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

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

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语言中填充了Hoare逻辑,但在现代软件制作中并未无效使用它。


0

没有。

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

在类似问题的语言中,该语言允许null为任何-ish类型的值,但不提供任何构造以供日后使用,从而导致事情null出乎意料(尤其是从其他地方传递给您时),以及因此您会崩溃。那是邪恶的:让您使用null而不用处理它。


-1

基本上,中心思想是应该在编译时而不是运行时捕获空指针引用问题。因此,如果您编写的函数带有某个对象,并且您不希望任何人使用空引用来调用它,则类型系统应该允许您指定该要求,以便编译器可以使用它来提供对函数的调用的保证如果调用者不满足您的要求,则进行编译。这可以通过多种方式来完成,例如,修饰类型或使用选项类型(在某些语言中也称为可空类型),但是显然,对于编译器设计者而言,这需要做很多工作。

我认为这是可以理解的,为什么在1960年代和70年代,编译器设计师没有全力以赴地实现这一想法,因为计算机资源有限,编译器需要保持相对简单,而且没人真正知道这有多么糟糕40几年下来。


-1

我有一个简单的反对意见null

通过引入歧义性,它完全破坏了代码的语义

通常,您希望结果是某种类型的。这在语义上是合理的。假设您向数据库查询了具有确定的用户id,那么您期望结果为特定类型(= user)。但是,如果没有该ID的用户怎么办?一种(坏的)解决方案是:增加null价值。所以结果是模棱两可的:要么是a,user要么是null。但是null不是预期的类型。这就是代码气味开始的地方。

在避免中null,您可以使代码在语义上清晰明了。

总有办法左右null:重构收藏,Null-objectsoptionals等。


-2

如果在运行代码以为其计算值之前在语义上可以访问引用或指针类型的存储位置,则对于应该发生的事情没有任何完美的选择。使此类位置默认为空指针引用是一种可能的选择,该指针可以自由读取和复制,但是如果取消引用或建立索引,则会出错。其他选择包括:

  1. 位置默认为空指针,但不允许代码查看它们(甚至确定它们为空)而不会崩溃。可能提供一种将指针设置为null的显式方法。
  2. 将位置默认为空指针,如果尝试读取一个指针,则崩溃,但是提供了一种测试指针是否持有空值的非崩溃方法。还提供将指针设置为null的显式方法。
  3. 使位置默认为指向某些由编译器提供的指定类型的默认实例的指针。
  4. 位置默认为指向由特定程序提供的指定类型的默认实例的指针。
  5. 可以通过任何空指针访问(而不仅仅是解除引用操作)调用某些程序提供的例程来提供实例。
  6. 设计语言语义,以使除非存在不可访问的编译器临时文件,否则存储位置的集合将不存在,直到在所有成员上都运行了初始化例程之前(必须为数组构造函数提供一个默认实例,一个可以返回实例的函数,或者可能是一对函数-一个可以返回实例,如果在构造数组期间发生异常,则可以在先前构造的实例上调用一个函数)。

最后一种选择可能会吸引人,特别是如果一种语言同时包含可为null和不可为null的类型(一种可以为任何类型调用特殊的数组构造函数,但是仅在创建不可为null的类型的数组时才需要调用它们),但在发明空指针的那段时间可能不可行。在其他选择中,似乎没有一个比允许复制空指针但不对其取消引用或索引更具吸引力。方法4可以很方便地作为一种选择,并且实现起来应该很便宜,但是它当然不是唯一的选择。要求指针默认情况下必须指向某个特定的有效对象要比使指针默认为可以读取或复制但不能取消引用或索引的空值要糟糕得多。


-2

是的,在面向对象的世界中,NULL是一个糟糕的设计。简而言之,使用NULL会导致:

  • 临时错误处理(而不是异常)
  • 模棱两可的语义
  • 缓慢而不是快速失败
  • 计算机思维与对象思维
  • 可变和不完整的对象

检查此博客文章以获取详细说明:http : //www.yegor256.com/2014/05/13/why-null-is-bad.html

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.