我听说它说在编程语言中包含空引用是“十亿美元的错误”。但为什么?当然,它们会导致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更具调试性,因为它更具描述性。这就是全部吗?

评论

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

只要我们能够消除所有运行时错误的源头,我们的代码将被保证是完美的!如果C#中的NULL引用使您不知所措,请尝试使用C中的无效但非NULL的指针;)

null本身还不错。没有区别T类型和T + null类型的类型系统是不好的。

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

当Tony Hoare叫空一个十亿美元的错误时,他正在谈论语言设计。如果要设计语言,请以不允许空引用的方式进行。如果您的语言已经有空引用,那么您就必须绕开它们。在现代语言中,我知道Perl不允许空引用,而我认为Python和Ruby也不允许。不幸的是,Java确实如此。

#1 楼

null是邪恶的

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

选项类型

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

好文章“ Option”模式讨论了Option类型并为Java提供了它的实现。 >
我还发现了有关此问题的Java错误报告:向Java添加Nice Option类型以防止NullPointerExceptions。 Java 8中引入了所需的功能。

评论


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

–马特·戴维(MattDavey)
2012年7月13日在17:47

+1对于选项类型。在熟悉Haskell的Maybe之后,null看起来很奇怪。

– Andres F.
2012年7月13日在17:53

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

–greenoldman
13年5月22日在13:24

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

–乔纳斯(Jonas)
13年5月22日在15:58



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

– KChaloux
2014年3月13日在15:01

#2 楼

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

优美的错误处理在功能上可以与null-check if语句相同。但是,当您说服自己不可能为null时,实际上是null会发生什么呢? Kerboom。不管接下来发生什么,我都敢打赌1)它不会很优美,2)您不会喜欢它。

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

评论


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

–user1249
2010-10-18 20:46

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

–dash-tom-bang
2010-10-18 23:54

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

–马特·奥莱尼克(Matt Olenik)
2010-10-19 18:25

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

– Huperniketes
13年5月26日4:10



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

– Huperniketes
2013年6月6日10:17



#3 楼

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

首先,它通常用于指示特殊状态。正常情况下,不像在专业化时那样为每个状态定义新的类或常量,而是使用null引用使用有损的,广义的类型/值。

其次,当null为空时,调试代码变得更加困难引用出现,并且即使您可以跟踪其上游执行路径,也要尝试确定生成它的原因,哪个状态有效以及其原因。
第三,空引用引入了要测试的其他代码路径。

第四,一旦将空引用用作参数以及返回值的有效状态,防御性编程(针对由设计引起的状态)就需要在各个地方进行更多的空引用检查……以防万一第五,该语言的运行时在对对象的方法表执行选择器查找时已经在执行类型检查。因此,通过检查对象的类型是否有效来重复工作,然后让运行时检查有效的对象的类型以调用其方法。

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

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

评论


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

–dash-tom-bang
2010-10-19 0:00
但是,垃圾数据比空引用少得多。特别是在托管语言中。

–迪恩·哈丁(Dean Harding)
2010-10-19在1:05

-1为空对象。

– DeadMG
2012年7月13日在22:18

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

–greenoldman
13年5月22日在13:29

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

– Huperniketes
2015年4月13日在2:31



#4 楼

除非您不期望它们,否则Null并不是那么糟糕。您应该需要在代码中明确指定期望为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)的原因是,它们不提供根据其作用域来更改变量的类型的方法。如果语言可以处理该问题,则可以完全解决该问题。类型系统。使用option-types和Maybe也可以解决此问题,但是我不太熟悉。我更喜欢这种方式,因为理论上正确的代码只需要添加一个字符即可正确编译。

评论


哇,真酷。除了在编译时捕获更多错误外,它使我不必检查文档以弄清楚GetByLastName是否在未找到的情况下返回null(与引发异常相反),我可以看看它是否返回Customer?或客户。

–蒂姆·古德曼(Tim Goodman)
10-10-18在19:37

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

–迪恩·哈丁(Dean Harding)
2010-10-18 23:12



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

–迪恩·哈丁(Dean Harding)
2010-10-18 23:15

就像Haskell所说的Maybe一样-Maybe Customer要么是Just c(其中c是Customer),要么是Nothing。在其他语言中,它称为Option -有关此问题的其他答案。

– MatrixFrog
11年8月22日在18:38



@SimonBarker:您可以确定Customer.FirstName是否为String类型(而不是String?)。那才是真正的好处-只有具有有意义的空值的变量/属性才应声明为可为空。

–阳谷
2014年5月2日在16:17

#5 楼

有很多很好的答案可以解决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的变量应仅包含Foo s-这就是类型系统的重点! null不是Foo,而是"Hello, world!"。更糟糕的是,null不是任何类型的值,您无法执行任何操作!

