【问题标题】:Is returning null bad design? [closed]是否返回 null 糟糕的设计? [关闭]
【发布时间】:2010-11-19 11:24:07
【问题描述】:

我听到一些声音说从方法中检查返回的空值是糟糕的设计。我想听听一些原因。

伪代码:

variable x = object.method()
if (x is null) do something

【问题讨论】:

  • 详细说明:这些说不好的人在哪里?链接?
  • 如果该方法是您可以控制的,您可以进行单元测试以确保它永远不会返回 null,否则,我不明白为什么检查它是否为 null 是一种不好的做法,在那次通话之后;该方法返回 null 可能是一种不好的做法,但您必须保护您的代码
  • 仅仅因为没有要返回的数据而引发异常是非常烦人的。正常的程序流程不应抛出异常。
  • @David:我确实是这么说的。如果一个方法应该返回数据,但没有,这意味着也出了问题。这不是正常的程序流程:)
  • @Thorarin:“正常”程序流程是一个相当可伸缩的概念:并不是一个真正的论证基础。

标签: oop null return-value


【解决方案1】:

不返回 null 背后的基本原理是您不必检查它,因此您的代码不需要根据返回值遵循不同的路径。您可能想查看Null Object Pattern,它提供了更多相关信息。

例如,如果我要在 Java 中定义一个返回集合的方法,我通常更愿意返回一个空集合(即Collections.emptyList())而不是 null,因为这意味着我的客户端代码更干净;例如

Collection<? extends Item> c = getItems(); // Will never return null.

for (Item item : c) { // Will not enter the loop if c is empty.
  // Process item.
}

... 比:

Collection<? extends Item> c = getItems(); // Could potentially return null.

// Two possible code paths now so harder to test.
if (c != null) {
  for (Item item : c) {
    // Process item.
  }
}

【讨论】:

  • 是的,比返回 null 并希望客户记得处理这个案子要好得多。
  • 我很高兴地同意,当它被用作空容器(或字符串)的替代品时,返回 null 是疯狂的。不过这种情况并不常见。
  • +1 也适用于我的空对象模式。此外,当我实际上想要返回 null 时,我一直在命名 getCustomerOrNull() 之类的方法以使其明确。我认为当读者不想看实现时,一个方法的命名很好。
  • 注释'//现在有两个可能的代码路径'不正确;无论哪种方式,您都有两个代码路径。但是,对于第一个示例中的 null Collection 对象,“null”代码路径更短。但是你仍然有两条路径,你仍然需要测试两条路径。
  • @MSalters - 可以说,在带有null 的OO 语言中的每个 变量是一个“容器”,它包含零个或一个指向对象的指针。 (这当然是 Haskell 的 Maybe 在这些情况下明确建模的方式,这样做会更好。)
【解决方案2】:

这就是原因。

Clean Code by Robert Martin 中,他写道,当您可以返回空数组时,返回 null 是不好的设计。既然预期的结果是一个数组,为什么不呢?它将使您能够在没有任何额外条件的情况下迭代结果。如果它是一个整数,也许 0 就足够了,如果它是一个散列,空散列。等等

前提是不强制调用代码立即处理问题。调用代码可能不想关心它们。这也是为什么在许多情况下异常优于零的原因。

【讨论】:

  • 这是这个答案中提到的空对象模式:stackoverflow.com/questions/1274792/…
  • 这里的想法是,如果发生错误,您将返回一个空/空白版本的任何您正常返回的对象类型。例如,空数组或字符串适用于这些情况。当您通常返回指针时,返回“NULL”是合适的(因为 NULL 本质上是一个空指针)。从通常返回哈希的函数中返回 NULL 可能会令人困惑。有些语言比其他语言处理得更好,但总的来说,一致性是最佳实践。
  • 您不一定会返回错误,除非错误以某种方式控制流程(这不是一个好习惯)或在 API 或接口中抽象。错误可以传播到您决定捕获它的任何级别,因此您不必在调用上下文中处理它。默认情况下,它们对 null-object-pattern 友好。
  • 不幸的是,调用代码并不总是考虑空集合的情况(很像它不考虑空集合的情况)。这可能是也可能不是问题。
【解决方案3】:

返回 null 的好用处:

  • 如果 null 是一个有效的功能结果,例如:FindFirstObjectThatNeedsProcessing() 可以在未找到时返回 null,调用者应进行相应检查。

