为什么“null”出现在C#和Java中

本文关键字:Java null 为什么 | 更新日期: 2023-09-27 17:47:21

我们注意到用C#(或Java)开发的软件中的许多错误会导致NullReferenceException。

是否有

理由将"null"包含在语言中?

毕竟,如果没有"空",我就不会有错误,对吧?

换句话说,没有 null,语言中的哪个功能就无法工作?

为什么“null”出现在C#和Java中

"

C#之父"Anders Hejlsberg刚刚在他的Computerworld采访中谈到了这一点:

例如,在类型系统中,我们没有值和引用类型之间的分离以及类型的可空性。这听起来可能有点不稳定或有点技术性,但在 C# 中,引用类型可以为 null,例如字符串,但值类型不能为 null。拥有不可为空的引用类型肯定会很好,所以你可以声明"这个字符串永远不能为空,我希望你的编译器检查我永远不会在这里命中空指针"。

今天人们在我们的平台中使用 C# 编码时遇到的 50% 的错误,Java 也是如此,可能是空引用异常。如果我们有一个更强大的类型系统,可以让你说"这个参数可能永远不会为空,你的编译器请在每次调用时通过对代码进行静态分析来检查它"。然后我们就可以消除错误类。

Cyrus Najmabadi,C#团队的前软件设计工程师(现在在Google工作)在他的博客上讨论了这个主题:(1st,2nd,3rd,4th)。似乎采用不可为空类型的最大障碍是符号会扰乱程序员的习惯和代码库。大约 70% 的 C# 程序引用可能最终成为不可为空的引用。

如果你真的想在 C# 中使用不可为空的引用类型,你应该尝试使用 Spec#这是一个 C# 扩展,允许使用 "!" 作为不可为空的符号。

static string AcceptNotNullObject(object! s)
{
    return s.ToString();
}

空性是引用类型的自然结果。如果你有一个引用,它必须引用某个对象 - 或者为空。如果要禁止 nullity,则始终必须确保每个变量都使用一些非 null 表达式进行初始化 - 即使这样,如果在初始化阶段读取变量,您也会遇到问题。

您如何建议删除无效性的概念?

就像面向对象编程中的许多事情一样,这一切都可以追溯到ALGOL。托尼·霍尔(Tony Hoare)刚刚称这是他的"十亿美元错误"。如果有的话,这是轻描淡写的。

这是一个非常有趣的论点,关于如何使可空性不是Java中的默认值。与 C# 的相似之处是显而易见的。

C# 中的 Null 主要是从 C++ 遗留下来的,它有指针不指向内存中的任何内容(或者更确切地说,地址0x00)。在这次采访中,Anders Hejlsberg 说他希望在 C# 中添加不可为空的引用类型。

但是,Null 在类型系统中也具有合法的位置,类似于底部类型(其中 object顶部类型)。在 lisp 中,底部类型是 NIL,在 Scala 中是 Nothing

设计

C#是可能的,没有任何空值,但你必须为人们通常对null的用法提出一个可接受的解决方案,例如unitialized-valuenot-founddefault-valueundefined-valueNone<T>。如果C++和Java程序员在这方面取得了成功,那么他们的采用率可能会减少。至少在他们看到 C# 程序从未出现任何空指针异常之前。

删除 null 解决不了太多问题。您需要为在 init 上设置的大多数变量提供默认引用。而不是空引用异常,你会得到意外的行为,因为变量指向了错误的对象。至少 null 引用会快速失败,而不是导致意外行为。

您可以查看空对象模式以找到解决此问题的部分方法

Null 是一个非常强大的功能。如果您缺少值,该怎么办?这是空的!

一种思想流派是

永不返回 null,另一种思想流派是永远。例如,有人说你应该返回一个有效但空的对象。

我更喜欢空,因为对我来说,它更真实地表明了它实际上是什么。如果我无法从持久性层中检索实体,则需要 null。我不想要一些空值。但这就是我。

它对于基元特别方便。例如,如果我有 true 或 false,但它用于安全表单,其中权限可以是"允许"、"拒绝"或"未设置"。好吧,我希望它不会设置为空。所以我可以使用布尔值吗?

