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


73

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

为什么在语言中甚至包含了“ null”?

毕竟,如果没有“ null”,那么我就不会有错误,对吧?

换句话说,如果没有null,该语言的什么功能将无法正常工作?


14
-5票?哦,好了,这个站点欢迎任何级别的编程问题。
David Arno,

8
// @ Cody Brocious:社区中显然充满了不愉快的人,他们惩罚那些敢问极端新手问题的人。答案可能对您来说很明显,但对于仅刚开始编程的人来说并不明显
David Arno

2
这绝对不是一个新手问题!Null是Assembly / C的遗物,不是必需的功能!
bltxd

5
我无法开始思考这个问题是如何引起如此多的反对的。
Konrad Rudolph

8
qconlondon.com/london-2009/presentation/…-NULL的发明者Tony Hoare(作为Algol中的快速技巧)作了关于为什么这是一个坏主意的演讲。如果图灵获奖的半神可以质疑它,为什么人们说答案很明显?
皮特·柯坎

Answers:


91

“ C#父亲” Anders Hejlsberg在接受Computerworld采访时谈到了这一点:

例如,在类型系统中,值和引用类型以及类型的可空性之间没有分隔。这听起来有些怪异或有点技术性,但是在C#中,引用类型可以为null,例如字符串,但值类型不能为null。拥有非空引用类型肯定会很好,因此您可以声明“此字符串永远不能为null,并且我希望编译器检查我在此处永远不能命中空指针”。

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

Cyrus Najmabadi,C#团队的前软件设计工程师(现在Google工作)在他的博客(1st2nd3rd4th)上讨论了该主题。似乎采用非空类型的最大障碍是符号会打扰程序员的习惯和代码库。大约70%的C#程序引用可能最终以不可空值引用。

如果您真的想在C#中使用不可为空的引用类型,则应尝试使用Spec# ,它是一种C#扩展名,允许使用“!”。作为不可为空的符号。

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

1
奇怪的是,我昨晚在博客中发布了一个肮脏的骇客 ,以实现其中的某些功能:msmvps.com/blogs/jon_skeet/archive/2008/10/06/…确实不是一个不错的解决方案,尽管存在各种陷阱,但仍然很有趣( IMO!)。
乔恩·斯基特

12
实际上,所有类型都应该是隐式不可为空的,甚至是引用类型。可空类型(值类型以及引用类型)应显式标记为“?”。
Michael Damatov

5
小修正:Cyrus Najmabadi不再加入C#团队。直到今天我收到他的内部电子邮件(在Google此处),我才意识到这一点:)
乔恩·斯凯特

29

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

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


4
它不是根本。您可能需要将任何ref类型变量初始化为(与非空值)值类型一样。虽然这将是一个麻烦。
JacquesB

1
假设您需要初始化任何ref类型变量。假设这是一个静态字段,并且您调用了一个静态方法来初始化该字段-如果该方法查看另一个尚未初始化的字段怎么办?它会有什么价值?
乔恩·斯基特

2
简而言之:可能有解决方法,但是当您查看边缘情况时,它们将变得越来越迷宫。
乔恩·斯基特

10
在纯函数式编程中,只能通过函数参数和表达式评估(包括函数调用)来指定值,所有这些都需要在使用它们之前进行指定,并且除非存在闭包中,否则它们仅在定义的范围内存在。
马克·西达德

3
@Jon,在“ nullity”名称下有两个概念:未初始化和可选。这就是它被Algol遗留的错误。
bltxd

20

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

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


Kotlin是无效的。它更多是一种语言的设计选择,它迫使您确保您的裁判(或ptrs)引用了某些东西。
阿尼斯·洛尼斯

17

C#中的Null大部分是C ++的遗留物,C ++的指针没有指向内存中的任何内容(或更确切地说,adress 0x00)。在这次采访中,Anders Hejlsberg说他希望在C#中添加不可为空的引用类型。

空也有一个类型系统中的合法地位,但是,作为一个类似于在底部类型(其中object顶级型)。在lisp中,底部类型为NIL,在Scala中为Nothing

它会一直可以设计C#没有任何零点但你不得不拿出的用途,人们通常有一个可以接受的解决方案null,如unitialized-valuenot-founddefault-valueundefined-value,和None<T>。如果C ++和Java程序员确实取得了成功,那么他们的采用率可能会降低。至少直到他们看到C#程序再也没有任何空指针异常为止。


4
更不用说您真正要做的就是重命名null。
CodeRedick

