【问题标题】:Should References in Object-Oriented Programming Languages be Non-Nullable by Default? [closed]面向对象编程语言中的引用是否应该默认为不可为空? [关闭]
【发布时间】:2010-12-22 06:36:24
【问题描述】:

空指针被描述为“billion dollar mistake”。某些语言具有无法分配空值的引用类型。

我想知道在设计一种新的面向对象语言时,是否应该将默认行为用于引用以防止被分配为 null。然后可以使用特殊版本的 来覆盖此行为。例如:

MyClass notNullable = new MyClass();
notNullable = null; // Error!
// a la C#, where "T?" means "Nullable<T>"
MyClass? nullable = new MyClass();
nullable = null; // Allowed

所以我的问题是,有什么理由不用新的编程语言来做这件事吗?

编辑:

我想补充一点,a recent comment on my blog 指出不可为空的类型在数组中使用时存在特定问题。我还要感谢大家的有用见解。很有帮助,抱歉我只能选择一个答案。

【问题讨论】:

  • 切换到函数式编程语言 ;-)
  • @jldupont:请将其发布为答案。
  • 我添加了一个链接。当然,在 Java、C# 或 Python 中,空值问题并不像在 C 或 C++ 中那样大。然而,在这些语言中仍然有很多样板代码专门用于空检查,并且开发人员花费了大量时间来追踪空指针。当您认为可以在编译时通过添加一个额外的字符来避免它时,这有点愚蠢。
  • 一个空的指针是危险的。空对象引用不是;如果你不使用 null 来表示一个未初始化的对象引用,你就必须发明另一个哨兵值来达到同样的目的
  • @Steven A Lowe - 关键是在不必要时消除“未初始化状态”/哨兵的可能性(常见情况)。您可以随时在需要时选择使用可为空的类型。

标签: language-agnostic null language-design nullable


【解决方案1】:

我认为默认情况下不可为空的引用类型的主要障碍是编程社区的某些部分更喜欢创建-设置-使用模式:

x = new Foo()
x.Prop <- someInitValue
x.DoSomething()

到重载的构造函数:

x = new Foo(someInitValue)
x.DoSomething()

这让 API 设计者在实例变量的初始值方面陷入困境,否则这些变量可能为空。

当然,就像 'null' 本身一样,create-set-use 模式本身会创建许多无意义的对象状态并阻止有用的不变量,因此摆脱它确实是福而不是祸。但是它确实以许多人不熟悉的方式影响了一点 API 设计,所以这不是轻而易举的事情。

但总的来说,是的,如果有一场大灾难摧毁了所有现有的语言和编译器,我们只能希望当我们重建时我们不会重复这个特定的错误。可空性是例外,而不是规则!

【讨论】:

  • 不可为空的 x.Prop 应设置为有意义的默认值或空值。 API 的使用者然后可以像以前一样覆盖它,但从类型中知道他们不能将其设置为 null。
  • 我认为一个不可为空的 x.Prop 应该保证在使用前被初始化。如果您需要“未初始化”状态,那么这就是 null 的用途,也许 x.Prop 实际上应该可以为空。
【解决方案2】:

我喜欢 Ocaml 处理“可能为空”问题的方式。每当'a 类型的值可能未知/未定义/未初始化时,它就会被包装在'a Option 类型中,该类型可以是NoneSome x,其中x 是实际的不可为空的值。访问x 时,您需要使用匹配机制进行解包。这是一个增加一个可空整数并在None上返回0的函数

>>> let f = function  Some x -> x+1 | None->0 ;;
val f : int option -> int = <fun>

它是如何工作的:

>>> f Some 5 ;;
- : int = 6
>>> f None ;;
- : int = 0

匹配机制会迫使您考虑None 的情况。当您忘记它时会发生以下情况:

 >>> let f = function  Some x -> x+1 ;;
 Characters 8-31:
 let f = function  Some x -> x+1 ;;
         ^^^^^^^^^^^^^^^^^^^^^^^
 Warning P: this pattern-matching is not exhaustive.
 Here is an example of a value that is not matched:
 None
 val f : int option -> int = <fun>

(这只是一个警告,而不是错误。现在如果你将None 传递给函数,你将得到一个匹配的异常。)

变体类型+匹配是一种通用机制,它也适用于仅将列表与head :: tail 匹配(忘记空列表的情况)。

【讨论】:

  • +1 F# 也支持选项类型,但不幸的是它仍然需要考虑 .NET 框架中的 Null。
【解决方案3】:

更好的是,禁用空引用。在极少数情况下,当“nothing”是有效值时,可能存在与其对应的对象状态,但引用仍会指向该对象,而不是零值。

【讨论】:

【解决方案4】:

据我了解,Martin Odersky 在 Scala 中包含 null 的理由是为了轻松使用 Java 库(即,您的所有 api 似乎都没有,例如“Object?”):

http://www.artima.com/scalazine/articles/goals_of_scala.html