不良用途:试图替换或隐藏特殊情况,例如:

  • catch(...) 并返回 null
  • API 依赖初始化失败
  • 磁盘空间不足
  • 输入参数无效(编程错误,输入必须由调用者清理)

在这些情况下,抛出异常更合适,因为:

  • 空返回值没有提供有意义的错误信息
  • 直接调用者很可能无法处理错误情况
  • 不能保证调用者检查空结果

但是,不应使用异常来处理正常的程序操作条件,例如:

  • 无效的用户名/密码(或任何用户提供的输入)
  • 中断循环或作为非本地 goto

【讨论】:

  • “无效的用户名”似乎是引发异常的一个很好的原因。
  • 用户输入无效的登录名/密码不应被视为例外情况,这是正常程序操作的一部分。但是,身份验证系统未能响应(例如活动目录)是一种例外情况。
  • 我会说这取决于:boolean login(String,String) 看起来不错,AuthenticationContext createAuthContext(String,String) throws AuthenticationException 也不错
  • 在你的例子中,如果没有第一个对象需要处理,那么有两种合理的方法来处理这种情况。第一个是空对象模式。创建一个代表该类的不存在版本的最终子类。它具有合理的默认值,并在请求无意义的操作时抛出异常。另一种技术是选项。这是 Java8 和 Scala 中可用的。这些都显示了开发人员的意图。 null 不能显示意图,因为它有太多可能的含义。 ** 显示意图** 是代码最重要的方面。
【解决方案4】:

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

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

查看此博文了解详细说明:http://www.yegor256.com/2014/05/13/why-null-is-bad.html。更多内容请参阅我的书 Elegant Objects,第 4.1 节。

【讨论】:

  • 我非常同意。我什至不喜欢看起来是“粉饰”延迟策略的空对象模式。您的方法要么总是成功,要么可能不会。如果它应该总是成功,那么抛出。如果它可能不成功,那么设计方法让消费者知道,例如bool TryGetBlah(out blah)FirstOrNull()MatchOrFallback(T fallbackValue).
  • 我也不喜欢返回 null。您将根本原因与症状分开,使调试更加困难。要么抛出异常(快速失败),要么调用首先返回布尔值的检查器方法(例如isEmpty()),并且只有当它为真时才调用该方法。人们反对第二个,说它的性能更差 - 但正如 Unix 哲学所说,“重视人的时间而不是机器的时间”(即,与开发人员调试产生虚假错误的代码相比,性能稍慢一点所浪费的时间更少)。
【解决方案5】:

谁说这是糟糕的设计?

检查空值是一种常见的做法,甚至受到鼓励,否则您将面临到处出现 NullReferenceExceptions 的风险。与其在不需要时抛出异常,不如优雅地处理错误。

【讨论】:

  • +1。对可以当场缓解的问题抛出异常的代码让我很难过。
  • 当编码人员忘记检查空值然后得到神秘的空指针异常时,您有多难过?已检查的异常,编译器会提醒用户他们没有处理错误情况,避免出现此类编码错误。
  • @djna:我想这也很可悲,但我发现那些“忘记”检查空值的编码器是那些在处理检查异常时经常最终吞下它们的编码器.
  • 发现空catch块的存在比发现空检查的缺失更容易。
  • @Preston:我强烈反对。当它崩溃时,你会立即发现没有空检查。吞下的异常可以传播多年的神秘和微妙的错误......
【解决方案6】:

根据您目前所说的,我认为信息不足。

从 CreateWidget() 方法返回 null 似乎很糟糕。

从 FindFooInBar() 方法返回 null 似乎没问题。

【讨论】:

  • 与我的约定类似:单项查询 - Create... 返回一个新实例,或抛出Get... 返回预期的现有实例,或抛出GetOrCreate... 返回现有实例,如果不存在则返回新实例,或抛出Find... 返回一个现有实例,如果它存在,null对于集合查询 - Get... 总是返回一个集合,如果没有找到匹配项,则该集合为空。
  • NULL 不好,原因在 yegor256 和 hakuinin 的回答中给出,返回一个有效但空的对象简化了整体推理
【解决方案7】:

它的发明者says这是一个十亿美元的错误!

