当可以有意义地缺少值时,新的布尔字段是否比空引用更好?


39

例如,假设我有一个类Member,它具有一个lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

缺少其lastChangePasswordTime可能是有意义的,因为某些成员可能永远不会更改其密码。

但是根据“ 如果空值是邪恶的”,当可以有意义地缺少一个值时应该使用什么?https://softwareengineering.stackexchange.com/a/12836/248528,我不应该使用null来表示有意义的缺失值。所以我尝试添加一个布尔标志:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

但是我认为它已经过时了,因为:

  1. 当isPasswordChanged为false时,lastChangePasswordTime必须为null,并且检查lastChangePasswordTime == null与检查isPasswordChanged为false几乎相同,因此我更喜欢直接检查lastChangePasswordTime == null。

  2. 在此处更改逻辑时,我可能会忘记同时更新两个字段。

注意:当用户更改密码时,我会这样记录时间:

this.lastChangePasswordTime=Date.now();

此处的附加布尔字段是否比空引用更好?


51
这个问题是特定于语言的,因为什么是一个好的解决方案,很大程度上取决于您的编程语言所提供的内容。例如,在C ++ 17或Scala中,您将使用std::optionalOption。在其他语言中,您可能必须自己构建一个适当的机制,或者您可能实际上诉诸于null类似机制,因为它更惯用了。
Christian Hackl

16
您是否有理由不希望将lastChangePasswordTime设置为密码创建时间(毕竟创建是mod)?
克里斯蒂安H

@ChristianHackl hmm,同意有不同的“完美”解决方案,但是我看不到任何(主要)语言,尽管通常使用单独的布尔值比执行null / nil检查更好。不过,我不太清楚C / C ++,因为我已经有一段时间没有活跃了。
Frank Hopkins

@FrankHopkins:一个例子是可以不初始化变量的语言,例如C或C ++。lastChangePasswordTime可能是一个未初始化的指针,将其与任何内容进行比较都将是未定义的行为。没有真正引人注目的理由是不初始化指向NULL/ 的指针nullptr尤其是在现代C ++中(根本不会使用指针),但谁知道呢?另一个例子可能是没有指针或对指针的支持不好的语言。(我想到了FORTRAN 77 ...)
Christian Hackl

我将为此使用3种情况的枚举:)
J. Doe

Answers:


72

我不明白为什么如果您有一个有意义的缺失值,null那么如果您故意并谨慎对待它,就不应该使用它。

如果您的目标是围绕可为空的值以防止意外引用它,则建议您将该isPasswordChanged值创建为返回空检查结果的函数或属性,例如:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

我认为这样做是这样的:

  • 与可能会丢失上下文的空检查相比,它提供了更好的代码可读性。
  • 无需您真正担心保持isPasswordChanged您提到的价值。

持久保存数据的方式(大概在数据库中)将负责确保保留空值。


29
+1为开幕。通常只有在null是意外结果的情况下(即,对消费者而言没有意义的情况),才不建议使用肆意使用null。如果null是有意义的,那么这不是意外的结果。
平坦的

28
我要说的要强一点,因为永远不会有两个变量保持相同的状态。它会失败。隐藏一个空值(假设该空值在实例内部有意义),从外部来说是一件好事。在这种情况下,您稍后可能会认为1970-01-01表示从未更改过密码。这样,程序其余部分的逻辑就不必关心了。
弯曲

3
您可能会忘记打电话isPasswordChanged,就像您忘记检查它是否为空一样。我什么都没得到。
Gherman

9
@Gherman如果消费者没有得到null有意义的概念,或者他需要检查存在的值,那么他会迷失方向,但是如果使用该方法,那么阅读该代码的每个人都很清楚为什么会出现这种情况是一项检查,并且有合理的可能性将该值设为null /不存在。否则,不清楚是只是因为“为什么不这样做”而只是添加空检查的开发人员,还是它是否属于概念的一部分。当然,可以找到一种方法,但是一种方法可以直接获取信息,而另一种则需要研究实现。
Frank Hopkins

2
@FrankHopkins:很好,但是如果使用并发,即使检查单个变量也可能需要锁定。
Christian Hackl

63

nulls不是邪恶的。不加思索地使用它们是。这是null正确答案的情况-没有日期。

请注意,您的解决方案会带来更多问题。将日期设置为某物的含义是什么,但是,它isPasswordChanged是错误的?您刚刚创建了一个需要特别捕获和处理的有冲突信息的案例,而一个null值具有明确定义的明确含义,并且不能与其他信息冲突。

因此,您的解决方案并不更好。null在这里允许值是正确的方法。声称自己null总是邪恶的人,无论上下文如何,都不了解为什么null存在。


17
从历史上讲,null存在是因为Tony Hoare于1965年犯了数十亿美元的错误。当然,声称null“邪恶”的人过分简化了这个话题,但是现代语言或编程风格正在逐渐远离null,取代这是一件好事。它具有专用的选项类型。
Christian Hackl

