7

我不断听到人们谈论不可为空的引用类型如何解决如此多的错误并使编程变得如此容易。甚至 null 的创建者也称其为十亿美元的错误,并且Spec#引入了不可为 null 的类型来解决这个问题。

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

编辑2:我一定是在和错误的人说话,我真的希望有人与之争论:-)


所以我猜,作为少数派,我错了,但我不明白为什么这场辩论有任何价值。我认为 null 是一种查找错误的工具。考虑以下:

class Class { ... }

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

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


现在考虑一下:

class Class { ... }

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

哎呀。循环被静默地跳过。可能需要一段时间才能找到问题所在。


如果你的类是空的,那么代码无论如何都会失败。为什么不让系统告诉你(尽管有点粗鲁),而不必自己弄清楚呢?

4

7 回答 7

12

Its a little odd that the response marked "answer" in this thread actually highlights the problem with null in the first place, namely:

I've also found that most of my NULL pointer errors revolve around functions from forgetting to check the return of the functions of string.h, where NULL is used as an indicator.

Wouldn't it be nice if the compiler could catch these kinds of errors at compile time, instead of runtime?

If you've used an ML-like language (SML, OCaml, SML, and F# to some extent) or Haskell, reference types are non-nullable. Instead, you represent a "null" value by wrapping it an option type. In this way, you actually change the return type of a function if it can return null as a legal value. So, let's say I wanted to pull a user out of the database:

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

Find user has the type val findUser : string -> user option, so the return type of the function actually tells you that it can return a null value. To consume the code, you need to handle both the Some and None cases:

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

If you don't handle both cases, the code won't even compile. So the type-system guarantees that you'll never get a null-reference exception, and it guarantees that you always handle nulls. And if a function returns user, its guaranteed to be an actual instance of an object. Awesomeness.

Now we see the problem in the OP's sample code:

class Class { ... }

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

Initialized and uninitialized objects have the same datatype, you can't tell the difference between them. Occasionally, the null object pattern can be useful, but the code above demonstrates that the compiler has no way to determine whether you're using your types correctly.

于 2009-09-20T20:13:47.463 回答
6

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

一个显示你忘记初始化 c 的异常会告诉你它在什么时候没有被初始化,但不是它应该在哪里被初始化。类似地,错过的循环将(隐式地)告诉您它需要在哪里有一个非零的 .count,而不是应该做什么或在哪里。我不认为这对程序员来说更容易。

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

于 2009-03-17T00:17:01.897 回答
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() 的上下文完全没有意义)。

于 2009-03-13T03:37:28.600 回答
2

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

当你定义一个函数时,你注释它的参数。例如,下面的代码将编译

int f(@nullable Foo foo) {
  如果 (foo == null)
    返回0;
  返回 foo.size();
}

尽管 foo 在入口处可能为空,但控制流保证,当您调用 foo.size() 时,foo 是非空的。

但是如果你取消对 null 的检查,你会得到一个编译时错误。

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

int g(@nonnull Foo foo) {
  返回 foo.size(); // 好的
}

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

@nullable Foo foo;
g(foo); // 编译器错误!

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

@nonnull Foo foo = new Foo();

在我的博客中有更多关于这个主题的内容。

于 2009-04-13T19:56:09.590 回答
0

正如我所看到的,有两个使用 null 的区域。

首先是没有价值。例如,布尔值可以为真或假,或者用户尚未选择设置,因此为空。这是有用的,也是一件好事,但可能最初实施不正确,现在正试图将这种使用形式化。(应该有第二个布尔值来保存设置/取消设置状态,还是 null 作为三态逻辑的一部分?)

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

那么,您对其中的哪些感兴趣?

于 2009-03-17T00:22:15.283 回答
0

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

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

于 2009-09-20T19:35:40.283 回答
0

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

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

于 2011-01-21T19:19:24.627 回答