程序员永远无法确定变量实际上是否包含Foo,程序也无法保证;为了避免未定义的行为,必须先将"Hello, world!"的变量用作Foo,然后再对其进行检查。请注意,在前面的代码段中执行字符串检查不会传播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类型的变量确实都包含Foo,编译器可以省略空检查。每个人都赢。

评论


在Perl 6中,类似空值的值呢?它们始终为空,除非它们总是被键入。仍然可以编写我的Int $ variable = Int,其中Int是Int类型的null值吗?

– Konrad Borowski
2014年4月25日12:00

@xfix我对Perl不熟悉,但是只要您没有强制这种类型只包含整数而不是这种类型包含整数和其他值的方法,每次您都会遇到问题想要整数。

–Doval
2014年4月25日在12:29

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

–法律
2015年4月10日在22:30

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

–萨拉
16年6月13日在6:29

#6 楼

一个null指针是一个工具,不是敌人。您只应该使用'em right'。

想一想,查找和修复典型的无效指针错误所需的时间要比查找和修复空指针错误所需的时间长。检查指向null的指针很容易。验证非空指针是否指向有效数据是一种PITA。

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

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


编辑:尽管NullObject模式似乎比可能为null的引用更好,但它本身会带来问题:调用NullObject的引用(根据理论)应在调用方法时不执行任何操作。因此,由于错误地分配了错误的引用,可能会引入细微的错误,这些引用现在可以保证为非null(yehaa!),但会执行有害的操作:什么也不做。使用NPE,问题的根源就显而易见了。对于表现出某种行为(但有误)的NullObject,我们引入了错误(过早)被发现的风险。这并非偶然地类似于无效指针问题,并且具有类似的结果:该引用指向的东西看起来像有效的数据,但并非如此,它引入了数据错误并且可能难以追踪。坦白地说,在这些情况下,我会眨眼间更喜欢现在立即失效的NPE,而不是逻辑/数据错误,后者后来突然击中了我。还记得IBM关于错误成本取决于在哪个阶段发现错误的研究吗?
当在NullObject上调用方法,调用属性getter或函数或方法通过out返回值时,什么都不做的想法不成立。假设返回值是int,我们决定“不执行任何操作”表示“返回0”。但是我们如何确定0是正确的“无”(不是)返回呢?毕竟,NullObject不应做任何事情,但是当要求输入值时,它必须以某种方式做出反应。失败不是一种选择:我们使用NullObject来防止NPE,我们肯定不会将它与另一个异常(未实现,除以零,...)进行交易,对吗?那么当您必须退货时,如何正确地什么也不退货呢?

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

评论


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

–Doval
2014年3月13日19:52



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

– JensG
2014年3月13日19:59

好吧,您不要将它们设置为任何值,因为您根本不应该允许它们。而是使用其他类型的变量,可能称为Maybe T,该变量可以包含Nothing或T值。这里的关键是,在被允许使用之前,您不得不检查Maybe是否确实包含T,并提供操作步骤以防万一。而且由于您不允许使用无效的指针,因此现在您不必检查任何可能不是Maybe的东西。

–Doval
2014年3月13日在20:05

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

–蒂姆·古德曼(Tim Goodman)
2014年3月13日在21:04



当要求输入值时,空对象返回什么?

– JensG
14年3月14日在9:17

#7 楼

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

null的具体问题是它破坏了静态输入。

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

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

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