我可以继续谈论的还有很多,但我会把它留在那里。

毕竟,如果没有"空",我就不会有错误,对吧?

答案是否定的。问题不在于 C# 允许 null,问题在于您有一些 bug 碰巧以 NullReferenceException 表现出来。如前所述,null 在语言中用于指示"空"引用类型或非值(空/无/未知)。

Null 不会导致 NullPointerExceptions...

编程器会导致 NullPointerExceptions。

如果没有 null,我们将回到使用实际的任意值来确定函数或方法的返回值是否无效。 你仍然需要检查返回的 -1(或其他什么),删除 null 不会神奇地解决懒惰问题,但会稍微混淆它。

这个问题可以解释为"为每个引用类型(如 String.Empty)或 null 设置默认值更好?在这个案例中,我宁愿有空值,因为;

  • 我不想写默认值我编写的每个类的构造函数。
  • 我不想要一些不必要的东西要为此类分配的内存默认值。
  • 检查是否引用为空相当便宜比价值比较。
  • 这是高度可能会有更多的错误更难检测,而不是NullReferanceExceptions.这是一个有这样的例外是件好事这清楚地表明我是做(假设)错事。
"

Null"包含在语言中,因为我们有值类型和引用类型。这可能是一个副作用,但我认为是一个很好的副作用。它给了我们如何有效地管理记忆的很多权力。

为什么我们有空?

类型存储在"堆栈"上,它们的值直接位于该内存中(即int x = 5表示该变量的内存位置包含"5")。

另一方面,引用类型在堆栈上有一个指向堆上实际值的"指针"(即字符串 x = "ello" 表示堆栈上的内存块仅包含指向堆上实际值的地址)。

null 值只是意味着我们在堆栈上的值不指向堆上的任何实际值 - 它是一个空指针。

希望我解释得足够好。

如果你得到一个"NullReferenceException",也许你会继续引用不再存在的对象。这不是"null"的问题,而是您的代码指向不存在的地址的问题。

在某些情况下,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# 的扩展,其中非空引用是语言的一部分。这可确保标记为非空的引用永远不会被分配空引用。

一个响应提到数据库中存在空值。 这是真的,但它们与 C# 中的空值有很大不同。

在 C# 中,null 是不引用任何内容的引用的标记。

在数据库中,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";;

我无法谈论您的具体问题,但听起来问题不在于 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 没有空值。

如果您创建一个对象,其中实例变量是对某个对象的引用,那么在为该变量分配任何对象引用之前,您会建议该变量具有哪个值?

我建议:

  1. 禁令空
  2. 扩展布尔值:真、假和文件未找到

通常 - NullReferenceException 意味着某些方法不喜欢它所传递的内容并返回一个 null 引用,后来在使用前没有检查引用就使用该引用。

该方法可能会引发一些更详细的异常,而不是返回 null,这符合快速失败的思维模式。

或者,为了方便起见,该方法可能会返回 null,以便您可以编写 if 而不是尝试避免异常的"开销"。

  • 显式空,尽管这很少是必需的。也许人们可以将其视为一种防御性编程形式。
  • 使用它(或非可为空的 nullable (T) 结构)作为标志,以指示将字段从一个数据源(字节流、数据库表)映射到对象时缺少字段。为每个可能的可为空字段制作布尔标志可能会变得非常笨拙,并且当该字段范围内的所有值都有效时,可能无法使用 -1 或 0 等哨兵值。当有很多字段时,这尤其方便。

这些是使用还是滥用是主观的,但我有时会使用它们。

很抱歉迟到了四年才回答,我很惊讶到目前为止没有一个答案以这种方式回答原始问题:

像 C# 和 Java 这样的语言,如 C 和它们之前的其他语言,都有null,以便程序员可以通过有效地使用指针来编写快速、优化的代码


  • 低级视图

先说一点历史。发明null的原因是为了效率。在汇编中进行低级编程时,没有抽象,寄存器中有值,并且希望充分利用它们。将零定义为无效指针值是表示对象或无对象的绝佳策略。

当你可以有一个零内存开销,真正快速实现可选值模式时,为什么要浪费一个完美的内存字的大部分可能值?这就是为什么null如此有用。

  • 高级视图。

