为什么C#和Java中存在“null”?

Why is “null” present in C# and Java?

我们注意到,在我们的软件开发了许多错误,在C(或Java)导致Null参考异常。

是否有理由将"空"包括在语言中?

毕竟,如果没有"空",我就没有bug了,对吧?

换句话说,如果没有空,语言中的什么功能就不能工作?


Anders Hejlsberg,"C父亲",刚刚在他的《计算机世界》采访中谈到了这一点:

For example, in the type system we do not have separation between value and reference types and nullability of types. This may sound a little wonky or a little technical, but in C# reference types can be null, such as strings, but value types cannot be null. It sure would be nice to have had non-nullable reference types, so you could declare that ‘this string can never be null, and I want you compiler to check that I can never hit a null pointer here’.

50% of the bugs that people run into today, coding with C# in our platform, and the same is true of Java for that matter, are probably null reference exceptions. If we had had a stronger type system that would allow you to say that ‘this parameter may never be null, and you compiler please check that at every call, by doing static analysis of the code’. Then we could have stamped out classes of bugs.

CyrusNajmabadi,C团队的前软件设计工程师(现在在谷歌工作),在他的博客上讨论这个主题:(第一,第二,第三,第四)。似乎采用不可为空的类型的最大障碍是符号会干扰程序员的习惯和代码库。类似于c程序的70%的引用可能最终会成为不可空的引用。

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

1
2
3
4
static string AcceptNotNullObject(object! s)
{
    return s.ToString();
}


无效是引用类型的自然结果。如果您有一个引用,它必须引用某个对象——或者为空。如果要禁止使用空值,则必须始终确保每个变量都是用某个非空表达式初始化的,即使这样,如果在初始化阶段读取变量,也会出现问题。

你将如何建议取消无效的概念?


就像面向对象编程中的许多事情一样,它都可以追溯到algol。托尼·霍尔刚刚称之为"10亿美元的错误",如果有的话,那是轻描淡写的。

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


C中的NULL主要是来自C++的一个承载,它的指针没有指向任何内存(或者更确切地说,Adress EDCOX1(0))。在这次采访中,AndersHejlsberg说他希望在C中添加不可为空的引用类型。

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

我们可以在没有任何空值的情况下设计c,但是您必须为人们通常使用的null提出一个可接受的解决方案,例如unitialized-valuenot-founddefault-valueundefined-valueNone。如果他们真的成功了,那么在C++和Java程序员之间可能不会有太多的采纳。至少在他们发现C程序从来没有空指针异常之前。


删除空值并不能解决很多问题。对于在init上设置的大多数变量,您需要有一个默认引用。由于变量指向错误的对象,您将得到意外的行为,而不是空引用异常。至少空引用会快速失败,而不会导致意外行为。

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


空是一个非常强大的功能。如果你缺少一个值,你会怎么做?它是空的!

一种思想是永远不归零,另一种思想是永远不归零。例如,有人说您应该返回一个有效但空的对象。

相对于我来说,我更喜欢空值,它更真实地反映了它的实际情况。如果无法从持久性层中检索实体,则需要空值。我不想要一些空值。但那就是我。

它对于原语特别方便。例如,如果我有"对"或"错",但它用于安全窗体,在该窗体中可以允许、拒绝或不设置权限。好吧,我希望它不设为空。所以我可以用布尔?

我还有很多事情要做,但我会把它留在那里。


After all, if there were no"null", I would have no bug, right?

答案是否定的。问题不是C允许空值,问题是您有错误,这些错误会以nullreferenceexception显示出来。如前所述,nulls在语言中有一个用途,用于指示"空"引用类型或非值(空/无/未知)。


这个问题可以解释为"为每个引用类型(如string.empty)设置默认值还是为空?".在这一点上,我宁愿有空,因为;

  • 我不想写默认值我编写的每个类的构造函数。
  • 我不想要不必要的为此分配的内存默认值。
  • 检查是否参考是空的比较便宜比价值比较。
  • 这是高度可能有更多的错误更难发现,而不是空引用异常。这是一个有这样一个例外是好事这清楚地表明我是做错事。

