为什么大多数“知名”命令式/ OO语言都允许未经检查的类型代表“无”值的访问?


29

我一直在阅读关于拥有null而不是(例如)的(不便)便利的信息Maybe。阅读本文之后我相信使用Maybe(或类似方法)会更好。但是,令我惊讶的是,所有“众所周知”的命令式或面向对象的编程语言仍在使用null(这允许对可以表示“无”值的类型进行未经检查的访问),并且Maybe大多数在函数式编程语言中使用。

作为示例,请看下面的C#代码:

void doSomething(string username)
{
    // Check that username is not null
    // Do something
}

这里有些难闻的气味...为什么我们要检查参数是否为null?我们不应该假设每个变量都包含对对象的引用吗?如您所见,问题在于,根据定义,几乎所有变量都可以包含空引用。如果我们可以决定哪些变量是“可为空的”而哪些则不是呢?这将节省我们调试和寻找“ NullReferenceException”时的工作量。想象一下,默认情况下,没有类型可以包含null引用。取而代之的是,您将明确声明变量只有在确实需要时才可以包含空引用。那就是Maybe背后的想法。如果您的函数在某些情况下会失败(例如,被零除),则可以返回Maybe<int>,明确指出结果可能是整数,但也没有任何结果!这是选择Maybe而不是null的原因之一。如果您对更多示例感兴趣,那么我建议阅读这篇文章

事实是,尽管存在使大多数类型默认为可空的缺点,但大多数OO编程语言实际上都可以做到这一点。这就是为什么我想知道:

  • 您必须null编程语言来实现什么样的参数Maybe呢?完全有原因还是仅仅是“历史包bag”?

在回答此问题之前,请确保您了解null和Maybe之间的区别。


3
我建议阅读有关Tony Hoare的文章,尤其是他数十亿美元的错误。
奥德

2
好吧,这就是他的原因。不幸的结果是,直到今天,它一直被错误地复制到大多数语言中。还有语言在那里null或它的概念是不存在的(IIRC Haskell是一个这样的例子)。
奥德

9
历史包is不可小something。并且请记住,这些构建在其上的操作系统是用null很长一段时间使用的语言编写的。丢掉它并不容易。
奥德

3
我认为您应该阐明Maybe的概念,而不是发布链接。
JeffO 2013年

1
@ GlenH7 C#中的字符串是参考值(它们可以为null)。我知道int是一个原始值,但它对显示may的用法很有用。
aochagavia 2013年

Answers:


15

我相信这主要是历史包g。

最著名和最古老的语言null是C和C ++。但是在这里,null确实是有道理的。指针仍然是相当数字化的底层概念。以及在C和C ++程序员的思维方式下,别人怎么说,必须明确地指出指针可以是null没有意义的。

第二排是Java。考虑到Java开发人员正试图最接近C ++,因此他们可以简化从C ++到Java的过渡,他们可能不想弄乱这种语言的核心概念。另外,实现显式null将需要更多的精力,因为您必须检查初始化后是否正确设置了非空引用。

所有其他语言与Java相同。他们通常复制C ++或Java的方式,并考虑null引用类型隐式的核心概念是如何的,因此设计使用显式的语言真的很困难null


我认为“他们可能不想弄乱这种语言的核心概念”。他们已经完全删除了指针,而且删除null也不会有太大的变化。
svick

2
@svick对于Java,引用代替了指针。而且在许多情况下,C ++中的指针的使用方式与Java引用所使用的方式相同。甚至有人声称Java确实有指针(程序员
.stackexchange.com / questions / 207196 /…

我已将其标记为正确答案。我想投票赞成,但我的声誉不高。
aochagavia

1
同意。请注意,在C ++中(与Java和C不同),可以为空是例外。std::string不能是nullint&不能null。一个int*罐头,并且C ++允许未经检查的访问它,其原因有两个:1.因为C做到了; 2.因为您应该了解在C ++中使用原始指针时在做什么。
MSalters 2013年

@MSalters:如果类型没有可复制的默认值,则创建该类型的数组将需要在允许访问数组本身之前为其每个元素调用构造函数。这可能需要无用的工作(如果某些或所有元素在被读取之前将被覆盖),如果在构建较早的元素之后后一个元素的构造函数失败,则可能会带来复杂性,并且可能最终并没有真​​正完成很多工作(如果如果不阅读其他内容,则无法确定某些数组元素的适当值)。
2014年

15

其实,null是个好主意。给定一个指针,我们要指定该指针未引用有效值。因此,我们采用一个内存位置,将其声明为无效,并遵守该约定(有时会使用segfaults强制执行该约定)。现在,只要有指针,我就可以检查它是否包含Nothingptr == null)或Some(value)ptr != nullvalue = *ptr)。我想让您了解这等同于一种Maybe类型。

