为什么C#和Java中存在“null”?
我们注意到我们用C#(或Java)开发的软件中有很多错误会导致NullReferenceExceptionexception。
为什么“null”甚至被包含在语言中?
毕竟,如果没有“空”,我没有错误,对吧?
换句话说,在没有null的情况下,语言中的哪些function不能工作?
Anders Hejlsberg,“C#的父亲”,在他的“计算机世界”采访中谈到了这一点:
例如,在types系统中,我们没有将值与引用types和types的可空性分开。 这可能听起来有点不可思议或者有点技术性,但是在C#中引用types可以是null,比如string,但是值types不能为null。 如果有非空的引用types,那么肯定会很好,所以你可以声明'这个string永远不能为空,我希望你编译器检查我不能在这里命中空指针。
现在人们遇到的错误中,有50%是在我们的平台上用C#编写的,Java对于这个问题也是如此,可能是空引用exception。 如果我们有一个更强大的types系统,可以让你说'这个参数可能永远不为空,而且你的编译器请在每次调用时检查一下,通过对代码进行静态分析'。 那么我们可以剔除一些bug。
C#团队(现在在Google工作)的前软件devise工程师Cyrus Najmabadi在他的博客( 第一 , 二 , 三 , 四 )上讨论了这个问题。 看来,采用非空types的最大障碍是符号会扰乱程序员的习惯和代码库。 类似于C#程序引用的70%可能会以不可空的方式结束。
如果你真的想在C#中有非空的引用types,你应该尝试使用Spec# ,这是一个允许使用“!”的C#扩展。 作为不可空的标志。
static string AcceptNotNullObject(object! s) { return s.ToString(); }
空值是参考types的自然结果。 如果你有一个引用,它必须引用一些对象 – 或者是null。 如果你禁止无效,你总是要确保每个variables都被初始化为一个非空的expression式 – 即使这样,如果在初始化阶段读取了variables,你也会遇到问题。
你会如何提出删除无效的概念?
就像面向对象编程中的许多事情一样,这一切都可以追溯到ALGOL。 托尼·霍尔(Tony Hoare)把这叫做“十亿美元的错误”。 如果有的话,这是一个轻描淡写。
这里是一个非常有趣的论文 ,如何使可空性不是Java中的默认值。 与C#的相似之处是显而易见的。
在C#中的空主要是从C + +的遗留,其中指针没有指向内存中的任何东西(或者说,地址0x00
)。 在这次采访中 ,Anders Hejlsberg说他会希望在C#中添加不可空的引用types。
在types系统中,Null也有一个合法的地方,但是,类似于底层types (其中object
是顶层types)。 在lisp中,底部types是NIL
而在Scala中则是Nothing
。
我们可以devise没有任何空值的C#,但是你必须想出一个可接受的解决scheme,用于人们通常用于null
的用法,比如unitialized-value
, not-found
, default-value
, undefined-value
和None<T>
。 如果C ++和Java程序员确实成功了,那么他们可能不太会被采用。 至less直到他们看到C#程序从来没有任何空指针exception。
删除null不会解决太多。 对于在init上设置的大多数variables,您需要具有默认参考。 而不是空引用exception,你会得到意想不到的行为,因为variables指向错误的对象。 至less空引用失败,而不是导致意外的行为。
您可以查看空对象模式来解决这个问题的一部分
空是一个非常强大的function。 如果你没有价值,你会怎么做? 这是NULL!
一个思想stream派是永远不会返回null,另一个是永远。 例如,有人说你应该返回一个有效但空的对象。
对于我来说,我更喜欢null,这是对它的真实指示。 如果我不能从我的持久层检索一个实体,我想空。 我不想要一些空的价值。 但那就是我。
原始图片特别方便。 例如,如果我有true或false,但在安全表单上使用,则权限可以是Allow,Deny或Not set。 那么,我希望没有设置为空。 所以我可以使用布尔?
还有很多我可以继续下去,但我会离开它。
毕竟,如果没有“空”,我没有错误,对吧?
答案是否定的 。 问题不在于C#允许null,问题是你有一些错误发生在NullReferenceException中。 正如已经指出的那样,空值在语言中有一个目的是表示一个“空的”引用types,或者一个非空值(空/无/未知)。
Null不会导致NullPointerExceptions …
程序员导致NullPointerExceptions。
如果没有空值,我们回到使用一个实际的任意值来确定函数或方法的返回值是无效的。 你仍然需要检查返回的-1(或其他),删除空值不会神奇地解决懒惰,但会使其变得混乱。
这个问题可能被解释为:“对每个参考types(如String.Empty)还是null都有一个默认值会更好?”。 在这方面我宁愿有空位,因为;
- 我不想为我写的每个类写一个默认的构造函数。
- 我不希望为这些默认值分配一些不必要的内存。
- 检查一个参考是否为空比价值比较便宜。
- 很可能有更多的错误难以检测,而不是NullReferanceExceptions。 有这样一个例外,这是一件好事,它清楚地表明我正在做(假设)错误的事情。
语言中包含“Null”,因为我们有值types和引用types。 这可能是一个副作用,但我认为是一个好的。 它为我们有效地pipe理内存提供了很大的权力。
为什么我们有空? …
值types存储在“堆栈”中,它们的值直接位于那块内存中(即int x = 5表示该variables的内存位置包含“5”)。
另一方面,引用types在堆栈上有一个“指针”,指向堆上的实际值(即,stringx =“ello”表示堆栈上的存储器块只包含指向堆上实际值的地址)。
空值仅仅意味着我们在堆栈上的值不会指向堆上的任何实际值 – 这是一个空指针。
希望我解释得很好。
如果你得到一个'NullReferenceException',也许你继续指向不存在的对象。 这不是'null'的问题,这是你的代码指向不存在的地址的问题。
在有些情况下, null是表示引用尚未初始化的好方法。 这在某些情况下很重要。
例如:
MyResource resource; try { resource = new MyResource(); // // Do some work // } finally { if (resource != null) resource.Close(); }
这在大多数情况下是通过使用using语句来完成的。 但是模式仍然被广泛使用。
关于你的NullReferenceException,这样的错误的原因往往很容易通过实现一个编码标准,其中所有的参数检查的有效性减less。 根据项目的性质,我发现在大多数情况下,检查暴露成员的参数就足够了。 如果参数不在预期范围内,则抛出某种types的ArgumentException ,或返回错误结果,具体取决于正在使用的error handling模式。
参数检查本身不会去除错误,但是在testing阶段发生的错误更容易定位和修正。
作为一个说明, Anders Hejlsberg提到缺乏非空执行是C#1.0规范中最大的错误之一,现在包括它是“困难的”。
如果你仍然认为一个静态强制的非空引用值是非常重要的,你可以检查spec#语言。 它是C#的一个扩展,其中非空引用是该语言的一部分。 这确保标记为非空的引用永远不能被分配空引用。
一个回应提到数据库中存在空值。 这是真的,但它们与C#中的空值非常不同。
在C#中,空值是不引用任何内容的引用的标记。
在数据库中,空值是不包含值的值单元格的标记。 通过值单元格,我通常意味着表中的行和列的交集,但是值单元的概念可以扩展到表之外。
两人之间的差异似乎微不足道,一开始就是这样。 但事实并非如此。
因为它在C#/ C ++ / Java / Ruby中可用,所以最好被看作是某种不知名的过去(Algol)的一种古怪,它以某种方式存活到今天。
你有两种使用方法:
- 声明引用而不初始化它们(坏)。
- 表示可选性(OK)。
正如你猜测的那样: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的存在。 数据库中存在空值,您需要某种方式来解释应用程序级别的数据。 我不认为这是它存在于.net中的唯一原因,请记住你。 但我认为这是其中一个原因。
我很惊讶没有人谈到数据库的答案。 数据库具有可空字段,任何将从数据库接收数据的语言都需要处理这些数据。 这意味着有一个空值。
实际上,这对于像int这样的基本types是非常重要的,你可以使它们为空。
还要考虑函数的返回值,如果你想有一个函数除了几个数字,分母可以是0,那么该怎么办呢? 在这种情况下唯一的“正确的”答案将是空的。 (我知道,在这样一个简单的例子中,例外可能是一个更好的select…但是可能会出现所有值都是正确的情况,但有效的数据可能会产生一个无效或无法计算的答案。案件…)
除了已经提到的所有原因之外,当需要一个尚未创build的对象的占位符时,需要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不具有空值。
如果用一个实例variables创build一个对象作为对某个对象的引用,那么在给它指定任何对象引用之前,该variables的值是什么?
我提议:
- 禁令空
- 扩展布尔值:True,False和FileNotFound
通常情况下 – NullReferenceException意味着某些方法不喜欢它传递的内容,并返回一个空引用,后来在使用之前没有检查引用。
该方法可能会发生一些更详细的exception,而不是返回null,这符合失败的快速思维模式。
或者该方法可能为了方便而返回null,以便您可以编写而不是尝试并避免exception的“开销”。
- 显式调零,尽pipe这是很less有必要的。 也许人们可以把它看作一种防御性的编程forms。
- 将字段(字节stream,数据库表)从一个数据源(字节stream,数据库表)映射到一个对象时,使用它(或非空的可空(T)结构)作为标志来指示缺less的字段。 对于每个可能为空的字段,都可以使用布尔标志,而且当字段有效范围内的所有值都可能不能使用像-1或0这样的标记值。 当有许多领域时,这是特别方便的。
不pipe这些是使用还是滥用,都是主观的,但是有时我会使用它们。
对不起回答迟了四年,我很惊讶,迄今没有答案这样回答了原来的问题:
像C和Java这样的语言,就像C语言和其他语言一样,它们都是null
所以程序员可以通过高效地使用指针来编写快速,优化的代码 。
- 低级视图
先有一点历史 null
发明的原因是为了效率。 在汇编中进行低级编程时,没有抽象,在寄存器中有值,并且要充分利用它们。 将零定义为不是一个有效的指针值是一个很好的策略来表示一个对象或没有 。
为什么要浪费大部分可能的完美的内存单词的值,当你可以有一个零内存开销,真正快速的实现可选的值模式? 这就是为什么null
非常有用。
- 高层次的看法。
语义上来说,编程语言绝不是必须的。 例如,在像Haskell或ML家族这样的经典函数式语言中,没有null,而是名为Maybe或Option的types。 它们代表了更高级别的可选值的概念,而不用担心生成的汇编代码会是什么样的(这将是编译器的工作)。
这也是非常有用的,因为它使编译器能够捕获更多的错误 ,这意味着更less的NullReferenceExceptions。
- 把他们放在一起
与这些非常高级的编程语言相反,C#和Java允许为每个引用types (这将是最终使用指针实现的types的另一个名称)可能的值为空值。
这可能看起来是一件坏事,但最好的方法是程序员可以利用它在底层工作的知识来创build更高效的代码(即使语言具有垃圾收集)。
这就是为什么当今语言仍然存在的原因:在一个任意价值的一般概念的需要和对效率的永远存在的需求之间的权衡。
没有null的情况下不能工作的function可以表示“缺less对象”。
没有一个对象是一个重要的概念。 在面向对象的编程中,我们需要它来表示对象之间的关联是可选的:对象A可以被附加到对象B上,或者A可能没有对象B.如果没有空值,我们仍然可以模拟它:例如我们可以使用一个对象列表来关联B和A.该列表可以包含一个元素(一个B),或者是空的。 这有些不方便,并不能解决任何问题。 假设有一个B的代码,比如aobj.blist.first().method()
将以类似于空引用exception的方式炸毁:(如果blist
为空, blist.first()
的行为是什么blist.first()
?)
说到列表,空让你终止一个链表。 ListNode
可以包含对另一个可以为null的ListNode
的引用。 关于其他dynamic集合结构(如树)也是如此。 空让你有一个普通的二叉树,其叶节点被标记为具有空引用的子引用。
列表和树可以被创build而不是null,但是它们必须是循环的,否则就是无限的/懒惰的。 这可能被大多数程序员认为是不可接受的限制,他们更愿意在devise数据结构时有所select。
与空引用相关的痛苦,比如由于错误引起的意外引起的空引用,以及导致exception的部分原因,是静态types系统的一个结果,它在每个types中引入一个空值:存在一个空String,空Integer,空Widget, …
在dynamictypes语言中,可以有一个空对象,它有自己的types。 这样做的结果是你拥有null的所有代表优点,再加上更大的安全性。 例如,如果你编写一个接受String参数的方法,那么你可以保证参数是一个string对象,而不是null。 String类中没有空引用:已知为String的东西不能是空对象。 参考文献没有dynamic语言的types。 存储位置(如类成员或函数参数)包含一个可以作为对象引用的值。 该对象有types,而不是参考。
所以这些语言提供了一个干净的,或多或less纯粹的“空”模型,然后静态的变成了一个科学怪人的怪物。
如果框架允许创build某种types的数组而不指定新项目应该做什么,那么该types必须具有一些默认值。 对于实现可变引用语义(*)的types,通常情况下没有合理的默认值。 我认为这是.NET框架的弱点,没有办法指定非虚函数调用应该抑制任何空检查。 这将允许像String这样的不可变types作为值types,通过为Length等属性返回合理的值。
(*)请注意,在VB.NET和C#中,可变引用语义可以由类或结构types实现; 一个结构types可以通过作为一个包含一个类对象的包装实例的代理来实现可变引用语义。
如果可以指定一个类应该具有不可空的可变值types语义(意味着 – 至less – 实例化该types的字段将使用默认的构造函数创build一个新的对象实例)复制该types的字段将通过复制旧的(recursion处理任何嵌套的值types的类)来创build新的实例。
然而,目前还不清楚在这个框架内应该有多less支持。 让框架本身识别可变值types,可变引用types和不可变types之间的区别,将允许自身持有对来自外部类的可变和不可变types的混合引用的类,以便有效地避免对深不可变对象进行不必要的拷贝。
空是任何面向对象语言的基本要求。 任何尚未分配对象引用的对象variables必须为空。