空不会导致NullPointerExceptions…

程序参数导致NullPointerExceptions。

如果没有空值,我们将使用实际的任意值来确定函数或方法的返回值无效。您仍然需要检查返回的-1(或其他),移除空值不会神奇地解决懒散问题,但会使它变得模糊。


如果您得到一个"NullReferenceException",那么您可能会一直引用不再存在的对象。这不是"空"的问题,而是您的代码指向不存在的地址的问题。


"空"包含在语言中,因为我们有值类型和引用类型。这可能是一个副作用,但我认为是一个很好的副作用。它让我们对如何有效地管理内存有了很大的了解。

为什么我们有空?…

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

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

空值只意味着堆栈上的值不指向堆上的任何实际值——它是一个空指针。

希望我解释得足够好。


在某些情况下,空值是表示引用尚未初始化的好方法。这在某些情况下很重要。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
MyResource resource;
try
{
  resource = new MyResource();
  //
  // Do some work
  //
}
finally
{
  if (resource != null)
    resource.Close();
}

在大多数情况下,这是通过使用using语句来完成的。但这种模式仍被广泛使用。

对于nullreferenceexception,通过实现一个编码标准,在其中检查所有参数的有效性,通常很容易减少此类错误的原因。根据项目的性质,我发现在大多数情况下,检查暴露成员的参数就足够了。如果参数不在预期范围内,则会根据所使用的错误处理模式引发某种类型的ArgumentException,或者返回错误结果。

参数检查本身并不能消除错误,但是在测试阶段,任何出现的错误都更容易定位和纠正。

值得注意的是,anders hejlsberg提到缺乏非空执行是C 1.0规范中最大的错误之一,而现在将其包括在内是"困难的"。

如果您仍然认为静态强制的非空引用值非常重要,您可以查看规范语言。它是C的扩展,其中非空引用是语言的一部分。这样可以确保标记为非空的引用永远不能分配空引用。


空的,因为它可以在C.A/C++/Java/Ruby中被视为一个奇怪的过去(AlGOL)的怪癖,不知何故幸存至今。

您可以通过两种方式使用它:

  • 声明引用而不初始化它们(错误)。
  • 表示可选性(OK)。

正如你所猜测的,1)是导致我们在通用命令式语言中无休止麻烦的原因,早就应该被禁止;2)是真正的基本特征。

有一些语言可以避免1)而不阻止2)。

例如,ocaml就是这样一种语言。

一个简单函数,返回一个从1开始不断递增的整数:

1
2
let counter = ref 0;;
let next_counter_value () = (counter := !counter + 1; !counter);;

关于可选性:

1
2
3
4
5
6
type distributed_computation_result = NotYetAvailable | Result of float;;
let print_result r = match r with
    | Result(f) -> Printf.printf"result is %f
"
f
    | NotYetAvailable -> Printf.printf"result not yet available
"
;;


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

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

在数据库中,空值是不包含值的值单元格的标记。对于值单元格,我通常是指表中的行和列的交集,但是值单元格的概念可以扩展到表之外。

乍一看,这两者的区别似乎微不足道。但事实并非如此。


我不能告诉你具体的问题,但听起来问题不是空值的存在。数据库中存在空值,您需要某种方法在应用程序级别解释它。我不认为这是它在.NET中存在的唯一原因,请注意。但我认为这是其中一个原因。


除了前面提到的所有原因之外,当需要一个尚未创建的对象的占位符时,还需要空值。例如。如果在一对对象之间有循环引用,则需要空值,因为不能同时实例化这两个对象。

1
2
3
4
5
6
7
8
9
10
11
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没有空值。


我很惊讶没有人谈论过他们答案的数据库。数据库有可以为空的字段,任何从数据库接收数据的语言都需要处理这些字段。这意味着有一个空值。