理想情况下,我认为 null 应该作为一个特性包含在语言中,但不可为 null 应该是所有类型的默认值。这将节省大量时间并防止错误。

【讨论】:

    【解决方案5】:

    语言设计中最大的“空相关错误”是索引空指针时缺少陷阱。许多编译器在尝试取消引用空指针时会陷入陷阱,如果向指针添加偏移量并尝试取消引用,则不会陷入陷阱。在 C 标准中,尝试添加偏移量是未定义的行为,并且检查指针的性能成本不会比检查取消引用更糟糕(特别是如果编译器可以意识到如果它之前检查过指针是非空的添加偏移量,之后可能不需要重新检查)。

    至于语言对不可为空的变量的支持,有一种方法来请求某些变量或声明包含初始值的字段应该自动测试任何写入以确保在尝试时立即发生异常,这可能是有用的被写入 null 给他们。如果有一种有效的惯用方式通过构造所有元素来构造数组,并且在构造完成之前不使数组对象本身可用,则数组可以包含类似的功能。请注意,如果在构造所有元素之前发生异常,则可能还应该有一种方法来指定要在所有先前构造的元素上调用的清理函数。

    最后,如果可以指定某些实例成员应该通过非虚拟调用调用,并且即使在 null 项上也应该是可调用的,这将很有帮助。与someStringVariable.IsNullOrEmpty 相比,String.IsNullOrEmpty(someStringVariable) 之类的东西很可怕。

    【讨论】:

      【解决方案6】:

      Null 只是一个问题,因为开发人员在使用它之前不会检查某些东西是否有效,但是,如果人们开始滥用新的可为 null 构造,它不会解决任何实际问题。

      重要的是在使用之前检查每个可以为空的变量,如果这意味着您必须使用注释来允许绕过检查,那么这可能是有意义的,否则编译器可能无法编译直到你检查。

      我们在编译器中加入了越来越多的逻辑,以保护开发人员免受自己的伤害,这很可怕也很可悲,因为我们知道应该做什么,但有时会跳过步骤。

      因此,您的解决方案也将受到滥用,不幸的是,我们将回到起点。

      更新:

      基于一些 cmets,这是我回答中的一个主题。我想我应该在我原来的答案中更明确:

      基本上,如果目标是限制空变量的影响,那么只要不检查变量是否为空,编译器就会抛出错误,如果你想假设它永远不会为空,那么需要一个注解跳过检查。通过这种方式,您可以让人们进行假设,但您也可以轻松找到代码中存在假设的所有位置,并且可以在代码审查中评估假设是否有效。

      这将有助于保护,同时不限制开发人员,但可以很容易地知道它在哪里被假定为不为空。

      我认为我们需要灵活性,我宁愿让编译时间更长,也不愿对运行时产生负面影响,而且我认为我的解决方案会达到预期的效果。

      【讨论】:

      • 你的第三段让我大吃一惊。您想避免自动解决常见问题,因为人类需要额外的工作来促进不“跳过步骤”的道德规范?
      • 是的,我必须同意布赖恩的观点。更多的工作投入到编译器中,以提高开发人员的工作效率。实用主义不是理想主义。
      • @Brian - 我建议我们可以强制开发人员声明某些变量可能为空,并且不会被检查,因为我们假设它不会为空,但目前为止我会说,因为这很难被滥用,如果它被滥用,很容易编写一个工具来检查所有非检查并确定它们是否有效,例如在代码审查中。跨度>
      • @Jason - 没关系,如果我们在编译器中放一些东西,我们应该很容易知道意图是什么,以便可以检查它。但是,埋下安全漏洞的方法有很多,开发者需要更加谨慎,否则我们还不如禁止动态sql查询,强制所有查询使用prepared statements。
      • @James Black:防止语言滥用几乎是不可能的。没有任何结构或工具可以拯救糟糕的程序员(除了砍掉他们的手指)。然而,代码契约是一个非常棒的主意。
      【解决方案7】:

      没有。

      uninitialized的状态由于逻辑的需要会以某种方式存在;目前外延为空。

      也许可以设计一个对象的“有效但未初始化”的概念,但这有什么显着不同? “访问未初始化的对象”的语义仍然存在。

      更好的方法是进行静态时间检查,确保您没有访问未分配给的对象(除了字符串 eval 之外,我想不出有什么可以阻止的)。

      【讨论】:

      • 我试图说明的一点是,变量或参数需要“未初始化”或“哨兵值”是例外而不是规则。当需要时,我认为没有理由不使用“null”和可为空的形式(例如“MyType?”)。
      • “静态时间检查您没有访问未分配给的对象”保证初始化,但不保证不会为 null(即它仍然可以被初始化或稍后更改为 null)。这些是单独的问题。如果你有一个永远不应该为空的方法参数怎么办?静态检查以确保值不为空 is 使用不可为空的类型。手动检查 null 和处理 NullReferenceException 都很痛苦。
      • 不,可以使用类型。他们不必是……
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-13
      • 2016-11-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多