1
您不必将“ null”关键字替换为另一个关键字。您可以限制“ null”的含义,然后添加其他关键字来表示不同的含义。这样可以减少空引用错误。除了“ null”之外,甚至JavaScript也具有“ undefined”。VB6有一个“空”,“没有”和“空”
马克·西达德,

不赞成投票。您对null的两种用法感到困惑:“未初始化”是没有用的(引用jalf表示“无意义的状态不应该表示”),并且与“可选性”不同。
bltxd

@bltxd:...什么?你能否透露细节
SOVA

2
我只是感到惊讶,有些人(请参阅上面的Telos)仍然使None和Null感到困惑。我大部分不同意第二段:Null在现代类型系统中没有合法位置。Null是与每种引用类型都兼容的单个值,None <T>不是,因为它是参数化的。那是巨大的区别。否决票可能有点苛刻,但张贴者似乎暗示某些人使用了“未初始化的值”,这是错误的。
bltxd 2010年

13

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

您可以查看空对象模式,以解决该问题的一部分


3
我称之为废话。引入非空引用类型可以让您选择所需的类型。OCaml和Haskell具有不可为空的引用,但它们适用于所有问题域,完全不受NullReferenceException的困扰
bltxd 2010年

基于不同的想法,OCaml和Haskell与C#和Java完全不同。仅从C#或Java中删除null不会使这些语言的行为更像Haskell这样的纯函数式语言。
Mendelt 2010年

12

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

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

对于我来说,我更喜欢null,它更真实地表明了它的真实含义。如果我无法从持久层中检索实体,则需要null。我不要空值。就是我

它对于基元特别方便。例如,如果我具有true或false,但是它用于安全性表单,则可以设置为Allow,Deny或未设置权限。好吧,我希望不要将其设置为null。所以我可以用布尔?

我还有很多可以继续的事情,但是我会留在那里。


2
在Allow / Deny / NotSet情况下,您想要的是一个枚举,而不是包含null的东西。
马克·西达德

2
False,True,FileNotFound。:-)
克里斯·杰斯特·杨

允许=真,拒绝=假,未设置=没有值,因此为NULL。那是一个简单的例子。是,否,没有数据,不确定的真假等等。列表可以不停地进行下去,它非常适合可空布尔而不是枚举。如果我有Allow / Deny / IDontKNow / WhoCares / IAmHungry / NotSet,那么我会使用enum。
mattlant

您会发现null可能是在数据中构建“无信息”元素的最坏方法。
我给予解答答案

1
在示例中,如果代码中有错误,而程序员只是忘记为Allow / Deny标志分配一个值,该怎么办?在这种情况下,null值是一个错误,并不表示“未设置”。
Outlaw程序员,

12

毕竟,如果没有“ null”,那么我就不会有错误,对吧?

答案是否定的。问题不是C#允许null,而是问题是您碰巧用NullReferenceException表现出来的错误。如前所述,空值在语言中的用途是指示“空”引用类型或非值(空/无/未知)。


3
从语言中删除null会减少某些类型的错误的可能性,就像删除指针类型会减少某些类型的错误的可能性一样。如果确实需要null,则可以使用不安全的块。值类型存在就很好,不能为空。
马克·西达德

2
pro3carp3试图说明的一点是,大多数空错误都与使用未初始化的变量有关,就好像它们准备使用一样。非空类型实际上只能解决“我不在乎我只想运行此方法的值”的情况,而通常您会在意。
古凡特

不赞成投票。您的前提是您需要空值来表示未初始化的引用。如果您确实坚持允许使用您的语言进行未初始化的引用,那么这是正确的。但是真正的问题是,这是否有意义,答案显然是否定的。这类似于要求能够表示无效的程序状态。
bltxd

bltxd:您的评论代表幼稚的观点。无效的程序状态的发生确实是由于程序员无法控制的原因,例如文件系统错误,Internet连接问题等。在这些情况下使用Null确实有意义。在表示未知,无效和空的数据库字段时,它也是无价的。
pro3carp3

请查看PostgreSQL的OCaml或Haskell绑定。他们可以管理您所说的一切,而没有普遍的null引用。简而言之,现有的实践证明您做错了。
bltxd

5

Null不会导致NullPointerExceptions ...

程序员导致NullPointerExceptions。

如果没有空值,我们将返回使用实际的任意值来确定函数或方法的返回值无效。您仍然必须检查返回的-1(或其他值),删除null不会神奇地解决惰性问题,但会对其进行模糊处理。