从语义上讲,null对于编程语言来说绝不是必需的。例如,在Haskell或ML家族等经典函数式语言中,没有null,而是名为May或Option的类型。它们表示更高级的可选值概念,而不以任何方式关心生成的汇编代码的外观(这将是编译器的工作)。

这也非常有用,因为它使编译器能够捕获更多的错误,这意味着更少的 NullReferenceExceptions。

  • 将他们聚集在一起

与这些非常高级的编程语言相比,C# 和 Java 允许每个引用类型的可能值为 null(这是最终将使用指针实现的类型的另一个名称)。

这似乎是一件坏事,但它的好处是程序员可以利用它如何在后台工作的知识来创建更有效的代码(即使该语言有垃圾回收)。

这就是为什么null今天仍然存在于语言中的原因:在对可选值的一般概念的需求和对效率的始终存在的需求之间进行权衡。

没有 null 就无法工作的功能是能够表示"缺少对象"。

没有对象是一个重要的概念。在面向对象编程中,我们需要它来表示对象之间的关联,这是可选的:对象 A 可以附加到对象 B,或者 A 可能没有对象 B。如果没有 null,我们仍然可以模拟这一点:例如,我们可以使用对象列表将 B 与 A 相关联。该列表可以包含一个元素(一个 B),也可以为空。这有点不方便,并不能真正解决任何问题。假设存在 B 的代码,例如 aobj.blist.first().method() 将以类似于空引用异常的方式爆炸:(如果blist为空,blist.first()的行为是什么?

说到列表,null 允许您终止链表。ListNode可以包含对另一个 ListNode 的引用,该引用可以是 null。 其他动态集结构(如树)也是如此。Null 允许您拥有一个普通的二叉树,其叶节点通过具有 null 的子引用来标记。

列表和树可以在没有 null 的情况下构建,但它们必须是循环的,否则是无限的/惰性的。 这可能被大多数程序员视为不可接受的约束,他们更愿意在设计数据结构时有选择权。

与空引用

相关的痛苦,例如由于错误和导致异常而意外产生的空引用,部分是静态类型系统的结果,它在每个类型中引入了一个空值:有一个空字符串,空整数,空小部件,...

在动态类型语言中,可以有一个具有自己类型的 null 对象。 这样做的结果是,您具有 null 的所有代表性优势,以及更高的安全性。例如,如果您编写一个接受 String 参数的方法,则可以保证该参数将是一个字符串对象,而不是 null。String 类中没有空引用:已知为 String 的东西不能是对象 null。引用没有动态语言的类型。存储位置(如类成员或函数参数)包含一个值,该值可以是对对象的引用。该对象具有类型,而不是引用。

因此,这些语言提供了一个干净的,或多或少纯粹的"空"模型,然后静态语言将其变成弗兰肯斯坦的怪物。

如果框架允许创建某种类型的数组而不指定应如何处理新项,则该类型必须具有某个默认值。对于实现可变引用语义 (*) 的类型,在一般情况下没有合理的默认值。我认为 .NET 框架的一个弱点是无法指定非虚拟函数调用应抑制任何空检查。这将允许不可变类型(如 String)通过返回 Length 等属性的合理值来充当值类型。

(*) 请注意,在 VB.NET 和 C# 中,可变引用语义可以由类或结构类型实现;结构类型将通过充当它持有不可变引用的类对象的包装实例的代理来实现可变引用语义。

如果可以指定一个类应该具有不可为空的可变值类型语义(这意味着 - 至少 - 实例化该类型的字段将使用默认构造函数创建一个新的对象实例,并且复制该类型的字段将通过复制旧实例(递归处理任何嵌套值类型类)来创建新实例,那也会很有帮助。

然而,目前尚不清楚应该在框架中为此建立多少支持。让框架本身认识到可变值类型、可变引用类型和不可变类型之间的区别,将允许本身持有对外部类的可变和不可变类型的混合引用的类,以有效地避免对深度不可变对象进行不必要的复制。

Null 是任何 OO 语言的基本要求。任何尚未分配对象引用的对象变量都必须为 null。