9
@ChristianHackl:出于历史兴趣,Hoare曾经说过更好的实践选择是什么(无需切换到全新的范例)?
AnoE

5
@AnoE:一个有趣的问题。不知道Hoare必须说些什么,但是如今,在现代,设计良好的编程语言中,无空编程经常成为现实。
Christian Hackl

10
@汤姆·沃特?在C#和Java之类的语言中,该DateTime字段是一个指针。整个概念null仅对指针有意义!
Todd Sewell

16
@Tom:空指针仅在允许盲目地索引和取消引用它们的语言和实现上很危险,无论会导致什么欢笑。在捕获企图索引或取消引用空指针的语言中,它通常比指向虚拟对象的指针危险小。在许多情况下,使程序异常终止可能比使程序产生无意义的数字更为可取,并且在捕获它们的系统上,空指针是正确的解决方案。
supercat

29

根据您的编程语言,可能有很好的替代方法,例如optional数据类型。在C ++(17)中,这将是std :: optional。它可以不存在,也可以具有基础数据类型的任意值。


8
Optional在Java中太; 空值Optional取决于您的意思...
Matthieu M.

1
也来到这里与Optional连接。我将其编码为具有它们的语言的类型。
Zipp

2
@FrankHopkins很遗憾,Java中没有任何东西可以阻止您进行编写Optional<Date> lastPasswordChangeTime = null;,但是我不会因此而放弃。相反,我会灌输非常牢固的团队规则,即永远不允许分配可选值null。可选功能为您提供了很多不错的功能,因此您很难轻易放弃它。您可以轻松地分配默认值(orElse或懒惰的评价:orElseGet),抛出错误(orElseThrow)在一个空安全的方式,变换值(mapflatmap)等
亚历山大

5
@FrankHopkins isPresent根据我的经验,检查几乎总是一种代码臭味,并且是一个非常可靠的信号,表明使用这些可选参数的开发人员正在“忽略这一点”。确实,它基本上没有给带来任何好处ref == null,但这也不是您应该经常使用的东西。即使团队不稳定,静态分析工具也可以制定法律,例如linter或FindBugs,它们可以捕获大多数情况。
亚历山大

5
@FrankHopkins:可能Java社区只需要进行一些范式转换,这可能需要很多年。如果你看一下Scala中,例如,它支持null,因为语言是Java的100%兼容,但没有人使用它,Option[T]是所有的地方,与像优秀的编程功能的支持mapflatMap模式匹配等,肚里到目前为止,远远超出了== null检查。没错,从理论上讲,期权类型听起来只不过是夸张的指针,但事实证明该论点是错误的。
Christian Hackl

11

正确使用null

有不同的使用方法null。最常见且语义上正确的方法是在您可能有或没有单个值时使用它。在这种情况下,值等于null或有意义,例如数据库中的记录或其他内容。

在这种情况下,您通常会像这样(以伪代码)使用它:

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

问题

这是一个很大的问题。问题在于,到您调用doSomethingUseful该值时,可能尚未检查null!如果不是,则程序可能会崩溃。而且用户甚至看不到任何好的错误消息,而留下诸如“可怕的错误:想要的值却为空!”之类的信息。(更新后:尽管信息错误可能更少,例如Segmentation fault. Core dumped.,或更糟的是,在某些情况下,没有错误和对null的错误操作)

忘记写检查null和处理null情况是一个非常常见的错误。这就是为什么发明的托尼·霍尔(Tony Hoare)null在2009年的QCon伦敦软件大会上说他在1965年犯了数十亿美元的错误的原因:https//www.infoq.com/presentations/Null-References-The-Billion-Dollar-托尼·霍尔

避免问题

一些技术和语言使检查null变得不可能以不同的方式被遗忘,从而减少了错误的数量。

例如,Haskell具有Maybemonad而不是null。假设这DatabaseRecord是用户定义的类型。在Haskell中,type的值Maybe DatabaseRecord可以等于Just <somevalue>或可以等于Nothing。然后,您可以以不同的方式使用它,但是无论如何使用,都无法在Nothing不知情的情况下对其进行某些操作。

例如,此函数称为zeroAsDefaultreturn xfor Just x0for Nothing

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl说C ++ 17和Scala有自己的方式。因此,您可能想尝试找出您的语言是否具有类似功能并加以使用。

空值仍在广泛使用

如果您没有更好的选择,那么使用null就可以了。只是继续提防它。无论如何,函数中的类型声明将对您有所帮助。

同样,这听起来可能不是很进步,但是您应该检查您的同事是否要使用它null或其他东西。他们可能很保守,出于某些原因可能不想使用新的数据结构。例如,支持语言的较早版本。这些事情应在项目的编码标准中声明,并与团队进行适当讨论。