不赞成投票。尝试使用ML系列的任何语言并获得启发。
bltxd

刚刚注意到下降投票...虽然它有点像问题的重点,因为它专门针对C#和Java。就是说,我将检查他们,不想错过通往天堂的钥匙!
Newtopian 2014年

5

该问题可以解释为“为每个引用类型(例如String.Empty)设置默认值还是将其设置为null更好?”。在这种情况下,我宁愿使用null,因为;

  • 我不想为我编写的每个类编写默认的构造函数。
  • 我不想为这些默认值分配一些不必要的内存。
  • 检查引用是否为空比值比较便宜。
  • 代替NullReferanceExceptions,很有可能有更多难以检测的错误。出现这样的例外情况是一件好事,它可以清楚地表明我在做(假设)做错了什么。

3

语言中包含“ Null”,因为我们有值类型和引用类型。这可能是副作用,但我认为这是一个很好的副作用。它为我们有效管理内存提供了强大的力量。

为什么我们为空?...

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

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

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

希望我解释得足够好。


不赞成投票。查看ML系列语言的选项类型。简而言之,通过使引用不可为空,您不会失去任何通用性,只需使程序释放NullReferenceException。
bltxd

3

如果收到“ NullReferenceException”,则可能会继续引用不再存在的对象。这不是'null'的问题,而是代码指向不存在的地址的问题。


3

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

例如:

MyResource resource;
try
{
  resource = new MyResource();
  //
  // Do some work
  //
}
finally
{
  if (resource != null)
    resource.Close();
}

在大多数情况下,这是通过使用using实现的语句。但是该模式仍然被广泛使用。

关于您的NullReferenceException,通常可以通过实施编码标准(所有参数均检查有效性)来减少此类错误的原因。根据项目的性质,我发现在大多数情况下足以检查暴露成员的参数。如果参数不在预期范围内,则根据使用的错误处理模式,抛出某种ArgumentException或返回错误结果。

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

作为说明,安德斯·海斯伯格提到缺乏非空强制执行是C#1.0规范中的最大错误之一,现在将其包括在内是“困难的”。

如果您仍然认为静态强制的非null参考值非常重要,则可以查看spec#语言。它是C#的扩展,其中非空引用是语言的一部分。这确保了标记为非空的引用永远不会被分配空引用。


2

一个答复提到数据库中存在空值。是的,但是它们与C#中的null有很大不同。

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

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

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


+1用于指出空引用之间的差异,表示某事物为空/无效/默认/空/任何。
R. Martinho Fernandes 2009年

2

C#/ C ++ / Java / Ruby中提供的Null最好被视为某种晦涩的过去(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";;

1
1不好?总是,你说?当您想在catch块中查看变量的值时,在异常处理中如何处理?几乎需要在try外部声明它,并在内部初始化(如果初始化可能导致异常)!
CodeRedick

如果变量构造失败,则其值为null。
bltxd

至少在Java中,您必须声明引用而无需对其进行初始化:类的非原始字段(在构造函数中初始化),在创建时引发异常且需要在相应的try / catch块之外使用的对象等。
PhiLho

@ blue--并且如果没有“ null”值,将无法表示失败吗?因为没有空值,该变量将是垃圾,并且并非总是可能将非空垃圾值与良好值区分开。
Onorio Catenacci

@PhiLho:永远不会创建在构造时抛出异常的对象。当然,要在以后使用它,必须使用null,但是您也可以使用布尔值来跟踪其成功初始化。本质上,您的逻辑不需要null,但会使用它。
bltxd

1

我无法说出您的具体问题,但听起来问题不在于null的存在。数据库中存在空值,您需要某种方法在应用程序级别解决该问题。请注意,我认为这不是它存在于.net中的唯一原因。但我认为这是原因之一。


-1。在.NET,数据库NULL通常表示为DBNull.Value代替null
stakx-不再贡献

1

令我惊讶的是,没有人谈论数据库的答案。数据库具有可为空的字段,并且将从数据库接收数据的任何语言都需要处理该字段。这意味着具有空值。

实际上,这是如此重要,以至于对于int这样的基本类型,您可以将它们设置为可为空!

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


2
处理数据库内容意味着具有空。最大的区别在于,在C#中,您具有null引用。不存在异常的除法示例已经存在(IEEE 754浮点算术),并且证明“仅”正确答案不是空引用。IEEE 754使用空(NaN-非数字)。
R. Martinho Fernandes 2009年