事实上,这一点非常重要,对于像int这样的基本类型,您可以使它们可以为空!

还要考虑函数的返回值,如果你想让一个函数除以一对数,分母可以是0,怎么办?在这种情况下,唯一的"正确"答案是空的。(我知道,在这样一个简单的例子中,一个例外可能是一个更好的选择……但在某些情况下,所有的值都是正确的,但有效的数据可能会产生一个无效或无法计算的答案。不确定在这种情况下应该使用例外情况…)


如果没有空,就无法工作的特性可以表示"缺少对象"。

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

说到列表,空可以终止链接列表。ListNode可以包含对另一个ListNode的引用,该引用可以为空。其他动态集结构如树也可以这样说。空允许您有一个普通的二叉树,其叶节点由空的子引用标记。

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

与空引用相关联的痛苦,例如由于错误和引起异常而意外产生的空引用,部分是静态类型系统的结果,该系统将空值引入每种类型:有一个空字符串、空整数、空小部件等。

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

所以这些语言提供了一个干净的,或多或少是纯母系的"空"模型,然后静态的语言把它变成了一个弗兰肯斯坦的怪物。


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


我提议:

  • 禁止零点
  • 扩展布尔值:true、false和fileNotFound

  • 通常-nullreferenceexception意味着某些方法不喜欢它被传递的内容,并返回了一个空引用,随后使用该引用时没有在使用前检查该引用。

    该方法可以有一些更详细的异常,而不是返回空值,这符合快速失败的思维模式。

    或者该方法可能会返回空值以方便您使用,这样您就可以编写if而不是尝试避免异常的"开销"。


    • 明确的无效,尽管这很少必要。也许人们可以把它看作防御编程的一种形式。
    • 在将字段从一个数据源(字节流、数据库表)映射到一个对象时,使用它(或不可为空的(T)结构)作为标志来指示缺少的字段。为每个可能为空的字段制作一个布尔标记会变得非常困难,并且当字段范围内的所有值都有效时,可能无法使用像-1或0这样的sentinel值。当有许多字段时,这特别方便。

    不管这些是使用或滥用的情况都是主观的,但我有时会使用它们。


    如果框架允许创建某种类型的数组,而不指定应该对新项执行什么操作,则该类型必须具有一些默认值。对于实现可变引用语义(*)的类型,在一般情况下没有合理的默认值。我认为.NET框架的一个弱点是无法指定非虚拟函数调用应禁止任何空检查。这将允许字符串等不可变类型作为值类型,方法是为长度等属性返回合理的值。

    (*)请注意,在vb.net和c中,可变引用语义可以由类或结构类型实现;结构类型将通过充当包含不可变引用的类对象的包装实例的代理来实现可变引用语义。

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

    然而,目前还不清楚在这个框架中应该建立多少支持。框架本身能够识别可变值类型、可变引用类型和不可变类型之间的区别,这将允许那些本身包含对外部类中可变和不可变类型的混合引用的类,从而有效地避免对非常不可变的对象进行不必要的复制。


    抱歉,迟了四年才回答,我很惊讶到目前为止,所有的答案都没有以这种方式回答最初的问题:

    C语言和Java语言,如C语言和其他语言,都有EDCOX1×0,这样程序员就可以用指针高效地编写快速、优化的代码。

    • 低层视图

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

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

    • 高级视图。

    从语义上讲,null对于编程语言来说是不必要的。例如,在诸如haskell或ml系列的经典函数语言中,不存在空值,而是存在名为maybe或option的类型。它们代表了更高级的可选值概念,而不关心生成的汇编代码是什么样子的(这将是编译器的工作)。

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

    • 把他们聚在一起

    与这些非常高级的编程语言形成对比的是,对于每一个引用类型来说C语言和Java允许一个可能的null值(这是使用指针实现的类型的另一个名称)。

    这看起来可能是一件坏事,但它的好处在于程序员可以利用它在引擎盖下如何工作的知识来创建更有效的代码(即使语言有垃圾收集)。

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


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