如果空值是邪恶的,那么当值可以有意义地缺失时应该使用什么?


26

这是一遍又一遍重复的规则之一,这令我感到困惑。

空是邪恶的,应尽可能避免

但是,但是-出于我的天真,让我尖叫-有时可能会有意义地缺乏价值!

请让我在一个示例中问这个问题,该示例来自我目前正在研究的此反模式缠身的可怕代码。从本质上讲,这是一个基于多人网络回合的游戏,其中两个玩家的回合都同时运行(例如在Pokemon中,与Chess相反)。

每次转动之后,服务器都会广播客户端JS代码的更新列表。Update类:

public class GameUpdate
{
    public Player player;
    // other stuff
}

此类被序列化为JSON并发送到连接的播放器。

大多数更新自然都会有一个与之相关的玩家-毕竟,有必要知道哪个玩家在此回合​​中做出了哪个动作。但是,某些更新无法使Player有意义地与它们相关联。示例:游戏被强行捆绑,因为超出了没有动作的回合限制。我认为,对于此类更新,让播放器为空是有意义的。

当然,我可以利用继承来“修复”此代码:

public class GameUpdate
{
    // stuff
}

public class GamePlayerUpdate : GameUpdate
{
    public Player player;
    // other stuff
}

但是,由于两个原因,我看不到有什么改进:

  • 现在,JS代码将简单地接收不带Player的对象作为已定义的属性,这与它为null相同,因为两种情况都需要检查该值是否存在。
  • 如果将另一个可为空的字段添加到GameUpdate类中,则我必须能够使用多重继承来继续进行此设计-但是MI本身是邪恶的(根据经验更丰富的程序员),更重要的是,C#不会有它,所以我不能使用它。

我有一个暗示,这段代码是经验丰富的优秀程序员惊骇地尖叫的其中之一。同时,我看不到这个null会如何伤害任何事物,而应该怎么做。

你能向我解释这个问题吗?