问题是:

  1. 在许多语言中,类型系统在这里不能帮助确保非空引用。

    这是历史包g,因为与以前的语言相比,许多主流的命令式语言或OOP语言在其类型系统中仅具有递增的进步。较小的更改的优势在于,新语言更易于学习。C#是一种主流语言,它引入了语言级工具来更好地处理null。

  2. API设计人员可能会null在失败时返回,但在成功时不会引用实际事物本身。通常,事物(没有参考)直接返回。一个指针级别的变平使其无法null用作值。

    这只是设计者的懒惰,如果不使用适当的类型系统强制进行适当的嵌套,就无济于事。某些人可能还会尝试根据性能考虑或存在可选检查来证明这一点(集合可能会返回,null或者项目本身也会提供contains方法)。

  3. 在Haskell中,该Maybe类型以monad 的形式清晰可见。这样可以更轻松地对包含的值进行转换。

    另一方面,像C这样的低级语言几乎不将数组视为单独的类型,因此我不确定我们期望什么。在具有参数化多态性的OOP语言中,运行时检查Maybe类型很容易实现。


“ C#正在远离null引用。”那是什么步骤?在更改语言方面,我还没有看到任何类似的东西。或者,您是说公共库的使用null量比过去少了吗?
svick

@svick我的措辞不好。“ C#是一种主流语言,它引入了语言级工具来更好地处理nulls ” –我正在谈论Nullable类型和??默认运算符。在存在遗留代码的情况下,它不能立即解决问题,但这迈向更美好未来的一步。
阿蒙(Amon)2013年