(顺便说一句,编译器可以解决一些问题,就像在编译时标记i = 1 / 0这样的表达式一样,但是您不能真正期望编译器遵循函数并确保参数都与函数的逻辑一致。 >
实际的问题是,您需要做很多额外的工作,并添加null检查以在运行时保护自己,但是如果忘记了该怎么办?编译器阻止您进行赋值:


字符串s = new Integer(1234);


为什么要允许赋值( null),这将破坏对s的取消引用?通过在代码中混合“无值”和“类型化”引用,给程序员增加了负担。而且,当NullPointerExceptions发生时,对其进行跟踪可能会更加耗时。您不是让静态语言说“这很可能是对期望的东西的引用。”而不是依靠静态类型来说“这是对期望的东西的引用”。

评论


在C语言中,* int类型的变量标识一个int或NULL。同样在C ++中,类型为* Widget的变量要么标识Widget(其类型从Widget派生而来),要么为NULL。 Java和C#等派生类的问题在于它们使用存储位置Widget来表示* Widget。解决方案不是假装* Widget不应该保持NULL,而应该具有适当的Widget存储位置类型。

–超级猫
2014年8月28日在2:25

#8 楼

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

foo = null
foo.bar()


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



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


通常的想法是将变量放在一个盒子里,而你唯一要做的就是

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

评论


-1为“杠杆”!

– Mark C
10-10-18在19:17

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

–蒂姆·古德曼(Tim Goodman)
2010-10-18 20:14

我不确定“从头开始”是什么意思,但是也许不是核心语言内置的,它的定义恰好在标准的前奏中:data may t = Nothing |只是

– fredoverflow
2010-10-18 21:28

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

– Matthieu M.
10-10-20在15:00

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

– kbolino
16-2-11在16:29



#9 楼


还有什么选择?


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


#10 楼

空值的问题在于允许它们使用的语言在很大程度上迫使您针对它进行防御性编程。要确保




不为空的对象确实绝不会花费很多精力(远远超过尝试使用防御性的if-block) null,并且
您的防御机制实际上可以有效地处理所有潜在的NPE。

因此,null最终是一件昂贵的事情。 br />

评论


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

–蒂姆·古德曼(Tim Goodman)
2010-10-18 20:10

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

–luis.espinal
2010-10-19在0:52

#11 楼

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


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

如果在获取东西时可以使用普通的可空类型,然后在那儿处理空值,那会更好吗?然后将其作为不可为null的类型传递给所有小辅助函数和类,而不始终检查是否为null?

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

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

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

评论


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

–萨拉
16年5月23日在10:35

#12 楼

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

#13 楼

空是邪恶的。但是,缺少null可能会带来更大的危害。

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

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

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

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

评论


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

–克里斯·麦考尔(Chris McCall)
15年4月13日在19:19

您假定null是建模缺少值的唯一(甚至是更可取的)方法。

–萨拉
16年5月23日在11:07

因此,空引用异常是一件好事,因为它们使您知道流程中的数据有问题,而不是在数据问题导致其他问题之后才使数据追踪变得混乱。我认为我们的文化喜欢通过推迟和经历更大的痛苦来避免直接的痛苦。

–OCDev
20/09/12在15:57

#14 楼

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

始终需要检查null是很乏味的-您希望能够只掌握Customer对象并使用它。

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

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

如果您处在不知道是否可以信任的情况下输入的数据(参数),可能是因为它是由用户输入的,因此,您还可以提供“ TryGetCustomer(name,out customer)”模式。与int.Parse和int.TryParse的交叉引用。

评论


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

–克里斯·麦考尔(Chris McCall)
2015年4月10日在18:52

#15 楼

异常不是eval!

出现异常是有原因的。如果您的代码运行不正常,则有一个天才模式,称为异常,它告诉您某些事情是错误的。

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

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

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

评论


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

–蒂姆·古德曼(Tim Goodman)
2014年3月13日19:55

例如:您不能说s:字符串=无,而必须说s:Option [String] =无。如果s是一个Option,则不能说s.length,但是可以使用模式匹配来检查它是否具有值并同时提取该值:s match {case Some(str)=> str。长度; case None => throw RuntimeException(“我的值在哪里?”)}

–蒂姆·古德曼(Tim Goodman)
2014年3月13日19:56



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

–蒂姆·古德曼(Tim Goodman)
14年3月13日在19:57

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

–蒂姆·古德曼(Tim Goodman)
2014年3月13日在20:01

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

–伊利亚·加兹曼(Ilya Gazman)
2014年3月15日上午10:55

#16 楼

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


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

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

#17 楼

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

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

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

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

null周围总有很多方法:重构到集合Null-objectsoptionals

#18 楼

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进行对比,它在这里进行类型检查就很好了,但是您必须在每个步骤上进行显式检查如果您很幸运在用例中进行单元测试,则可能会导致整个应用程序在运行时崩溃。只是不值得麻烦。

#19 楼

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

#20 楼

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

我认为这是可以理解的为什么在1960年代和70年代,编译器设计者并没有全力以赴地实现这一想法,因为计算机的资源有限,编译器需要保持相对简单,并且没人真正知道这会导致40年的失败。

#21 楼

否。

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

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

#22 楼

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


将位置默认为空指针,但是不允许代码查看它们(甚至确定它们为空)而不会崩溃。可能提供一种将指针设置为null的显式方法。

将位置默认为null指针,如果尝试读取一个指针,则崩溃,但是提供了一种非崩溃的方法来测试是否指针为空。还提供一种将指针设置为null的显式方法。

将位置默认为指向由特定编译器提供的指定类型的默认实例的指针。

将位置默认为

具有指向某些特定程序提供的指示类型的默认实例的指针。

可以进行任何空指针访问(不仅是取消引用操作),还可以调用某些程序提供的例程来提供实例。 >
设计语言语义,以使除不可访问的编译器临时文件外,不存在存储位置的集合,直到在所有成员上都运行了初始化例程之前(必须提供数组构造函数之类的东西)一个默认实例,一个将返回一个实例的函数或可能是一对函数-一个用于返回实例,如果在构造数组期间发生异常,则一个函数将在先前构造的实例上调用)。 br />

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