2
这个问题专门针对JavaScript吗?许多语言都为此目的提供了Optional类型,但是我不知道js是否具有一个(虽然您可以制作一个)或是否可以与json一起使用(尽管这是讨厌的null检查的一部分,而不是整个代码的一部分
理查德Tingle

9
那条规则是错误的。我很惊讶有人会赞成这个想法。在某些情况下,有一些很好的替代方法可以替代null。null非常适合表示缺失值。不要让这样的随机互联网规则阻止您信任自己的合理判断。
usr

1
@usr-我完全同意。空是你的朋友。恕我直言,没有一种更清晰的方法来表示缺失值。它可以帮助您查找错误,可以帮助您处理错误情况,可以帮助您原谅(尝试/捕获)。什么不喜欢?!
水肺史蒂夫

4
设计软件的首要规则之一是“始终检查是否为空”。为什么这甚至是一个问题,我都难以理解。
MattE

1
@MattE-绝对。实际上,为什么我要设计一种设计模式,所以Null在基本工具箱中是一件好事。恕我直言,格雷厄姆的建议令我感到工程过度。
水肺史蒂夫

Answers:


20

返回很多东西比返回null更好。

  • 空字符串(“”)
  • 空集合
  • “可选”或“也许”单子
  • 悄无声息的功能
  • 一个充满方法的对象,它静静地不执行任何操作
  • 一个有意义的值,它拒绝问题的不正确前提
    (这是使您首先考虑为null的原因)

诀窍在于实现何时执行此操作。Null真的很擅长于炸毁您的脸,但是只有当您点掉它而不检查它时,才可以。不善于解释原因。

当您不需要崩溃,又不想检查null时,请使用以下替代方法之一。如果一个集合只有0或1个元素,则返回一个集合似乎很奇怪,但是它非常擅长让您静默处理缺失的值。

Monad听起来很花哨,但是在这里,他们要做的就是让您清楚地知道该集合的有效大小为0和1。如果有,请考虑使用它们。

与null相比,这些方法中的任何一个都能使您的意图更清晰。那是最重要的区别。

这可能有助于理解为什么您要返回而不是简单地引发异常。异常本质上是拒绝假设的一种方式(它们不是唯一的方式)。当您要求两条线相交的点时,您假设它们相交。它们可能是平行的。您是否应该抛出“平行线”异常?可以,但是现在您必须在其他地方处理该异常,或者让它停止系统。

如果您愿意留在自己的位置,则可以将对假设的拒绝转化为一种可以表达结果或拒绝的价值。那不是那么奇怪。我们一直在做代数运算。诀窍是使该值有意义,以便我们了解发生了什么。

如果返回的值可以表示结果和拒绝,则需要将其发送到可以同时处理这两个值的代码。在这里使用多态确实非常强大。isPresent()您可以编写在两种情况下均表现良好的代码,而不是简单地将空检查换成检查。通过将空值替换为默认值,或者通过静默不执行任何操作。

null的问题在于它可能意味着太多的事情。因此,它最终只会破坏信息。


在我最喜欢的空想实验中,我请您想象一台复杂的Rube Goldbergian机器,该机器通过从传送带上捡起彩色灯泡,将它们拧入插座并通电来使用彩色光发出编码消息。信号由放置在传送带上的灯泡的不同颜色控制。

您被要求使这件昂贵的东西符合RFC3.14159,RFC3.14159指出在消息之间应该有一个标记。标记完全不亮。它应该恰好持续一个脉冲。单色光正常发光的时间相同。

您不能只在消息之间留空格,因为如果找不到插在插座中的灯泡,设备就会发出警报并暂停。

熟悉该项目的每个人都为触摸机器或控制代码而感到震惊。改变并不容易。你是做什么?好吧,您可以潜入并开始破坏事物。也许更新这件事令人毛骨悚然的设计。是的,您可以做得更好。或者,您可以与管理员联系,然后开始收集烧坏的灯泡。

那就是多态解决方案。系统甚至不需要知道任何更改。


如果您能做到的话,那真的很好。通过将对假设的有意义的拒绝编码为一个值,您不必强迫问题改变。

如果我给你一个苹果,您欠我多少个苹果?-1。因为我昨天欠你两个苹果。在这里,“您将欠我”这一问题的假设被否定了。即使问题是错误的,有意义的信息也会在此答案中进行编码。通过以有意义的方式拒绝它,问题就不会改变。

但是,有时改变问题实际上是要走的路。如果可以使假设更可靠,同时仍然有用,请考虑这样做。

您的问题是,通常情况下,更新来自播放器。但是您发现需要一个不是来自播放器1或播放器2的更新。您需要一个表明时间已到期的更新。两位玩家都没有这么说,那你该怎么办?留下玩家为空似乎很诱人。但是它破坏了信息。您正在尝试将知识编码在一个黑洞中。

玩家字段不会告诉您谁刚刚移动。您认为确实如此,但是这个问题证明这是谎言。播放器字段会告诉您更新的来源。您的时间已过期更新来自游戏时钟。我的建议是给予应有的荣誉,并将player字段名称更改为source

这样,如果服务器关闭并不得不暂停游戏,则它可以发出更新以报告正在发生的事情,并将自身列为更新源。

这是否意味着您永远都不要遗漏任何东西?不能。但是您需要考虑没有什么能真正代表一个众所周知的良好默认值。真的没有什么容易过度使用的。因此,使用时请小心。有一门学派主张什么都不做。称为约定优于配置

我喜欢我自己,但只有在公约以某种方式明确传达时,我才喜欢。这不应该是一种阻碍新手的方法。但是,即使您正在使用它,null仍然不是最好的方法。空无危险。Null假装是您可以一直使用的东西,直到您使用它为止,然后它在宇宙中吹了一个洞(破坏了语义模型)。除非在宇宙中打出洞,否则您就需要这样做,为什么要搞砸呢?使用其他东西。


9
“如果集合只有0或1个元素,则返回一个集合”-很抱歉,但我不认为我可以做到这一点:(从我的POV这样做(a)向类(b)添加了不必要的无效状态,需要记录以下内容:集合最多必须包含1个元素(c)似乎还是不能解决问题,因为在访问该集合之前是否有必要检查该集合是否包含其唯一元素,否则会引发异常吗?
gaazkam

2
@gaazkam看到了吗?我告诉过你它使人烦恼。但这正是您每次使用空字符串时所做的事情。
candied_orange

16
当有效值恰好是空字符串或集时会发生什么?
Blrfl

5
无论如何,@ gaazkam都不会明确检查集合中是否有项目。在空列表上循环(或映射)是安全的,没有任何作用。
RubberDuck

3
我知道您是在回答某个人应该怎么做而不是返回null的问题,但是我确实不同意其中许多观点。但是,由于我们不是特定语言,并且有违反我拒绝
投票的

15

null这不是问题所在。问题是当前大多数编程语言如何处理丢失的信息。他们没有认真对待这个问题(或者至少没有提供大多数人为避免陷阱所需要的支持和安全性)。这导致了我们已经学会担心的所有问题(Javas NullPointerException,Segfaults等)。

让我们看看如何以更好的方式处理该问题。

其他人建议使用Null-Object Pattern。虽然我认为有时它是适用的,但在许多情况下,没有一种适合所有情况的默认值。在那些情况下,可能缺少信息的消费者必须能够自行决定如果该信息丢失了该怎么办。

有一些编程语言可在编译时提供针对空指针的安全性(所谓的空安全性)。举一些现代的例子:Kotlin,Rust,Swift。在那些语言中,丢失信息的问题已经得到了认真的对待,使用null完全没有问题-编译器将阻止您取消引用空指针。
在Java中,已努力建立@ Nullable / @ NotNull批注以及编译器+ IDE插件,以达到相同的效果。它并没有真正成功,但这超出了此答案的范围。

表示可能不存在的显式通用类型是处理它的另一种方法。例如在Java中java.util.Optional。它可以工作:
在项目或公司级别,您可以建立规则/准则

  • 仅使用高质量的第三方库
  • 始终使用java.util.Optional代替null

您的语言社区/生态系统可能提出了比编译器/解释器更好的解决此问题的其他方法。


您能否详细说明一下,Java Optionals比null好吗?当我阅读文档时,如果从可选值中获取值,则该值不存在会产生异常。看来我们在同一个地方:检查是必要的,如果我们忘记了,就会被搞砸了吗?
gaazkam

3
@gaazkam是的,它不提供编译时安全性。另外,Optional本身可能为null,这是另一个问题。但是它是显式的(与可能只是null / doc注释的变量相比)。如果为整个代码库设置了编码规则,这只会​​带来真正的好处。您不能将其设置为null。如果可能缺少信息,请使用“可选”。这迫使明确。然后,通常的空检查成为对Optional.isPresent
marstato '18


10

在声明null不好的陈述中,我没有看到任何好处。我检查了有关的讨论,这些讨论只会让我不耐烦和恼火。

当Null实际上表示某些含义时,可以将其用作“魔术值”。但是您必须要做一些非常复杂的编程才能到达那里。

空值应表示“不存在”,如果它是一个自变量,则表示尚未创建(尚未)或(已经)没有提供。这不是一个状态,整个事情都不见了。在OO环境中,始终创建和销毁事物的有效条件是有意义的,不同于任何状态。

使用null时,您会在运行时受到打击,在运行时会使用某种类型安全的null替代方法在编译时受到打击。

并非完全正确,在使用变量之前,我会收到警告,提示您不要检查null。而且不一样。我可能不想保留没有目的的对象的资源。而且更重要的是,我将不得不检查替代品是否相同,以获得所需的行为。如果我忘记这样做,我宁愿在运行时获取NullReferenceException而不是某些未定义/意外的行为。

有一个需要注意的问题。在多线程上下文中,您必须通过在执行null检查之前首先分配给局部变量来防止竞争情况。

null的问题在于,它可能意味着很多事情。因此,它最终只会破坏信息。

不,它不(不应该)意味着什么,所以没有什么可以破坏的。变量/参数的名称和类型将始终告诉您缺少的内容,这就是所需要知道的全部。

编辑:

我在他有关函数式编程的其他答案之一中链接了视频 candied_orange,这很有趣。我可以看到它在某些情况下的用处。到目前为止,我所看到的功能性方法随处可见代码片段,在面向对象环境中使用时通常会使我感到畏缩。但是我想它有它的位置。某处。而且我猜想有些语言会很好地隐藏我在C#中遇到的令人困惑的混乱局面,而这些语言会提供干净且可行的模型。

如果该功能模型是您的思维定势/框架,那么实际上除了将任何不幸的事情作为记录的错误描述向前推,并最终让调用者处理从一路拖到起点的累积垃圾,别无选择。显然,这就是函数式编程的工作方式。称其为局限,称其为新范式。您可能将其视为功能门徒的骇客,使他们能够在邪恶的道路上走得更远,您可能会对这种新的编程方式感到欣喜,这释放了令人兴奋的新可能性。无论如何,对我来说,进入那个世界似乎没有意义,回到传统的面向对象的做事方式(是的,我们可以抛出异常,对不起),从中选择一些概念,

任何概念都可能以错误的方式使用。从我的立场来看,提出的“解决方案”是正确使用null的替代方案。它们是来自另一个世界的概念。


9
Null破坏了它代表什么的知识。没有什么应该被悄悄忽略的。一种不需要停止系统以避免无效状态的东西。没什么,意味着程序员会搞砸了,需要修复代码。一种没有什么意思,意味着用户要求的东西不存在,需要礼貌地告知。抱歉,没有。所有这些都被编码为空。因此,如果您将其中任何一个编码为null,那么如果没有人知道您的意思,就不会感到惊讶。您是在强迫我们从上下文中猜测。
candied_orange

3
@candied_orange您可以对任何可选类型使用完全相同的参数:None表示…。
Deduplicator

3
@candied_orange我仍然不明白你的意思。没什么不同?如果在应该使用枚举的情况下使用null呢?只有一种没有。什么都不是的原因可能有所不同,但这并不会改变什么也没有改变,也不会改变对什么都不是的适当反应。如果您期望存储中的值不存在并且值得关注,则在查询时抛出或记录日志,却一无所获,则不要将null作为参数传递给方法,然后在该方法中尝试找出为什么你为空。空不会是错误。
马丁·马特

2
空是错误,因为您有机会对无意义进行编码。您对虚无的了解并不要求立即处理虚无。0,null,NaN,空字符串,空集,void,null对象,不适用,未实现的异常,没有很多形式。您不必因为不想处理现在知道的事情而就忘记现在知道的事情。如果我要求您采用-1的平方根,则可以抛出一个异常让我知道这是不可能的,然后忘记一切,或者发明虚数并保留您所知道的。
candied_orange

1
@MartinMaat会让您听起来像函数每次违反您的期望时都必须引发异常。这是不正确的。通常会有一些极端的情况要处理。如果您真的看不出来,请阅读这篇内容
candied_orange

7

你的问题。

但是,但是-出于我的天真,让我尖叫-有时可能会有意义地缺乏价值!

与您一起在船上。但是,我将在一秒钟内说明一些警告。但首先:

空是邪恶的,应尽可能避免。

我认为这是一个很好的想法,但过于笼统。

空不是邪恶。它们不是反模式。这些都不是不惜一切代价避免的事情。但是,空值容易出错(因为无法检查空值)。

但是我不明白如何不检查null是否能以某种方式证明null本质上是错误使用的。

如果null为恶,那么数组索引和C ++指针也是如此。他们不是。他们很容易用脚砸自己,但是不应该不惜一切代价避免它们。

对于此答案的其余部分,我将使“零是邪恶的”的意图适应于更加细微的“ 应负责任地处理零 ”。


您的解决方案。

我同意您的意见,即使用null作为全局规则;在这种特殊情况下,我不同意您使用null的情况。

总结以下反馈:您误解了继承和多态的目的。尽管您使用null的论点具有有效性,但您对另一种方法为何不好的进一步“证明”是建立在滥用继承的基础上的,这使您的观点与null问题无关。

大多数更新自然都会有一个与之相关的玩家-毕竟,有必要知道哪个玩家在此回合​​中做出了哪个动作。但是,某些更新无法使Player有意义地与它们相关联。

如果这些更新有显着不同(它们是不同的),则您需要两种单独的更新类型。

例如,考虑消息。您有错误消息和IM聊天消息。但是,尽管它们可能包含非常相似的数据(字符串消息和发件人),但它们的行为方式却不同。它们应作为不同的类分别使用。

您现在要执行的操作是根据Player属性的空值有效地区分两种更新。这等效于根据错误代码的存在来确定IM消息和错误消息。它有效,但这不是最佳方法。

  • 现在,JS代码将简单地接收不带Player的对象作为已定义的属性,这与它为null相同,因为两种情况都需要检查该值是否存在。

您的论点依赖于多态性的误解。如果你有一个返回的方法GameUpdate的对象,一般不应该关心过区分GameUpdatePlayerGameUpdateNewlyDevelopedGameUpdate

基类需要具有完全有效的约定,即,其属性是在基类上定义的,而不是在派生类上定义的(也就是说,这些基属性的当然可以在派生类中被覆盖)。

这使您的论据毫无根据。在良好实践中,您永远不要在乎您的GameUpdate对象是否具有播放器。如果您尝试访问的Player属性GameUpdate,则说明您的操作不合逻辑。

诸如C#之类的类型化语言甚至不允许您尝试访问a的Player属性,GameUpdate因为GameUpdate根本没有该属性。Javascript的方法更为宽松(并且不需要预编译),因此它不会费心去纠正您,而是会在运行时将其炸毁。

  • 如果将另一个可为空的字段添加到GameUpdate类中,则我必须能够使用多重继承来继续进行此设计-但是MI本身是邪恶的(根据经验更丰富的程序员),更重要的是,C#不会有它,所以我不能使用它。

您不应基于可空属性的添加来创建类。您应该基于功能上独特的游戏更新类型来创建类。


一般如何避免使用null的问题?

我确实认为,详细阐述如何有意义地表达缺乏价值是很重要的。这是没有特定顺序的解决方案(或不好的方法)的集合。

1.仍然使用null。

这是最简单的方法。但是,您正在注册自己进行大量的空检查,以及(如果不这样做)对空引用异常进行故障排除。

2.使用非空无意义的值

一个简单的例子是indexOf()

"abcde".indexOf("b"); // 1
"abcde".indexOf("f"); // -1

相反null,你得到了-1。从技术上讲,这是一个有效的整数值。但是,负值是无意义的答案,因为索引应为正整数。

从逻辑上讲,这仅适用于以下情况:返回类型允许的值可能大于有意义返回的值(indexOf =正整数; int =负整数和正整数)

对于引用类型,您仍然可以应用此原理。例如,如果您有一个具有整数ID的实体,则可以返回其ID设置为-1(而不是null)的虚拟对象。
您只需要定义一个“虚拟状态”,就可以通过该状态测量对象是否实际可用。

请注意,如果您不控制字符串值,则不应依赖于预定的字符串值。当您想返回一个空对象时,您可以考虑将用户名设置为“ null”,但是当Christopher Null注册服务时会遇到问题

3.引发异常。

不就是不。逻辑流不应处理异常。

就是说,在某些情况下,您不想隐藏(nullreference)异常,而是显示适当的异常。例如:

var existingPerson = personRepository.GetById(123);

PersonNotFoundException如果没有ID为123的人,这可能引发(或类似的)错误。

显然,这仅在以下情况下才可以接受:找不到商品导致无法进行任何进一步处理。如果找不到项目,并且应用程序可以继续,则永远不要抛出异常。我不能太强调这一点。


5

空值之所以不好的原因不在于缺失值被编码为空值,而是任何参考值都可以为空值。如果您拥有C#,无论如何您可能会迷路。我看不到点ro在此处使用一些Optional,因为引用类型已经是可选的。但是对于Java,有@Notnull批注,您似乎可以在程序包级别设置它,然后对于可以错过的值,可以使用批注@Nullable。


是! 问题是毫无根据的默认值。
Deduplicator

我不同意。以避免null的样式进行编程会导致更少的引用变量为null,从而导致更少的不应为null的引用变量为null。这不是100%,但可能是90%,这值得IMO。
恢复莫妮卡

1

空值不是邪恶的,实际上在没有处理看起来像空值的东西的情况下,实际上没有办法实现任何替代方法。

空值是危险的。就像其他任何底层构造一样,如果您弄错了,下面也没什么能吸引您的。

我认为有很多反对null的有效论点:

  • 那就是,如果您已经在高级域中,那么有比使用低级模型更安全的模式。

  • 除非您需要低级系统的优点并且准备谨慎对待,否则您不应该使用低级语言。

  • ...

这些都是有效的,并且进一步不必费力编写getter和setter等,这是不遵循该建议的常见原因。真的很奇怪,很多程序员得出的结论是,空值是危险且懒惰的(不好的组合!),但是像其他许多“通用”建议一样,它们不像其措辞那样普遍。

写这种语言的好人认为他们对某人有用。如果您理解异议并且仍然认为它们适合您,那么没有人会更好。


问题是,“通用建议”通常不像人们所声称的那样被表述为“通用”。如果您找到了此类建议的原始来源,他们通常会说出经验丰富的程序员所知道的警告。发生的事情是,当其他人阅读建议时,他们往往会忽略这些警告,而只会记住“通用建议”部分,因此,当他们将这些建议与其他人联系起来时,它比发起者的意图更具有“通用性” 。
Nicol Bolas

1
@NicolBolas,您是对的,但是原始消息不是大多数人引用的建议,而是中继消息。我只想指出的是,很多程序员都被告知“ X永不干,X有害/不好/无所不包”,而且正如您所说的,它确实缺少许多警告。也许它们掉了,也许它们从未出现过,最终结果还是一样。
drjpizzle

0

Java有一个Optional带有显式ifPresent+ lambda 的类,可以安全地访问任何值。这也可以在JS中进行:

请参阅此StackOverflow问题

顺便说一句:许多纯粹主义者会发现这种用法令人怀疑,但我不这么认为。确实存在可选功能。


不能给一个参考java.lang.Optionalnull吗?
重复数据删除器

@Deduplicator否,Optional.of(null)将引发异常,Optional.ofNullable(null)并给出Optional.empty()-不存在的值。
Joop Eggen '18年

还有Optional<Type> a = null
重复数据删除器

@Deduplicator是正确的,但您应该在其中使用Optional<Optional<Type>> a = Optional.empty();;)-换句话说,在JS(和其他语言)中,此类操作将不可行。使用默认值的Scala可以。Java可能带有枚举。JS我怀疑:Optional<Type> a = Optional.empty();
Joop Eggen

因此,我们已经从一个间接的,潜在的null或空的转移到两个,再加上对插入对象的一些额外方法。那是相当大的成就。
重复数据删除器'18

0

CAR Hoare称他的“十亿美元错误”无效。但是,必须指出的是,他认为解决方案不是“在任何地方都没有空值”,而是“将非空指针作为默认值,而只有在语义上需要空值的情况下才为空”。

重复他的错误的语言中存在null的问题是,您不能说一个函数期望不可为空的数据;从根本上说,这是语言无法表达的。这迫使函数包含许多无用的多余的空值处理样板,而忘记空值检查是错误的常见来源。

为了解决根本缺乏表达能力的问题,某些社区(例如Scala社区)不使用null。在Scala中,如果要使用type的可为空的值,则采用type T的参数Option[T],而如果要使用不可为null的值,则只需采用T。如果有人将空值传递给您的代码,则您的代码将崩溃。但是,认为调用代码是错误代码。 Option不过,它可以很好地替代null,因为常见的null处理模式已提取到库函数(例如.map(f)或)中.getOrElse(someDefault)

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.