【问题标题】:About the non-nullable types debate关于不可为空类型的争论
【发布时间】:2010-10-13 01:59:41
【问题描述】:

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

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

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


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

【问题讨论】:

  • 很高兴看到其他人喜欢 null,我还在上学,所以我只是假设我缺少一些东西。
  • 处理“无价值”的方法更原则。 NULL 不包括原始类型,例如 int。类型系统最好在所有类型中一致地表示缺少值,而不是仅隐式地用于引用。请参阅 Haskell 的“Maybe”和 ML/OCaml/F# 的“选项”类型以了解它应该如何完成。

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


【解决方案1】:

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

如果有一个带有不可为空的 UserId 字符串字段的 User 类,那就太方便了。您可以在编译时减少一些错误。

【讨论】:

    【解决方案2】:

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

    我还发现我的大部分 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 -&gt; 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 可能很有用,但上面的代码表明编译器无法确定您是否正确使用了类型。

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

        非空类型的想法是让编译器而不是您的客户端发现错误。假设您向您的语言添加两个类型说明符 @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();

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

        【讨论】:

          【解决方案5】:

          在我看来,有两个地方使用了 null。

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

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

          那么,你对其中的哪一个感兴趣?

          【讨论】:

            【解决方案6】:

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

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

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

            【讨论】:

            • 所以如果一种语言实现了不可为空的类型,那么像“Class c;”这样的语句是什么?将 c 设置为? (顺便说一句,我完全同意你的 cmets)
            • 我的第一个想法:不应该被允许。从逻辑上讲,“制作一个必须存储 Foo 的插槽(但将其留空)”是没有意义的。这可能需要一切都是表达式(如 Lisp 或 Ruby)——我不知道如果你必须执行顺序语句来进行赋值,它会如何工作。
            • 回首往事:想想 SQL。如果您有一个“NOT NULL”列并尝试插入一条记录而不为其指定非空值,它只会引发错误。
            • 所以在这种情况下,每次分配都会生成代码,以确保我们不会将其设置为 null 并在此时引发异常。这真的比在关键点手动检查 null 并作为程序员对引发异常或在程序流程中考虑它的效用做出明智决定更可取吗?
            【解决方案7】:

            我承认我并没有真正阅读过很多关于 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() 的上下文完全没有意义)。

            【讨论】:

              猜你喜欢
              • 2022-01-04
              • 1970-01-01
              • 2012-10-27
              • 2010-10-10
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-10-21
              相关资源
              最近更新 更多