关于非空类型争议

7

我一直听��们谈论非空引用类型如何解决许多错误并使编程变得更加容易。甚至null的创建者称其为他的十亿美元失误,而Spec#已经引入了非空类型来解决这个问题。

编辑:忽略我关于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) { ... }
}

糟糕,循环被默默地跳过了。追踪问题可能需要一些时间。


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

很高兴看到其他人喜欢null,我还在上学,所以我认为可能有些东西我还没掌握。 - he_the_great
1
处理“无值”的更有原则的方法还有很多。NULL不包括基本类型,如int。最好的方式是让类型系统在所有类型中一致地表示缺少值,而不仅仅是对引用隐式地表示。可以看看Haskell的“Maybe”和ML/OCaml/F#的“option”类型,了解如何应该处理它。 - MichaelGG
可能是没有null的语言的最佳解释的重复。 - nawfal
我们现在已经到了2022年,C#添加了这个功能。 - Ian Boyd
7个回答

13

在这个线程中标记为“答案”的响应实际上凸显了空指针问题的奇怪之处,即:

我也发现我的大多数NULL指针错误都与函数有关,忘记检查string.h的函数返回值,在其中使用NULL作为指示符。

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

如果您使用过类似ML的语言(SML、OCaml、SML以及在某种程度上的F#)或Haskell,引用类型是不可为空的。相反,您通过包装它来表示“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的函数,因此该函数的返回类型告诉您它可以返回空值。为了使用该代码,您需要处理SomeNone这两种情况:

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 可以很有用,但上面的代码表明编译器无法确定您是否正确使用了类型。


6
我不理解你的例子。如果你的“= new Class()”只是一个占位符,代替了没有null,那么(在我看来)显然是一个bug。如果不是这样,那么真正的bug就是“...”没有正确地设置其内容,在两种情况下完全相同。
一个异常会告诉你忘记初始化c的位置,但不会告诉你应该在哪里初始化它。同样,一个错过的循环将(隐含地)告诉你需要一个非零.count的位置,但不会告诉你应该做什么或在哪里。我认为这两个方案对程序员来说都不容易。
我认为“无null”的重点不是简单地进行文本查找和替换,并将它们全部变成空实例。那显然是无用的。重点是构建你的代码,使得你的变量永远不处于指向无用/不正确值的状态,其中NULL只是最常见的一种情况。

如果一种语言实现了非空类型,那么像“Class c;”这样的语句会将c设置为什么?(顺便说一下,我完全同意你的评论) - zildjohn01
2
我的第一反应是:这不应该被允许。从逻辑上讲,说“创建一个必须存储Foo的插槽(但保持为空)”是没有意义的。这可能需要一切皆为表达式(像Lisp或Ruby一样)--如果您必须执行顺序语句来进行赋值,我不知道它将如何工作。 - Ken
1
回顾一下:想到 SQL。如果您有一个“NOT NULL”列,并尝试插入一个没有为其指定非空值的记录,它只会引发错误。 - Ken
那么在这种情况下,每次分配都会生成代码以确保我们不将其设置为null,并在该点引发异常。这是否真的比在关键点手动检查null更可取,并作为程序员对引发异常的效用进行知情决策,或在程序流程中考虑它? - Jason D

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()的上下文完全无关)。


2
非空类型的概念是让编译器而不是客户端发现错误。假设你在语言中增加了两个类型指示符@nullable(可以为空)和@nonnull(不可能为空)(我使用Java注释语法)。
当你定义一个函数时,需要对其参数进行注释。例如,下面的代码将编译:
int f(@nullable Foo foo) {
  if (foo == null) 
    return 0;
  return foo.size();
}
即使foo在进入时可能为空,控制流也保证在调用foo.size()时foo是非空的。
但是,如果你移除了对null的检查,就会得到一个编译时错误。
以下代码也将编译,因为foo在进入时是非空的:
int g(@nonnull Foo foo) {
  return foo.size(); // OK
}
然而,你将无法使用可为空指针调用g:
@nullable Foo foo;
g(foo); // 编译器错误!
编译器会为每个函数进行流分析,因此它可以检测@nullable何时变为@nonnull(例如,在检查null的if语句内部)。它还将接受@nonnull变量定义,前提是立即初始化。
@nonnull Foo foo = new Foo();
有关此主题的更多信息,请参见我的博客

0

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

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


0

我目前正在使用C#研究这个主题。.NET为值类型提供了Nullable,但对于引用类型不存在相反的功能。

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


0

在我看来,null被用于两个方面。

第一个是值的缺失。例如,布尔值可以是true或false,或者用户尚未选择设置,因此为null。这是有用的和好的事情,但可能最初实现不正确,现在正在尝试正式化使用它。(是否应该有第二个布尔值来保存设置/取消设置状态,或者将null作为三态逻辑的一部分?)

第二个是在空指针意义上。这往往是程序错误情况,即异常。这不是一个预期的状态,而是程序错误。这应该包含在正式异常的范围内,如现代语言中所实现的那样。也就是说,通过try/catch块捕获NullException。

那么,你对这些哪一个感兴趣?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接