【讨论】:

    【解决方案8】:

    这取决于您使用的语言。如果您使用的是像 C# 这样的语言,其中指示缺少值的惯用方式是返回 null,那么如果您没有值,则返回 null 是一个不错的设计。或者,在 Haskell 等语言中,在这种情况下惯用地使用 Maybe monad,那么返回 null 将是一个糟糕的设计(如果它甚至可能的话)。

    【讨论】:

    • +1 可能会提到 monad。我发现 null 在 C# 和 Java 等语言中的定义经常被重载并在域中被赋予了一些含义。如果您在语言规范中查找 null 关键字,它仅表示“无效指针”。这可能在任何问题领域都没有任何意义。
    • 问题是你不知道它是“缺失值”null还是“未初始化”null
    【解决方案9】:

    如果您阅读了所有的答案,就会清楚这个问题的答案取决于方法的种类。

    首先,当发生异常情况(IOproblem 等)时,逻辑上会抛出异常。当某些事情确实很特别时,可能是针对不同主题的事情..

    当一个方法预期可能没有结果时,有两个类别:

    • 如果可以返回中性值,请这样做
      空枚举、字符串等都是很好的例子
    • 如果这样的中性值不存在,应该返回null
      如前所述,假设该方法可能没有结果,因此它不是异常,因此不应抛出异常。中性值是不可能的(例如:0 不是特别中性的结果,具体取决于程序)

    在我们有一个正式的方式来表示一个函数可以或不能返回 null 之前,我会尝试有一个命名约定来表示它。
    就像您对预期会失败的方法有 TrySomething() 约定一样,我经常将我的方法命名为 SafeSomething( ) 当方法返回中性结果而不是 null 时。

    我对这个名字还不是很满意,但想不出更好的名字。所以我现在正在使用它。

    【讨论】:

      【解决方案10】:

      我在这方面有一个对我很有帮助的会议

      对于单项查询:

      • Create... 返回一个新实例,或抛出
      • Get... 返回预期的现有实例,或抛出
      • GetOrCreate... 返回现有实例,如果不存在则返回新实例,或抛出
      • Find... 返回一个现有实例,如果存在,null

      对于集合查询:

      • Get... 总是返回一个集合,如果没有找到匹配的[1]项,则该集合为空

      [1] 给定一些标准,显式或隐式,在函数名中或作为参数给出。

      【讨论】:

      • 为什么 GetOne 和 FindOne 如果未找到返回不同?
      • 我描述了不同之处。当我期望它存在时,我使用Get,所以如果它不存在,那么它就是一个错误并且我抛出 - 我永远不需要检查返回值。如果我真的不知道它是否存在,我使用Find - 然后我需要检查返回值。
      【解决方案11】:

      这不一定是一个糟糕的设计 - 就像许多设计决策一样,这取决于。

      如果方法的结果在正常使用中不会有好的结果,返回null就可以了:

      object x = GetObjectFromCache();   // return null if it's not in the cache
      

      如果确实应该总是有一个非空结果,那么最好抛出一个异常:

      try {
         Controller c = GetController();    // the controller object is central to 
                                            //   the application. If we don't get one, 
                                            //   we're fubar
      
         // it's likely that it's OK to not have the try/catch since you won't 
         // be able to really handle the problem here
      }
      catch /* ... */ {
      }
      

      【讨论】:

      • 虽然您可以在此处记录诊断错误,但可能会重新引发异常。有时第一次故障数据捕获会很有帮助。
      • 或将异常包装在 RuntimeException 中,并在消息字符串中包含诊断信息。将信息放在一起。
      • 现在(2018 年)我不能再同意了,在这种情况下我们应该支持返回 Optional&lt;&gt;
      【解决方案12】:

      例外情况适用于例外所有情况。

      如果您的函数旨在查找与给定对象关联的属性,而该对象确实没有此类属性,则返回 null 可能是合适的。如果对象不存在,则抛出异常可能更合适。如果该函数旨在返回一个属性列表,并且没有要返回的属性,则返回一个空列表是有意义的 - 您将返回所有零属性。

      【讨论】:

      • 如果你尝试使用一个没有值的属性,那值得例外。 (我假设您的规范说该属性是可选的。)有一个单独的方法来检查其值是否已设置。
      【解决方案13】:

      如果这样做在某种程度上有意义,则可以返回 null:

      public String getEmployeeName(int id){ ..}
      

      在这种情况下,如果 id 与现有实体不对应,则返回 null 是有意义的,因为它可以让您区分未找到匹配项和合法错误的情况。

      人们可能认为这很糟糕,因为它可能被滥用为指示错误条件的“特殊”返回值,这不太好,有点像从函数返回错误代码但令人困惑,因为用户必须检查返回 null,而不是捕获适当的异常,例如

      public Integer getId(...){
         try{ ... ; return id; }
         catch(Exception e){ return null;}
      }
      

      【讨论】:

      • 是的。如果您遇到错误情况,请抛出异常。
      • 这是一种需要例外的情况。如果您请求的员工姓名不存在,那么显然有问题。
      • 我认为这有点字面意思,Thorarin-你当然可以用我的例子来狡辩,但我相信你可以想象一些返回匹配值的函数实例,如果没有,则返回 null匹配。如何从哈希表中的键获取值? (应该首先想到那个例子)。
      • 为不存在的键返回 null 的哈希表是错误的。自动这样做意味着您不能将 null 存储为没有不一致代码的值。
      【解决方案14】:

      对于某些情况,您希望在故障发生时立即注意到它。

      在失败案例中检查 NULL 并且不断言(针对程序员错误)或抛出(针对用户或调用者错误)可能意味着以后的崩溃更难追踪,因为没有找到原始的奇数案例。

      此外,忽略错误可能会导致安全漏洞。也许空值来自于缓冲区被覆盖或类似情况的事实。现在,您没有崩溃,这意味着漏洞利用者有机会在您的代码中执行。

      【讨论】:

        【解决方案15】:

        您认为返回 null 有哪些替代方法?

        我看到两种情况:

        • findAnItem(id)。如果找不到该项目该怎么办

        在这种情况下,我们可以:返回 Null 或抛出(已检查)异常(或者创建一个项目并返回它)

        • listItemsMatching (criteria) 如果没有找到,应该返回什么?

        在这种情况下,我们可以返回 Null、返回空列表或抛出异常。

        我相信 return null 可能不如替代方案好,因为它要求客户端记住检查 null,程序员忘记并编码

        x = find();
        x.getField();  // bang null pointer exception
        

        在 Java 中,抛出已检查异常 RecordNotFoundException 允许编译器提醒客户端处理大小写。

        我发现返回空列表的搜索非常方便 - 只需使用列表的所有内容填充显示,哦,它是空的,代码“正常工作”。

        【讨论】:

        • 抛出异常以指示在程序正常流程中可能发生的情况会导致非常丑陋的代码,例如 try { x = find() } catch (RecordNotFound e) { // 做事 }.
        • 返回空列表是一个很好的解决方案,但前提是该方法位于可以返回列表的上下文中。对于“findById”情况,您需要返回 null。我不喜欢 RecordNotFound 异常。
        • 这就是我们意见不同的症结所在。在我看来,x = find(); 之间的美并没有太大区别; if ( x = null ) { work } else { do stuff } 并尝试 catch。而且,如果有必要,我准备牺牲美感来换取代码的正确性。在我的生活中,我经常遇到没有检查返回值的代码。
        【解决方案16】:

        让他们在事后调用另一个方法来确定之前的调用是否为空。 ;-) 嘿,是good enough for JDBC

        【讨论】:

          【解决方案17】:

          嗯,这肯定取决于方法的目的......有时,更好的选择是抛出异常。这一切都取决于具体情况。

          【讨论】:

            【解决方案18】:

            有时,返回 NULL 是正确的做法,但特别是当您处理不同类型的序列(数组、列表、字符串、你有什么)时,返回零长度序列可能更好,因为它会导致代码更短,并且希望更易于理解,同时不会在 API 实现者方面花费更多的文字。

            【讨论】:

              【解决方案19】:

              这个线程背后的基本思想是防御性编程。也就是说,针对意外情况编写代码。 有一系列不同的回复:

              Adamski 建议查看 Null Object Pattern,该回复被投票支持该建议。

              Michael Valenty 还建议使用命名约定来告诉开发人员可能会发生什么。 ZeroConcept 建议正确使用 Exception,如果这是 NULL 的原因。 和其他人。

              如果我们制定了我们一直想做防御性编程的“规则”,那么我们可以看到这些建议是有效的。

              但我们有 2 个开发场景。

              由开发人员“创作”的类:作者

              另一个(可能)开发者“使用”的类:开发者

              不管一个类是否对有返回值的方法返回NULL, 开发者需要测试对象是否有效。

              如果开发人员无法做到这一点,那么该类/方法就不是确定性的。 也就是说,如果获取对象的“方法调用”没有做它“宣传”的事情(例如 getEmployee),它就违反了合同。

              作为课程的作者,我总是希望在创建方法时保持善良和防御性(和确定性)。

              因此,鉴于需要检查 NULL 或 NULL OBJECT(例如 if(employee as NullEmployee.ISVALID)) 这可能需要在员工集合中发生,那么空对象方法是更好的方法。

              但我也喜欢 Michael Valenty 的建议,即命名必须返回 null 的方法,例如 getEmployeeOrNull。

              抛出异常的作者正在取消开发人员测试对象有效性的选择, 这对对象集合非常不利,并迫使开发人员进行异常处理 分支他们的消费代码时。

              作为一个消费类的开发者,希望作者能够给我避免或者针对null情况编程的能力 他们的类/方法可能会面临。

              因此,作为一名开发人员,我会针对方法中的 NULL 进行防御性编程。 如果作者给了我一个总是返回一个对象的合约(NULL OBJECT 总是这样) 并且该对象具有用于测试对象有效性的方法/属性, 然后我将使用该方法/属性继续使用该对象,否则该对象无效 我不能使用它。

              底线是类/方法的作者必须提供机制 开发人员可以在他们的防御性编程中使用。也就是方法的意图更明确。

              开发人员应始终使用防御性编程来测试返回对象的有效性 来自另一个类/方法。

              问候

              格雷格JF

              【讨论】:

                【解决方案20】:

                对此的其他选择是: 返回一些指示成功与否(或错误类型)的值,但如果您只需要指示成功/失败的布尔值,则返回 null 表示失败,并且成功的对象不会不那么正确,然后返回 true /false 并通过参数获取对象。
                其他方法是使用异常来指示失败,但在这里 - 实际上有更多的声音,说这是一种不好的做法(因为使用异常可能很方便,但有很多缺点)。
                因此,我个人认为返回 null 表示出现问题并稍后检查(实际知道您是否成功)并没有什么不好的地方。此外,盲目地认为您的方法不会返回 NULL,然后将您的代码基于它,可能会导致其他有时很难找到的错误(尽管在大多数情况下它只会使您的系统崩溃:),正如您将参考0x00000000 迟早)。

                【讨论】:

                  【解决方案21】:

                  在复杂程序的开发过程中可能会出现意外的空函数,就像死代码一样,这种情况表明程序结构存在严重缺陷。

                  空函数或方法通常用作对象框架中可重新向量函数或可覆盖方法的默认行为。

                  Null_function @wikipedia

                  【讨论】:

                    【解决方案22】:

                    如果代码是这样的:

                    command = get_something_to_do()
                    
                    if command:  # if not Null
                        command.execute()
                    

                    如果你有一个虚拟对象,它的 execute() 方法什么都不做,并且在适当的情况下你返回它而不是 Null,你就不必检查 Null 的情况,而是可以这样做:

                    get_something_to_do().execute()
                    

                    因此,这里的问题不在于检查 NULL 与异常之间,而是在于调用者必须以不同方式(以任何方式)处理特殊的非情况。

                    【讨论】:

                      【解决方案23】:

                      对于我的用例,我需要从方法返回一个 Map,然后寻找一个特定的键。但是,如果我返回一个空 Map,那么它会导致 NullPointerException,然后返回 null 而不是一个空 Map 不会有太大不同。 但从 Java8 开始,我们可以使用Optional。以上就是引入 Optional 概念的原因。

                      【讨论】:

                        【解决方案24】:

                        生日,

                        当您无法创建新对象时返回 NULL 是许多 API 的标准做法。

                        我不知道为什么这是糟糕的设计。

                        编辑:这适用于没有例外的语言,例如多年来一直是惯例的 C。

                        HTH

                        'Avahappy,

                        【讨论】:

                        • 如果你不能创建一个对象,你真的应该抛出一个异常。如果您无法找到与某个查询匹配的对象,则返回 null 是可行的方法。
                        • @Thilo,告诉我如何在 C 中做到这一点,我会最感兴趣的。
                        • @RobWells C 不是面向对象的语言,但是这个问题被标记为“oop”
                        • @yegor256 你是对的。我错过了原来的 OOP 标签。但是,正如 BS 所说,C++ 中的类只是一个结构,其中添加了一些额外的成员函数,并在内部进行了一些花哨的内存管理。但是如果一个 API 应该返回一个结构体,那么当你不能创建结构体时返回 NUL 就是惯例。
                        猜你喜欢
                        • 1970-01-01
                        • 2013-02-15
                        • 1970-01-01
                        • 1970-01-01
                        • 2011-03-07
                        • 1970-01-01
                        • 1970-01-01
                        • 2012-09-06
                        • 2011-08-01
                        相关资源
                        最近更新 更多