根据您的建议

您建议使用一个单独的布尔字段。但是无论如何您都必须对其进行检查,但仍然可能忘记对其进行检查。因此,这里没有任何胜利。如果您甚至忘记了其他事情,例如每次都更新两个值,那就更糟了。如果忘记检查的问题null没有解决,那就没有意义了。避免null是困难的,您不应以使情况变得更糟的方式来避免。

如何不使用null

最后,有null不正确使用的常见方法。一种这样的方法是使用它代替空的数据结构,例如数组和字符串。空数组是与其他数组一样的适当数组!对于可以容纳多个值的数据结构来说,它可以为空(即长度为0)几乎总是重要且有用的。

从代数的角度来看,字符串的空字符串非常类似于数字的0,即身份:

a+0=a
concat(str, '')=str

空字符串通常会使字符串变成一个半形体:https : //en.wikipedia.org/wiki/Monoid 如果您不明白,那对您来说就不那么重要了。

现在,让我们来看一下为什么这个例子对编程很重要:

for (element in array) {
  doSomething(element);
}

如果我们在此处传递一个空数组,代码将正常工作。它什么也不会做。但是,如果我们在null此处传递a ,则可能会崩溃,并显示诸如“无法循​​环通过null,抱歉”之类的错误。我们可以将其包裹起来,if但是不太干净,您可能会忘记检查它

如何处理null

什么doSomethingAboutIt()应该做,特别是它是否应该抛出一个异常是另一个复杂的问题。简而言之,它取决于null给定任务的输入值是否可接受以及响应期望值。异常是意外事件。我将不进一步讨论该主题。这个答案已经很久了。


5
其实,horrible error: wanted value but got null!是不是更典型的要好得多Segmentation fault. Core dumped.......
托比斯佩特

2
@TobySpeight为True,但我更喜欢在用户的术语范围之内的内容Error: the product you want to buy is out of stock
Gherman

当然-我只是观察到它可能(而且经常)变得更糟。我完全同意会更好!您的答案几乎就是我要对这个问题说的:+1。
Toby Speight

2
@Gherman:不允许通过的null地方null是编程错误,即代码中的错误。产品缺货是常规业务逻辑的一部分,而不是错误。您不能也不应尝试将对错误的检测转换为用户界面中的正常消息。立即崩溃而不执行更多代码是首选的操作方法,特别是如果它是一个重要的应用程序(例如执行真实财务交易的应用程序)时。
Christian Hackl

1
@EricDuminil我们想要消除(或减少)犯错的可能性,而不是null完全从后台消除自身。
Gherman

4

除了先前给出的所有非常好的答案之外,我还要补充一点,就是每次您试图让字段为空时,请仔细考虑它是否可能是列表类型。可空类型等效地是一个0或1个元素的列表,通常可以将其概括为N个元素的列表。特别是在这种情况下,您可能要考虑lastChangePasswordTime成为的列表passwordChangeTimes


这是一个很好的观点。期权类型通常也是如此。选项可以看作是序列的特殊情况。
Christian Hackl

2

问问自己:哪种行为需要lastChangePasswordTime字段?

如果您需要该字段用于方法IsPasswordExpired()以确定是否应经常提示成员更改其密码,则将字段设置为最初创建该成员的时间。新成员和现有成员的IsPasswordExpired()实现都是相同的。

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

如果您有一个单独的要求,即新创建的成员必须更新其密码,则可以添加一个单独的布尔字段,称为passwordShouldBeChanged,并在创建时将其设置为true。然后,我将更改IsPasswordExpired()方法的功能以包括对该字段的检查(并将该方法重命名为ShouldChangePassword)。

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

在代码中表达您的意图。


2

首先,将空值定义为邪恶是教条,与往常一样,对于教条来说,它最适合作为指导,而不是通过/不通过测试。

其次,您可以重新定义您的情况,使该值永远不会为空。InititialPasswordChanged是一个布尔值,最初设置为false,PasswordSetTime是设置当前密码的日期和时间。

请注意,尽管这确实要花一点钱,但您现在可以始终计算距上次设置密码以来的时间。


1

如果呼叫者在使用前进行检查,则两者均为“安全/正常/正确”。问题是如果呼叫者不检查会发生什么。哪种方法更好,某种程度上存在空错误或使用无效值?

没有一个正确的答案。这取决于您所担心的。

如果崩溃确实很糟糕,但是答案不是关键性的或具有可接受的默认值,那么最好使用布尔值作为标志。如果使用错误答案比崩溃更糟糕,那么使用null更好。

对于大多数“典型”情况,快速失败并强制调用者检查是理智代码的最快方法,因此,我认为null应该是默认选择。但是,我不会对“ X是所有邪恶的福音传教士”的信仰过分相信;他们通常没有预料到所有用例。

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.