我们注意到我们用 C#(或 Java)开发的软件中的许多错误会导致 NullReferenceException。
语言中甚至包含“null”是否有原因?
毕竟,如果没有“null”,我就不会有错误,对吧?
换句话说,语言中的哪些特性在没有 null 的情况下无法工作?
Anders Hejlsberg,“C# 之父”,刚刚在他的 Computerworld 采访中谈到了这一点:
例如,在类型系统中,我们没有区分值类型和引用类型以及类型的可空性。这可能听起来有点古怪或有点技术性,但在 C# 中,引用类型可以为空,例如字符串,但值类型不能为空。拥有不可为空的引用类型肯定会很好,所以你可以声明'这个字符串永远不能为空,我希望你的编译器检查我永远不会在这里碰到一个空指针'。
今天人们遇到的 50% 的错误,在我们的平台上使用 C# 编码,Java 也是如此,可能是空引用异常。如果我们有一个更强大的类型系统,可以让您说“此参数可能永远不会为空,并且您的编译器请在每次调用时通过对代码进行静态分析来检查它”。然后我们就可以消除错误类别。
Cyrus Najmabadi,前 C# 团队的软件设计工程师(现在在 Google 工作)在他的博客上讨论了这个主题:(第 1次,第2 次,第 3 次,第 4 次)。似乎采用不可为空类型的最大障碍是符号会扰乱程序员的习惯和代码库。大约 70% 的 C# 程序引用最终可能是不可为空的。
如果你真的想在 C# 中使用不可为空的引用类型,你应该尝试使用Spec# ,这是一个允许使用“!”的 C# 扩展。作为一个不可为空的符号。
static string AcceptNotNullObject(object! s)
{
return s.ToString();
}
空值是引用类型的自然结果。如果你有一个引用,它必须引用某个对象——或者为空。如果要禁止空值,则始终必须确保使用某些非空表达式初始化每个变量-即使在初始化阶段读取变量时也会遇到问题。
你会如何提议删除无效的概念?
就像面向对象编程中的许多事情一样,这一切都可以追溯到 ALGOL。托尼·霍尔(Tony Hoare)只是称其为“十亿美元的错误”。如果有的话,那是轻描淡写的。
这是一个非常有趣的论文,关于如何使可空性不是 Java 中的默认值。与 C# 的相似之处是显而易见的。
C# 中的Null主要是 C++ 的遗留物,它的指针不指向内存中的任何内容(或者更确切地说,是 adress 0x00
)。在这次采访中,Anders Hejlsberg 说他希望在 C# 中添加不可为空的引用类型。
但是,Null 在类型系统中也有合法的位置,类似于底部类型(顶部object
类型在哪里)。在 lisp 中,底部类型是,在 Scala 中是.NIL
Nothing
可以设计没有任何空值的C # null
,但是您必须为人们通常对. 如果 C++ 和 Java 程序员确实在这方面取得了成功,他们的采用率可能会降低。至少在他们看到 C# 程序从来没有任何空指针异常之前是这样。unitialized-value
not-found
default-value
undefined-value
None<T>
删除 null 不会解决太多问题。对于在 init 上设置的大多数变量,您需要有一个默认引用。由于变量指向错误的对象,您会得到意想不到的行为,而不是空引用异常。至少空引用会快速失败,而不是导致意外行为。
您可以查看空对象模式以找到解决部分问题的方法
Null 是一个非常强大的功能。如果你没有价值,你会怎么做?它是空的!
一种思想是永不返回 null,另一种思想是永远。例如,有人说您应该返回一个有效但为空的对象。
我更喜欢 null 因为它更真实地表明它实际上是什么。如果我无法从我的持久层检索实体,我想要 null。我不想要一些空值。但这就是我。
它对原语特别方便。例如,如果我有 true 或 false,但它用于安全表单,其中权限可以是 Allow、Deny 或未设置。好吧,我希望它不设置为空。所以我可以使用布尔?
还有很多我可以继续说的,但我会把它留在那里。
毕竟,如果没有“null”,我就不会有错误,对吧?
答案是否定的。问题不在于 C# 允许 null,问题在于您有错误,这些错误恰好通过 NullReferenceException 表现出来。如前所述,空值在语言中具有指示“空”引用类型或非值(空/无/未知)的目的。
Null 不会导致 NullPointerExceptions...
程序员导致 NullPointerExceptions。
如果没有空值,我们将返回使用实际的任意值来确定函数或方法的返回值是否无效。您仍然必须检查返回的 -1 (或其他),删除空值不会神奇地解决惰性,但会混淆它。
该问题可以解释为“为每个引用类型(如 String.Empty)设置默认值还是 null 更好?”。在这个方面,我更喜欢空值,因为;
“Null”包含在语言中,因为我们有值类型和引用类型。这可能是一个副作用,但我认为这是一个很好的。它给了我们很大的权力来控制我们如何有效地管理内存。
为什么我们有空?...
值类型存储在“堆栈”上,它们的值直接位于那块内存中(即 int x = 5 意味着该变量的内存位置包含“5”)。
另一方面,引用类型在堆栈上有一个“指针”,指向堆上的实际值(即字符串x =“ello”意味着堆栈上的内存块只包含一个指向堆上实际值的地址)。
空值仅仅意味着我们在堆栈上的值不指向堆上的任何实际值——它是一个空指针。
希望我解释得足够好。
在某些情况下,null是表示引用尚未初始化的好方法。这在某些情况下很重要。
例如:
MyResource resource;
try
{
resource = new MyResource();
//
// Do some work
//
}
finally
{
if (resource != null)
resource.Close();
}
在大多数情况下,这是通过使用using语句来完成的。但这种模式仍然被广泛使用。
关于您的 NullReferenceException,通过实施检查所有参数的有效性的编码标准,通常很容易减少此类错误的原因。根据项目的性质,我发现在大多数情况下检查暴露成员的参数就足够了。如果参数不在预期范围内,则会抛出某种ArgumentException,或者返回错误结果,具体取决于使用的错误处理模式。
参数检查本身并不能消除错误,但是在测试阶段发生的任何错误都更容易定位和纠正。
作为注释,Anders Hejlsberg提到了缺乏非空强制执行是 C# 1.0 规范中最大的错误之一,现在包括它是“困难的”。
如果您仍然认为静态强制的非空引用值非常重要,您可以查看spec#语言。它是 C# 的扩展,其中非空引用是语言的一部分。这确保了标记为非空的引用永远不会被分配空引用。
如果你得到一个“NullReferenceException”,也许你一直在引用不再存在的对象。这不是“null”的问题,而是您的代码指向不存在的地址的问题。
Null 在 C#/C++/Java/Ruby 中可用,最好将其视为某些晦涩的过去 (Algol) 的奇怪现象,它以某种方式幸存至今。
您可以通过两种方式使用它:
正如您所猜到的,1) 是在普通命令式语言中给我们带来无尽麻烦的原因,早就应该被禁止,2) 是真正的基本特征。
有一些语言可以避免 1)而不防止 2)。
例如OCaml就是这样一种语言。
一个简单的函数返回一个从 1 开始不断递增的整数:
let counter = ref 0;;
let next_counter_value () = (counter := !counter + 1; !counter);;
关于可选性:
type distributed_computation_result = NotYetAvailable | Result of float;;
let print_result r = match r with
| Result(f) -> Printf.printf "result is %f\n" f
| NotYetAvailable -> Printf.printf "result not yet available\n";;
一份回复提到数据库中有空值。没错,但它们与 C# 中的空值有很大不同。
在 C# 中,空值是不引用任何内容的引用的标记。
在数据库中,空值是不包含值的值单元格的标记。值单元格通常是指表格中行和列的交集,但值单元格的概念可以扩展到表格之外。
乍一看,两者之间的差异似乎微不足道。但事实并非如此。
我无法谈论您的具体问题,但听起来问题不在于 null 的存在。Null 存在于数据库中,您需要一些方法来在应用程序级别解释这一点。请注意,我认为这不是它存在于 .net 中的唯一原因。但我认为这是原因之一。
我很惊讶没有人谈论数据库作为他们的答案。数据库具有可为空的字段,任何将从数据库接收数据的语言都需要处理它。这意味着有一个空值。
事实上,这非常重要,以至于对于像 int 这样的基本类型,您可以让它们为空!
还要考虑函数的返回值,如果你想让一个函数除以几个数字并且分母可以是 0 怎么办?在这种情况下,唯一的“正确”答案将是空的。(我知道,在这样一个简单的例子中,异常可能是一个更好的选择......但在某些情况下,所有值都是正确的,但有效数据可能会产生无效或无法计算的答案。不确定应该在这种情况下使用异常案例...)
除了已经提到的所有原因之外,当您需要一个尚未创建的对象的占位符时,还需要 NULL。例如。如果您在一对对象之间有循环引用,那么您需要 null 因为您不能同时实例化两者。
class A {
B fieldb;
}
class B {
A fielda;
}
A a = new A() // a.fieldb is null
B b = new B() { fielda = a } // b.fielda isnt
a.fieldb = b // now it isnt null anymore
编辑:您可能能够提取一种没有空值的语言,但它绝对不是面向对象的语言。例如,prolog 没有空值。
如果您创建一个实例变量作为对某个对象的引用的对象,那么在您将任何对象引用分配给它之前,您建议该变量具有什么值?
我提议:
通常 - NullReferenceException 表示某些方法不喜欢它所传递的内容并返回一个空引用,后来在使用前没有检查引用就被使用了。
该方法可以抛出一些更详细的异常而不是返回 null,这符合快速失败的思维模式。
或者该方法可能会返回 null 以方便您,以便您可以编写if而不是尝试避免异常的“开销”。
这些是使用案例还是滥用案例是主观的,但我有时会使用它们。
如果框架允许创建某种类型的数组而不指定应该对新项目做什么,则该类型必须具有一些默认值。对于实现可变引用语义 (*) 的类型,在一般情况下没有合理的默认值。我认为 .NET 框架的一个弱点是无法指定非虚拟函数调用应该禁止任何空值检查。这将允许像 String 这样的不可变类型作为值类型,通过返回像 Length 这样的属性的合理值。
(*) 请注意,在 VB.NET 和 C# 中,可变引用语义可以由类或结构类型实现;结构类型将通过充当其持有不可变引用的类对象的包装实例的代理来实现可变引用语义。
如果可以指定一个类应该具有不可为空的可变值类型语义(这意味着 - 至少 - 实例化该类型的字段将使用默认构造函数创建一个新的对象实例,并且复制该类型的字段将通过复制旧实例来创建一个新实例(递归处理任何嵌套的值类型类)。
然而,目前尚不清楚框架中应该为此构建多少支持。让框架本身识别可变值类型、可变引用类型和不可变类型之间的区别,将允许类本身持有对来自外部类的可变和不可变类型的混合引用,从而有效地避免对深度不可变对象进行不必要的复制。
很抱歉回答晚了四年,我很惊讶到目前为止没有一个答案以这种方式回答了原始问题:
像C# 和 Java这样的语言,就像 C 和它们之前的其他语言一样,使null
程序员可以通过有效地使用指针来编写快速、优化的代码。
先说一点历史。之所以null
发明是为了效率。在汇编中进行低级编程时,没有抽象,您在寄存器中有值,并且您希望充分利用它们。将零定义为无效的指针值是表示对象或空的一种极好的策略。
当您可以零内存开销、非常快速地实现可选值模式时,为什么要浪费内存中一个非常好的单词的大部分可能值呢?这就是null
它如此有用的原因。
从语义上讲,null
对于编程语言来说,这绝不是必要的。例如,在 Haskell 等经典函数式语言或 ML 家族中,没有 null,而是命名为 Maybe 或 Option 的类型。它们代表了更高级别的可选值概念,而不以任何方式关心生成的汇编代码的外观(这将是编译器的工作)。
这也非常有用,因为它使编译器能够捕获更多的错误,这意味着更少的 NullReferenceExceptions。
与这些非常高级的编程语言相比,C# 和 Java 允许每个引用类型可能的 null 值(这是最终将使用指针实现的类型的另一个名称)。
这似乎是一件坏事,但它的好处是程序员可以使用它在后台如何工作的知识来创建更高效的代码(即使该语言具有垃圾收集)。
这就是为什么null
现在仍然存在于语言中的原因:在对可选值的一般概念的需求和对效率的永远存在的需求之间进行权衡。
没有 null 就无法工作的功能是能够表示“没有对象”。
物体的缺失是一个重要的概念。在面向对象编程中,我们需要它来表示对象之间的可选关联:对象 A 可以附加到对象 B,或者 A 可能没有对象 B。没有 null 我们仍然可以模拟:例如我们可以使用一个对象列表将 B 与 A 关联起来。该列表可以包含一个元素(一个 B),也可以为空。这有点不方便,并不能真正解决任何问题。假设存在 B 的代码,例如aobj.blist.first().method()
将以类似于空引用异常的方式爆炸:(如果blist
为空,则行为是blist.first()
什么?)
说到列表,null 可以让你终止一个链表。AListNode
可以包含对另一个ListNode
可以为空的引用。其他动态集合结构(例如树)也是如此。Null 允许您拥有一个普通的二叉树,其叶节点通过具有为空的子引用来标记。
列表和树可以在没有 null 的情况下构建,但它们必须是循环的,否则是无限/惰性的。这可能会被大多数程序员认为是不可接受的约束,他们更愿意在设计数据结构时有选择。
与空引用相关的痛苦,例如由于错误和导致异常而意外产生的空引用,部分是静态类型系统的结果,该系统将空值引入每种类型:有一个空字符串,空整数,空小部件, ...
在动态类型语言中,可以有一个空对象,它有自己的类型。这样做的结果是您拥有 null 的所有代表性优势,以及更高的安全性。例如,如果您编写一个接受 String 参数的方法,那么您可以保证该参数将是一个字符串对象,而不是 null。String 类中没有 null 引用:已知为 String 的东西不能是对象 null。引用在动态语言中没有类型。诸如类成员或函数参数之类的存储位置包含一个值,该值可以是对对象的引用。该对象具有类型,而不是引用。
因此,这些语言提供了一个干净的、或多或少纯数学的“null”模型,然后静态语言将其变成了某种科学怪人的怪物。
Null 是任何 OO 语言的基本要求。任何未分配对象引用的对象变量都必须为空。