关于非可空类型的辩论

时间:2009-03-13 03:05:03

标签: null d access-violation non-nullable null-pointer

我一直听到人们谈论非可空引用类型如何解决如此多的错误并使编程变得更加容易。即使null的创建者将其称为billion dollar mistakeSpec#也引入了不可为空的类型来解决此问题。

编辑: 忽略我对Spec#的评论。我误解了它是如何工作的。

编辑2: 我必须和错误的人交谈,我真的希望有人争辩: - )


所以我猜想,在少数人中,我错了,但我不明白为什么这场辩论有任何优点。我认为null是一个bug查找工具。请考虑以下事项:

class Class { ... }

void main() {
    Class c = nullptr;
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

BAM!访问冲突。有人忘了初始化c


现在考虑一下:

class Class { ... }

void main() {
    Class c = new Class(); // set to new Class() by default
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

糟糕。静默地跳过循环。追踪问题可能需要一段时间。


如果你的课是空的,那么代码无论如何都会失败。为什么不让系统告诉你(虽然有点粗鲁)而不是自己搞清楚?

8 个答案:

答案 0 :(得分:12)

有点奇怪的是,在这个帖子中标记为“回答”的响应实际上首先突出了null的问题,即:

  

我也发现我的大多数是NULL   指针错误围绕着   功能从忘记检查   返回string.h的函数,   其中NULL用作指标。

如果编译器可以在编译时捕获这些类型的错误而不是运行时,那会不会很好?

如果您使用类似ML的语言(某种程度上是SML,OCaml,SML和F#)或Haskell,则引用类型是不可为空的。相反,您通过将其包装为选项类型来表示“null”值。这样,如果函数的返回类型可以返回null作为合法值,则实际更改它的返回类型。所以,假设我想将用户拉出数据库:

let findUser username =
    let recordset = executeQuery("select * from users where username = @username")
    if recordset.getCount() > 0 then
        let user = initUser(recordset)
        Some(user)
    else
        None

查找用户的类型为val findUser : string -> user option,因此函数的返回类型实际上告诉您它可以返回空值。要使用代码,您需要处理Some和None情况:

match findUser "Juliet Thunderwitch" with
| Some x -> print_endline "Juliet exists in database"
| None -> print_endline "Juliet not in database"

如果你不处理这两种情况,代码甚至都不会编译。因此类型系统保证您永远不会得到空引用异常,并且它保证您始终处理空值。如果函数返回user,则保证它是对象的实际实例。迷死。

现在我们在OP的示例代码中看到了问题:

class Class { ... }

void main() {
    Class c = new Class(); // set to new Class() by default
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

初始化和未初始化的对象具有相同的数据类型,您无法区分它们之间的区别。偶尔,null object pattern可能很有用,但上面的代码表明编译器无法确定您是否正确使用了类型。

答案 1 :(得分:6)

我不明白你的例子。如果你的“= new Class()”只是一个占位符而不是null,那么它(在我看来)显然是一个bug。如果不是,那么真正的错误是“......”没有正确设置其内容,这在两种情况下完全相同。

一个异常,向您显示您忘记初始化c将告诉您它何时未初始化,而不是它应该初始化的位置。类似地,错过的循环将(隐式)告诉您需要具有非零.count的位置,而不是应该执行的操作或位置。我没有看到任何一个对程序员来说更容易。

我认为“无空”的意思是简单地进行文本查找和替换,并将它们全部转换为空实例。那显然没用。关键是要构造你的代码,这样你的变量永远不会处于无用/不正确的值,其中NULL只是最常见的。

答案 2 :(得分:2)

我承认我并没有真正阅读过很多关于Spec#的内容,但我明白NonNullable本质上是一个属性,你放在一个参数上,不一定是变量声明;把你的例子改为:

class Class { ... }

void DoSomething(Class c)
{
    if (c == null) return;
    for(int i = 0; i < c.count; ++i) { ... }
}

void main() {
    Class c = nullptr;
    // ... ... ... code ...
    DoSomething(c);
}

使用Spec#,你标记doSomething说“参数c不能为空”。这似乎是一个很好的功能,因为这意味着我不需要DoSomething()方法中的第一行(这是一个容易忘记的行,对于DoSomething()的上下文完全没有意义。) / p>

答案 3 :(得分:2)

非null类型的想法是让编译器而不是客户端发现错误。假设您在语言中添加了两个类型说明符@nullable(可能为null)和@nonnull(从不为null)(我正在使用Java注释语法)。

定义函数时,可以对其参数进行注释。例如,以下代码将编译

int f(@nullable Foo foo) {
  if (foo == null) 
    return 0;
  return foo.size();
}

即使foo在条目中可能为null,控制流也可以保证,当你调用foo.size()时,foo是非空的。

但是如果删除null检查,则会出现编译时错误。

以下内容也将编译,因为foo在条目处是非空的:

int g(@nonnull Foo foo) {
  return foo.size(); // OK
}

但是,您将无法使用可空指针调用g:

@nullable Foo foo;
g(foo); // compiler error!

编译器对每个函数进行流分析,因此它可以检测@nullable何时变为@nonnull(例如,在检查null的if语句中)。如果立即初始化,它也会接受@nonnull可验证的定义。

@nonnull Foo foo = new Foo();

my blog中有关于此主题的更多内容。

答案 4 :(得分:0)

我认为有两个区域使用null。

首先是没有价值。例如,布尔值可以是true或false,或者用户尚未选择设置,因此为null。这是有用的,也是一件好事,但最初可能已经错误地实现了,并且现在尝试将该用法正式化。 (是否应该有第二个布尔值来保持设置/未设置状态,或者作为三态逻辑的一部分为空?)

第二个是空指针意义。这通常是程序错误情况,即。一个例外。它不是预期的状态,存在程序错误。这应该是在现代语言中实施的正式例外的保护下。也就是说,通过try / catch块捕获NullException。

那么,您对哪些内容感兴趣?

答案 5 :(得分:0)

我目前正在C#中研究这个主题。 .NET对于值类型具有Nullable,但引用类型不存在反向特征。

我为引用类型创建了NotNullable,并将问题从if(不再检查null)转移到数据类型域。这使得应用程序在运行时而不是在编译时抛出异常。

答案 6 :(得分:0)

当我们处理域对象时,非可空类型对我来说更有意义。将数据库表映射到对象并且具有不可为空的列时。假设您有一个名为User的表,它的列userid varchar(20)不可为空;

使用具有不可为空的UserId字符串字段的User类会非常方便。你可以在编译时减少一些错误。

答案 7 :(得分:-2)

我喜欢使用NULL。自从我使用C ++以来已经有一段时间了,但是它很容易找到关于返回值和引用的问题并修复它们。