1

除了已经提到的所有原因之外,当您需要一个尚未创建的对象的占位符时,还需要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

编辑:您也许可以提取一种没有空值的语言,但是它绝对不是面向对象的语言。例如,序言没有空值。


1
选择现有的OO语言,例如C#。现在需要在声明/构造时对所有内容进行初始化,并对需要具有null / undefined / invalid / default / empty /任何值的任何内容使用null对象模式。
R. Martinho Fernandes 2009年

0

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


1
很多选择。例如,编译器可以强制变量在显式初始化之前不能使用。或编译器可以使用默认的(无参数)构造函数,如果没有这样的构造被发现发出一个错误(类似于C ++)
尼基

0

我提议:

  1. 禁令零
  2. 扩展布尔值:True,False和FileNotFound

2
可悲的是,我认为大多数人不会开玩笑。
rjzii

0

通常-NullReferenceException意味着某些方法不喜欢它所处理的内容,并返回了空引用,该引用后来在使用前不检查引用就被使用。

该方法可能抛出了一些更详细的异常,而不是返回null,这符合快速失败的要求的思维方式。

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


0
  • 显式归零,尽管几乎没有必要。也许可以将其视为一种防御性编程形式。
  • 在将字段从一个数据源(字节流,数据库表)映射到对象时,可以使用它(对于不可为null的对象,可以为nullable(T)结构)作为标志来指示缺少的字段。为每个可能的可为空的字段创建一个布尔标志可能会变得非常笨拙,并且当该字段范围内的所有值均有效时,可能无法使用-1或0这样的前哨值。当有许多字段时,这尤其方便。

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


0

对不起,回答了四年之后,我很惊讶,到目前为止,没有答案以这种方式回答了原始问题:

C#和Java这样的语言(如C和之前的其他语言)都拥有,null因此程序员可以通过高效地使用指针来编写快速,优化的代码


  • 低层视图

首先有一点历史。null发明的原因是为了提高效率。在汇编中进行低级编程时,没有抽象,寄存器中有值,并且您想充分利用它们。将零定义为无效的指针值是一种表示对象或什么都不表示的极好的策略

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

  • 高级视图。

从语义上讲,null对于编程语言而言,它绝对不是必需的。例如,在像Haskell这样的经典功能语言或ML系列中,没有空值,而是名为Maybe或Option的类型。它们代表了可选价值的更高层次的概念而与生成的汇编代码的外观无关(这将是编译器的工作),而无需任何考虑。

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

  • 汇集他们

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

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

这就是为什么null如今的语言仍然存在的原因:在对可选价值的一般概念的需求与对效率的永恒需求之间进行权衡。


0

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

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

说到列表,使用null可以终止链接列表。阿ListNode可以包含到另一个参考ListNode,其可以为空。可以说其他动态集结构(例如树)也是如此。使用Null,您可以拥有一棵普通的二叉树,其子节点通过具有null的子引用来标记。

列表和树可以不带null来构建,但是它们必须是循环的,否则是无限/惰性的。对于大多数程序员而言,这可能被认为是无法接受的约束,他们更愿意在设计数据结构时选择。

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

在动态类型的语言中,可以有一个空对象,它具有自己的类型。这样做的结果是您具有null的所有代表性优点,并具有更高的安全性。例如,如果编写一个接受String参数的方法,那么可以保证该参数将是一个字符串对象,而不是null。String类中没有null引用:某些已知为String的对象不能为null。引用没有使用动态语言键入。诸如类成员或函数参数之类的存储位置包含一个值,该值可以作为对对象的引用。该对象具有类型,而不是引用。

因此,这些语言提供了一个纯净的,或多或少纯净的“空”模型,然后静态语言将其变成了科学怪人的怪兽。


0

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

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

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

但是,尚不清楚应在框架中为此提供多少支持。使框架本身能够识别可变值类型,可变引用类型和不可变类型之间的区别,将允许类本身持有对来自外部类的可变和不可变类型的混合的引用,从而有效地避免制作不必要的深可变对象。


-3

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


因此,尼基,您愿意向这个无知的老傻瓜解释我的陈述有什么问题吗?
David Arno,

@David:基本上空引用并不是任何OO语言的基本 要求。一种解决方案:要求在声明/构造时对所有内容进行初始化,并对任何需要具有null / undefined / invalid / default / empty / every值的事物使用null对象模式。
R. Martinho Fernandes 2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.