空指针被描述为“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指出,当在数组中使用时,非可空类型有一个特殊的问题。我还要感谢大家的有益见解。这非常有帮助,对不起,我只能选择一个答案。
答案 0 :(得分:5)
默认情况下,我看到非可空引用类型的主要障碍是编程社区的某些部分更喜欢创建集使用模式:
x = new Foo()
x.Prop <- someInitValue
x.DoSomething()
重载构造函数:
x = new Foo(someInitValue)
x.DoSomething()
这使得API设计器在实例变量的初始值方面处于绑定状态,否则可能为null。
当然,就像'null'本身一样,create-set-use模式本身会创建许多无意义的对象状态并阻止有用的不变量,因此摆脱它实际上是一种祝福,而不是一种诅咒。然而,它确实以许多人不熟悉的方式影响了一些API设计,因此不能轻易做到。
但总的来说,是的,如果有一场大灾难摧毁了所有现有的语言和编译器,人们只能希望,当我们重建时,我们不会重复这个特殊的错误。可空性是例外,不是规则!
答案 1 :(得分:4)
我喜欢Ocaml处理'may null'问题的方法。每当'a
类型的值可能未知/未定义/单元化时,它都会包含在'a Option
类型中,该类型可以是None
或Some x
,其中{{1}是实际的非可空值。访问x
时,您需要使用匹配机制进行展开。这是一个增加可以为空的整数并在x
None
工作原理:
>>> 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
匹配列表的事情(忘记空列表情况)。
答案 2 :(得分:1)
更好的是,禁用空引用。在极少数情况下,“nothing”是有效值时,可能存在与其对应的对象状态,但引用仍将指向该对象,而不是零值。
答案 3 :(得分:1)
据我了解,Martin Odersky在Scala中包含null的理由是轻松使用Java库(即所有api似乎都没有,例如“Object?”):
http://www.artima.com/scalazine/articles/goals_of_scala.html
理想情况下,我认为null应该作为一个功能包含在语言中,但是不可为空的应该是所有类型的默认值。它可以节省大量时间并防止错误。
答案 4 :(得分:0)
语言设计中最大的“与空相关的错误”是在索引空指针时缺少陷阱。许多编译器将在尝试取消引用空陷阱时陷阱,如果向指针添加偏移量并尝试取消引用,则不会陷阱。在C标准中,尝试添加偏移量是未定义行为,检查指针的性能成本不会比检查取消引用更糟糕(特别是如果编译器可以意识到如果它检查指针是否为非null之前添加偏移量后,可能不需要重新检查。)
对于非可空变量的语言支持,有一种方法可以请求声明包含初始值的某些变量或字段应自动测试任何写入以确保在尝试时发生立即异常使它们写入null。如果通过构造所有元素并且在构造完成之前没有使数组对象本身可用来构造数组,则数组可以包括类似的特征。请注意,如果在构造所有元素之前发生异常,则可能还应该指定要在所有先前构造的元素上调用的清理函数。
最后,如果可以指定应该使用非虚拟调用来调用某些实例成员,并且即使在null项上也应该是可调用的,这将是有帮助的。与String.IsNullOrEmpty(someStringVariable)
相比,像someStringVariable.IsNullOrEmpty
这样的东西很可怕。
答案 5 :(得分:-1)
Null只是一个问题,因为开发人员在使用它之前不会检查某些内容是否有效,但是,如果人们开始滥用新的可空构造,它将无法解决任何实际问题。
重要的是只检查在使用之前检查每个可以为null的变量,如果这意味着你必须使用注释来允许绕过检查,那么这可能是有意义的,否则编译器可能会失败编译,直到你检查。
我们在编译器中加入越来越多的逻辑来保护开发人员免受他们自己的攻击,这是非常可怕和非常悲伤的,因为我们知道应该做什么,但有时会跳过步骤。
因此,您的解决方案也将受到滥用,不幸的是,我们将回到我们开始的地方。
<强>更新强>
基于一些评论,这是我的答案中的一个主题。我想我应该在原来的答案中更加明确:
基本上,如果目标是限制空变量的影响,那么每当没有检查变量为null时,编译器都会抛出错误,如果你想假设它永远不会为null,那么需要注释跳过支票。通过这种方式,您可以让人们能够承担,但您也可以轻松找到代码中具有该假设的所有位置,并且在代码审查中,如果假设有效,则可以对其进行评估。
这将有助于保护,同时不限制开发人员,但可以很容易地知道它被认为不为空。
我认为我们需要灵活性,而且我宁愿让编译花费更长时间而不是对运行时产生负面影响,我认为我的解决方案可以做到所需的。
答案 6 :(得分:-1)
否。
由于逻辑上的必要性,未初始化的状态将以某种方式存在;目前,外延是空的。
也许可以设计一个“有效但未初始化”的对象概念,但这有何显着不同? “访问未初始化对象”的语义仍然存在。
更好的方法是进行静态时间检查,不要访问未分配给的对象(除了字符串evals之外,我无法想到除了字符串之外的东西)。< / p>