我同意你的看法。我会投你的答案,但我没有足够多的声誉:(然而,可为空值仅适用于原始类型所以这只是一个小小的一步。
aochagavia

1
@svick Nullable与此无关。我们正在谈论所有隐含允许空值的引用类型,而不是让程序员显式定义它。Nullable仅可用于值类型。
欣快感2013年

@Euphoric我认为您的评论只是对amon的回复,我没有提及Nullable
2013年

9

我的理解是,这null是使程序语言抽象出来的必要构造。1个 程序员需要指出的指针或寄存器值的能力not a valid value,并null成为该含义的共同术语。

为了强调null仅代表一个概念的约定,过去的实际价值null能够/可以根据编程语言和平台而有所不同。

如果您正在设计一种新语言并希望避免null使用maybe,而是使用它,那么我鼓励使用更具描述性的术语,例如not a valid valuenavv表示意图。但是,非价值的名称与您是否应允许非价值甚至存在于您的语言中是一个不同的概念。

在决定这两点中的任何一个之前,您需要定义的含义maybe对您的系统意味着什么。您可能会发现它只是null的含义的重命名,not a valid value或者您发现它的语言具有不同的语义。

同样,是否根据null访问或引用检查访问权限的决定是您语言的另一项设计决定。

为了提供一些历史信息,我们C有一个隐含的假设,即程序员了解他们在操纵内存时试图做的事情。因为它是对汇编及其之前的命令性语言的一种高级抽象,所以我敢冒险认为,保护程序员免受错误引用的思想并未引起他们的注意。

我相信某些编译器或其附加工具可以提供一种检查无效指针访问的措施。因此,其他人已经注意到了这个潜在的问题,并采取了措施来防范它。

是否应该允许它取决于您希望您的语言完成什么以及您要对语言用户施加何种责任。这也取决于您编写编译器以限制该行为类型的能力。

因此,回答您的问题:

  1. “什么样的论点……”-嗯,这取决于您要使用哪种语言。如果要模拟裸机访问,则可能要允许它。

  2. “这只是历史的包bag吗?” 也许,也许不是。 null当然对多种语言具有意义,并有助于推动这些语言的表达。历史先例可能影响了较新的语言及其允许范围,null但是挥舞双手并宣布该概念无用的历史包s有点过分。


1 请参见Wikipedia上的这篇文章,尽管应该将Hoare归功于空值和面向对象的语言。我相信命令式语言与Algol沿着不同的家谱发展。


关键是,例如C#或Java中的大多数变量都可以分配一个空引用。似乎仅将空引用分配给明确指示“也许”不存在引用的对象,这会更好。因此,我的问题是关于“概念”空值,而不是单词。
aochagavia 2013年

2
“空指针引用可能在编译期间显示为错误”。哪些编译器可以做到这一点?
svick 2013年

完全公平地说,您永远不要为对象分配空引用...对该对象的引用就不存在(引用指向无(0x000000000),这是根据定义null)。
mgw854 2013年

C99规范的引文讨论的是空字符,而不是空指针,这是两个截然不同的概念。
svick 2013年

2
@ GlenH7它对我没有帮助。该代码object o = null; o.ToString();对我来说很好,在VS2012中没有错误或警告。ReSharper确实对此有所抱怨,但这不是编译器。
svick 2013年

7

如果您查看引用的文章中的示例,大多数情况下,使用Maybe不会缩短代码。它不会消除检查的需要Nothing。唯一的区别是它提醒您通过类型系统执行此操作。

注意,我说的是“提醒”,不是强制。程序员是懒惰的。如果程序员确信一个值不可能是Nothing,他们将Maybe不检查而取消引用,就像现在取消引用空指针一样。最终结果是将空指针异常转换为“可能取消引用的空”异常。

人性的相同原理适用于其他领域,在这些领域中,编程语言试图迫使程序员做某事。例如,Java设计人员试图迫使人们处理大多数异常,这导致了很多样板文件,它们要么默默忽略要么盲目传播异常。

Maybe当通过模式匹配和多态性而不是显式检查做出很多决策时,这是很好的选择。例如,您可以创建单独的函数processData(Some<T>)processData(Nothing<T>),而您不能使用null。您会自动将错误处理移至一个单独的函数,这在函数编程中非常需要,在函数编程中,函数会被随意传递和评估,而不是始终以自上而下的方式进行调用。在OOP中,分离错误处理代码的首选方法是使用异常。


您是否认为这是一种新的面向对象语言的理想功能?
aochagavia 2013年

如果您想获得多态优势,则可以自己实现。您需要语言支持的唯一一件事就是不可空性。我很乐意看到一种类似于的方式为您自己指定该方式const,但是将其设为可选。某些较低级别的代码(例如链表)对于使用不可为空的对象实现会非常烦人。
Karl Bielefeldt

2
不过,您不必检查Maybe类型。Maybe类型是monad,因此它应该具有功能map :: Maybe a -> (a -> b)bind :: Maybe a -> (a -> Maybe b)在其上定义,因此您可以继续使用大部分if语句进行重铸,并进一步线程化进一步的计算。并getValueOrDefault :: Maybe a -> (() -> a) -> a允许您处理可为空的情况。它比Maybe a显式模式匹配要优雅得多。
DetriusXii 2014年

1

Maybe是思考问题的一种非常实用的方法-存在事物,它可能具有也可能没有定义的值。但是,在面向对象的意义上,我们用对象代替了对事物的想法(无论它是否具有值)。显然,一个对象具有一个值。如果不是,我们说对象是null,但是我们真正的意思是根本没有任何对象。我们对对象的引用没有任何意义。转换Maybe为OO概念并没有什么新奇的-实际上,它只会使代码更加混乱。您仍然必须为null引用值Maybe<T>。即使现在将它们称为“也许检查”,您仍然必须执行空检查(实际上,您必须执行更多的空检查,使代码混乱)。当然,您会写出作者所说的更健壮的代码,但是我认为这是唯一的情况,因为您已经使该语言变得更加抽象和晦涩难懂,这需要在大多数情况下不需要进行一定程度的工作。我愿意偶尔执行一次NullReferenceException,而不是每次访问新变量时都要处理意大利面条式代码进行Maybe检查。


2
我认为这样做可以减少空检查,因为您只需要检查是否看到Maybe <T>即可,而不必担心其余类型。
aochagavia 2013年

1
@svick Maybe<T>必须允许null作为值,因为该值可能不存在。如果我有Maybe<MyClass>,但它没有值,则值字段必须包含空引用。毫无疑问,它是可验证的安全性。
mgw854 2013年

1
@ mgw854当然可以。在OO语言中,它Maybe可以是一个抽象类,有两个从其继承的类:(Some确实有一个值字段)和None(没有该字段)。那样,价值永远不会null
svick 2013年

6
缺省情况下,使用“不可为空”和Maybe <T>可以确定某些变量始终包含对象。也就是说,所有不是Maybe <T>的变量
aochagavia 2013年

3
@ mgw854这项更改的目的是提高开发人员的表达能力。现在,开发人员始终必须假设引用或指针可以为null,并且需要进行检查以确保存在可用值。通过实施此更改,您可以使开发人员有能力说他确实需要一个有效值,并进行编译器检查以确保传递了有效值。但是仍然给开发人员选择权,让他们选择加入并且没有价值传递。
欣快感2013年

1

的概念null很容易追溯到C,但这不是问题所在。

我每天选择的语言是C#,我会保持null一种差异。C#有两种类型,引用。值永远不会是null,但是有时候我希望能够表达出没有任何值是完美的。为此,C#使用Nullable类型,因此int将值和int?可为空的值作为类型。我认为引用类型也应该如此。

另请参见: 空引用可能不是一个错误

空引用很有用,有时是必不可少的(考虑一下在C ++中是否可以返回字符串有多少麻烦)。错误的真正原因不是空指针的存在,而是类型系统如何处理它们。不幸的是,大多数语言(C ++,Java,C#)都无法正确处理它们。


0

我认为这是因为函数式编程非常关心类型,尤其是与面向对象的程序设计(或至少在最初使用的类型)组合的其他类型(元组,作为第一类类型的函数,monad等)组合在一起的类型。

我认为您在谈论的现代编程语言版本(C ++,C#,Java)都是基于没有任何形式的通用编程语言(C,C#1.0,​​Java 1)的。否则,您仍然可以在语言中将可为空和不可为空的对象之间建立某种区别(例如C ++引用,该引用不能是null,但也受到限制),但是它的自然性要差得多。


我认为在函数式编程的情况下,就是FP没有引用或指针类型的情况。在FP中,一切都是有价值的。而且,如果您使用的是指针类型,则很容易说“无物指针”。
欣快感2013年

0

我认为,根本原因是使程序“安全”以防止数据损坏所需的空检查相对较少。如果程序试图使用数组元素或其他存储位置的内容,而该内容应该已经使用有效的引用编写但不是,则最好的结果是抛出异常。理想情况下,异常将准确指示问题发生的位置,但是重要的是,在将空引用存储到可能导致数据损坏的位置之前引发了某种异常。除非方法存储对象而不首先尝试以某种方式使用它,否则使用对象的尝试本身将构成某种“空检查”。

如果要确保在不应该出现的空引用之外会引起特定的异常NullReferenceException,则通常有必要在整个地方都包含空检查。另一方面,仅确保在空引用会导致“损坏”超出已经执行的操作之前发生一些异常通常将需要相对较少的测试-通常仅在对象将存储一个对象的情况下才需要进行测试。参考不尝试使用它,无论是空引用会覆盖一个有效的,或者会引起其他代码程序状态的曲解等方面。确实存在这种情况,但不是很常见。大多数意外的空引用将很快被捕获是否检查他们


这篇文章很难阅读(文字墙)。您介意将其编辑为更好的形状吗?
蚊蚋

1
@gnat:更好吗?
supercat 2014年

0

Maybe如所写,“ ”是比null更高级的构造。用更多的词来定义它,也许是“指向事物的指针,或指向无物的指针,但是尚未为编译器提供足够的信息来确定哪个。” 这将迫使您不断地显式检查每个值,除非您构建的编译器规范足够智能以跟上编写的代码。

您可以使用具有null的语言轻松实现Maybe的实现。C ++有一个形式为boost::optional<T>。使Maybe等于null非常困难。特别是,如果我有一个Maybe<Just<T>>,则不能将其分配为null(因为这样的概念不存在),而T**使用null的语言中的a则很容易将其分配为null。这将强制使用Maybe<Maybe<T>>,这是完全有效的,但将迫使您进行更多检查以使用该对象。

某些功能语言使用Maybe可能是因为null需要未定义的行为或异常处理,而这两者都不是映射为功能语言语法的简单概念。在这种功能性情况下,也许可以更好地发挥作用,但是在过程语言中,null为王。这不是对与错的问题,而仅仅是使计算机更容易告诉您要执行的操作的问题。

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.