没有空值的编程语言的最佳解释

229

经常当程序员抱怨 null 错误/异常时,会有人问如果不用 null 会怎样。

我对选项类型的酷炫之处有一些基本概念,但我没有足够的知识或语言技能来最好地表达它。以下是一个可以向普通程序员解释的 的说明:

  • 默认情况下将引用/指针设为可空的不可取性
  • 选项类型的工作原理,包括简化检查 null 情况的策略,例如
    • 模式匹配和
    • 单子综合
  • 其他解决方案,如消息消耗 nil
  • (我可能遗漏了其他方面)

11
如果您为此问题添加功能编程或 F# 标签,您肯定会得到一些绝妙的答案。 - Stephen Swensen
我添加了函数式编程标签,因为option类型来自ml世界。我不想将其标记为F#(太具体)。顺便说一句,需要有分类能力的人添加一个maybe-type或option-type标签。 - Roman A. Taycher
4
我认为这些特定的标签基本上没有太大必要。主要是为了让人们能够找到相关的问题(例如,“我知道很多答案并且能够回答的问题”、“函数式编程”在这方面非常有用)。但类似“null”或“option-type”这样的标签则没那么有用。很少有人会监视“option-type”标签以寻找他们能够回答的问题。;) - jalf
不要忘记,空值(null)的主要原因之一是计算机与集合论紧密相连地发展。在所有集合论中,空值是最重要的集合之一。如果没有它,整个算法都会崩溃。例如,执行归并排序。这涉及多次将列表分成两半。如果列表长度为7项,那怎么办?首先将其分成4和3。然后是2、2、2和1。然后是1、1、1、1、1、1、1,以及......空值!空值有一个目的,只是你实际上看不见它。它更存在于理论领域。 - stevendesu
6
@steven_desu - 我不同意你的观点。在“可为null”的编程语言中,你可以有一个对空列表[]的引用,同时也可以有一个空的null列表引用。这个问题涉及到混淆这两个概念之间的区别。 - stusmith
有点重复:https://dev59.com/PUzSa4cB1Zd3GeqPkC3fhttps://dev59.com/-3M_5IYBdhLWcg3wSBAUhttps://dev59.com/bHVC5IYBdhLWcg3wz0l9https://dev59.com/vkrSa4cB1Zd3GeqPXprUhttps://dev59.com/yXRB5IYBdhLWcg3w77kn?lq=1https://dev59.com/1HRB5IYBdhLWcg3wkH9N - nawfal
11个回答

440

我认为null不可取的简洁概括是: 没有意义的状态不应该被表示出来

假设我正在模拟一扇门。它可以处于三种状态之一:打开、关闭但未锁定和关闭且已锁定。现在我可以按以下方式对其进行建模:

class Door
    private bool isShut
    private bool isLocked

很明显如何将我的三个状态映射到这两个布尔变量中。但这留下了第四种不希望出现的状态:isShut==false && isLocked==true。因为我选择的类型允许存在这种状态,所以我必须花费心力确保该类永远不会进入此状态(例如通过显式编写不变量)。相比之下,如果我使用具有代数数据类型或已检查枚举的语言,则可以定义...

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

那我可以定义

class Door
    private DoorState state
并且不需要再担心什么了。类型系统将确保Door类实例只有三种可能的状态。这正是类型系统所擅长的-在编译时显式地排除一整类错误。 null的问题在于每个引用类型都会获得这个通常不需要的额外状态。一个string变量可以是任何字符序列,或者它可以是这个疯狂的额外null值,它不能映射到我的问题域。一个Triangle对象有三个Point,它们本身有XY值,但不幸的是,这些PointTriangle本身可能是这个毫无意义的空值,对于我正在处理的图形领域来说是没有意义的。等等。
当您确实打算模拟一种可能不存在的值时,您应该明确选择它。如果我打算模拟人员的方式是每个Person都有一个FirstName和一个LastName,但只有一些人有MiddleName,那么我想说的是:
class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

假设这里的 string 类型是一个非空类型,那么就不需要建立棘手的不变量,也不会在尝试计算某人姓名长度时出现意外的 NullReferenceException。类型系统可以确保任何处理 MiddleName 的代码都考虑到了它可能为 None,而任何处理 FirstName 的代码都可以安全地假定那里有一个值。

例如,使用上面的类型,我们可以编写以下愚蠢的函数:

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

无需担心。相比之下,在一种具有可空引用的语言中,假设字符串是nullable类型,那么

class Person
    private string FirstName
    private string MiddleName
    private string LastName

你最终会创作出像这样的东西

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

如果传入的Person对象不符合所有非空不变量,则会出现问题。

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

或者也许

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length
假设 p 确保了第一个和最后一个值存在,但中间的值可能为空,或者您可以进行不同类型异常检查,还有其他未知的操作。所有这些疯狂的实现选择和需要考虑的事情都是因为有这个无用的可表示值,而且您既不需要也不想要它。 通常情况下,null 值会增加不必要的复杂性。 复杂性是所有软件的敌人,您应该尽力在合理的范围内减少复杂性。
(请注意,即使 FirstName 不能为 null,但 string 可以表示空字符串 "",这可能也不是我们想要建模的人名。因此,即使是非 null 字符串,仍然可能存在“表示无意义值”的情况。同样,您可以选择通过运行时的不变式和条件代码来解决这个问题,或者使用类型系统(例如,使用 NonEmptyString 类型)。后者可能不明智(“好”类型通常在一组公共操作上“封闭”,例如,NonEmptyString 不适用于 .SubString(0,0)),但它展示了设计空间中的更多观点。总之,在任何给定类型系统中,从“默认可空引用”到“默认非空引用”的变化几乎总是一个简单的变化,使得类型系统更好地应对复杂性并消除某些类型的错误和无意义状态。因此,令人惊讶的是,很多语言仍然一次又一次地重复这个错误。)

31
回复:名称 - 确实。也许你关心的是建模一扇敞开的门,但是死锁插销露出来,防止门关闭。世界上有很多复杂性。关键是在软件中实现“世界状态”和“程序状态”之间的映射时不要增加更多的复杂性。 - Brian
61
什么,你从未把门锁开着吗? - Joshua
59
我不明白为什么人们会对某个领域的语义纠缠不休。布莱恩简洁明了地解释了 null 的缺陷,他在示例中通过说每个人都有名字和姓氏来简化问题领域。问题被完美地回答了,如果布莱恩你有机会来波士顿,我会请你喝啤酒以感谢你在这里的所有发帖! - akaphenom
67
谢谢,但请注意不是所有人都喝啤酒(我是不喝酒的人)。但是我很感激你只是使用了一个简化的世界模型来表达感激之情,所以我不会再为你的世界模型中的错误假设争辩太多。 (现实世界中有太多的复杂性! :)) - Brian
4
这个世界上竟然有三态门!它们被一些旅馆当作厕所门使用。一个按钮在门内充当钥匙,可以从外面锁住门。当插销移动时,它会自动解锁。请注意,此翻译仅供参考,如有不准确之处请以原文为准。 - comonad
显示剩余8条评论

68
  • There is no value assigned to the variable.
  • The variable has an explicitly assigned value of null.
  • Option types provide a way to handle the first case in a more explicit and safe way. By default, types are not nullable, which means that there is no need to check for null values unless an option type is explicitly used. This eliminates the ambiguity and confusion that can arise from allowing null values by default.

    In cases where a null state needs to be represented, an option type can be used to allow for this possibility. However, because option types are not used by default, it is clear when a value can potentially be null and when it cannot be. This reduces the need for constant checking and documentation, as the compiler enforces the non-nullability of types by default.

  • 变量未初始化,这是理想情况下不应该发生的事情。除非变量被初始化,否则它不应该存在
  • 变量包含一些“可选”数据:它需要能够表示没有数据的情况。有时候这是必要的。也许你正在尝试在列表中查找一个对象,你事先不知道它是否存在。那么我们需要能够表示“没有找到对象”的情况。
  • 第二个意义必须保留,但第一个意义必须完全消除。即使第二个意义也不应该是默认设置。它是我们可以在需要时选择使用的可选项。但是当我们不需要某些东西是可选的时候,我们希望类型检查器保证它永远不会为null。


    1
    在第二个意义上,我们希望编译器在我们尝试访问这些变量之前没有进行空值检查时发出警告(停止?)。这是一篇关于即将到来的 C# 空/非空特性(终于!)的优秀文章:https://blogs.msdn.microsoft.com/dotnet/2017/11/15/nullable-reference-types-in-csharp/ - Ohad Schneider

    45

    到目前为止,所有的答案都集中在为什么null是一件坏事以及如果一种语言可以保证某些值永远不会为空,那么这种情况会很方便。

    然后他们建议,如果您添加像OptionMaybe这样的概念来表示可能不总是有定义值的类型,则可以强制执行所有值的非空性。Haskell采用了这种方法。

    这都是好东西!但它并不排除使用明确可空/非空类型来实现相同的效果。那么,为什么Option仍然是一件好事呢?毕竟,Scala支持可空值(必须支持,以便与Java库一起使用),但也支持Options

    问:除了能够完全从语言中删除null之外,还有什么好处呢?

    答:组合

    如果您从具有null感知的代码进行了一个幼稚的翻译

    def fullNameLength(p:Person) = {
      val middleLen =
        if (null == p.middleName)
          p.middleName.length
        else
          0
      p.firstName.length + middleLen + p.lastName.length
    }
    

    转化为支持选项的代码
    def fullNameLength(p:Person) = {
      val middleLen = p.middleName match {
        case Some(x) => x.length
        case _ => 0
      }
      p.firstName.length + middleLen + p.lastName.length
    }
    

    并没有太大的区别!但这也是一种糟糕的使用Options的方式...这种方法更加简洁:

    def fullNameLength(p:Person) = {
      val middleLen = p.middleName map {_.length} getOrElse 0
      p.firstName.length + middleLen + p.lastName.length
    }
    

    甚至可以这样:
    def fullNameLength(p:Person) =       
      p.firstName.length +
      p.middleName.map{length}.getOrElse(0) +
      p.lastName.length
    

    当你开始处理选项列表时,它变得更加优秀。 想象一下List people 本身是可选的:

    people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)
    

    这是如何运作的呢?
    //convert an Option[List[Person]] to an Option[S]
    //where the function f takes a List[Person] and returns an S
    people map f
    
    //find a person named "Joe" in a List[Person].
    //returns Some[Person], or None if "Joe" isn't in the list
    validPeopleList find (_.firstName == "joe")
    
    //returns None if people is None
    //Some(None) if people is valid but doesn't contain Joe
    //Some[Some[Person]] if Joe is found
    people map (_ find (_.firstName == "joe")) 
    
    //flatten it to return None if people is None or Joe isn't found
    //Some[Person] if Joe is found
    people flatMap (_ find (_.firstName == "joe")) 
    
    //return Some(length) if the list isn't None and Joe is found
    //otherwise return None
    people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)
    

    带有空值检查(甚至是elvis ?: 运算符)的相应代码将非常冗长。真正的诀窍在于flatMap操作,它允许以可为空的值无法实现的方式对选项和集合进行嵌套理解。


    8
    +1,这是一个值得强调的好观点。有一个补充:在Haskell界,flatMap被称为(>>=),也就是单子(monad)绑定操作符。没错,Haskeller们非常喜欢使用flatMap,以至于我们将它添加到了我们语言的标志中。 - C. A. McCann
    1
    +1 希望 Option<T> 的表达式永远不会为空。可惜,Scala 仍然与 Java 相关联 :-) (另一方面,如果 Scala 不与 Java 兼容,谁会使用它呢?O.o) - user166390
    很容易做到:'List(null).headOption'。请注意,这意味着返回值与“None”非常不同。 - Kevin Wright
    4
    我给了你赏金是因为我很喜欢你关于构图方面的观点,而其他人似乎没有提到。 - Roman A. Taycher
    非常好的回答,有很棒的例子! - thSoft

    39

    由于有些人可能会遗漏,null是含糊不清的。

    Alice的出生日期是null。这是什么意思?

    Bob的死亡日期是null。那是什么意思?

    一个“合理”的解释可能是Alice的出生日期存在,但未知,而Bob的死亡日期不存在(Bob还活着)。但为什么我们得到了不同的答案呢?


    另一个问题:null是一个边缘情况。

    • null = null吗?
    • nan = nan吗?
    • inf = inf吗?
    • +0 = -0吗?
    • +0/0 = -0/0吗?

    答案通常是“是”,“不是”,“是”,“是”,“不是”,“是”。疯狂的“数学家”称NaN为“nullity”,并说它与自身相等。SQL将null视为不等于任何东西(因此它们的行为类似于NaNs)。人们想知道当您尝试将±∞、±0和NaN存储到同一数据库列中时会发生什么(有253个NaN,其中一半是“负数”)。

    更糟糕的是,数据库在处理NULL方面存在差异,而且大多数数据库都不一致(有关概述,请参见SQLite中的NULL处理)。这非常可怕。


    现在是必备的故事:

    我最近设计了一个(sqlite3)数据库表,其中包含五列a NOT NULL,b,id_a,id_b NOT NULL,timestamp。因为它是一个通用模式,旨在解决相当任意的应用程序的通用问题,所以有两个唯一性约束:

    UNIQUE(a, b, id_a)
    UNIQUE(a, b, id_b)
    
    id_a只是为了兼容现有的应用程序设计(部分原因是因为我还没有想出更好的解决方案),在新应用程序中不会使用。由于NULL在SQL中的工作方式,我可以插入(1, 2, NULL, 3, t)(1, 2, NULL, 4, t)而不违反第一个唯一性约束(因为(1, 2, NULL)!=(1, 2, NULL))。
    这种方法之所以有效,是因为大多数数据库在唯一性约束中使用NULL的方式(可能是为了更容易地模拟“现实世界”的情况,例如,没有两个人可以拥有相同的社会安全号码,但并非所有人都有这个号码)。
    顺便说一句,在未定义行为之前,C++引用不能“指向”空,也不可能构造具有未初始化引用成员变量的类(如果抛出异常,则构造失败)。
    附注:偶尔您可能需要互斥指针(即只有其中一个可以为非NULL),例如在假设的iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed中。相反,我被迫执行像assert((bool)actionSheet + (bool)alertView == 1)这样的操作。

    真正的数学家并不使用“NaN”的概念,放心吧。 - Noldorin
    @Noldorin:他们确实这样做,但是他们使用“不定式形式”的术语。 - I. J. Kennedy
    @I.J.Kennedy:那是一所不同的学院,我非常了解,谢谢。一些“NaN”可能代表不定形式,但由于FPA不进行符号推理,将其等同于不定形式是非常误导人的! - Noldorin
    assert(actionSheet ^ alertView)有什么问题吗?或者你的编程语言不支持异或布尔值? - cat

    16

    将引用/指针默认设置为可空的不良影响。

    我认为这不是null的主要问题,null的主要问题是它们可以有两种含义:

    1. 引用/指针未初始化:这里的问题与一般的可变性相同。首先,它使分析代码更加困难。
    2. 变量为空实际上意味着某些东西:这就是Option类型实际上规范的情况。

    支持Option类型的语言通常也禁止或不鼓励使用未初始化的变量。

    Option类型的工作原理,包括简化检查null案例的策略,如模式匹配。

    为了有效,Option类型需要直接在语言中支持。否则,需要大量样板代码来模拟它们。模式匹配和类型推断是使Option类型易于使用的两个关键语言特性。例如:

    在F#中:

    //first we create the option list, and then filter out all None Option types and 
    //map all Some Option types to their values.  See how type-inference shines.
    let optionList = [Some(1); Some(2); None; Some(3); None]
    optionList |> List.choose id //evaluates to [1;2;3]
    
    //here is a simple pattern-matching example
    //which prints "1;2;None;3;None;".
    //notice how value is extracted from op during the match
    optionList 
    |> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")
    

    但是,在像Java这样没有直接支持Option类型的语言中,我们可能会有以下代码:

    //here we perform the same filter/map operation as in the F# example.
    List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
    List<Integer> filteredList = new ArrayList<Integer>();
    for(Option<Integer> op : list)
        if(op instanceof Some)
            filteredList.add(((Some<Integer>)op).getValue());
    

    替代方案:如消息吃掉nil

    Objective-C的“消息吃掉nil”并不是一个解决方案,而是试图减轻空指针检查的头痛。基本上,当尝试在一个空对象上调用方法时,它不会抛出运行时异常,而是表达式本身评估为空值。暂时搁置不谈,好像每个实例方法都以if (this == null) return null;开头。但是这样会丢失信息:你不知道方法返回null是因为它是有效的返回值,还是因为对象实际上是null。这很像异常吞噬,而且并没有任何进展来解决前面提到的null问题。


    这是一个小恼点,但C#几乎不像C语言。 - Roman A. Taycher
    4
    我原本打算使用Java,因为C#可能有更好的解决方案......但我感谢你的抱怨,人们真正想表达的是“具有C式语法的语言”。我已经将“类C”的语句替换掉了。 - Stephen Swensen
    用linq,对吧。我一直在想c#,没注意到这个。 - Roman A. Taycher
    1
    是的,大多数编程语言都受到C语言的启发,但我认为像Python/Ruby这样几乎没有C语言风格的命令式编程语言被函数式程序员称为类C语言。 - Roman A. Taycher

    11

    汇编语言提供了地址,也被称为未类型化指针。C语言直接将它们映射为类型化指针,但引入了Algol语言的null作为唯一的指针值,与所有类型的指针兼容。在C语言中,null的一个大问题是由于每个指针都可以是null,因此永远不能在没有手动检查的情况下安全地使用指针。

    在更高级别的语言中,有null是令人尴尬的,因为它实际上传达了两个不同的概念:

    • 告诉我们某些内容是未定义的
    • 告诉我们某些内容是可选的

    拥有未定义的变量基本上是没有用的,并且会导致不确定行为,无论何时发生。我想每个人都会同意,应该尽一切可能避免出现未定义的情况。

    第二种情况是可选性,最好明确提供,例如通过option type


    假设我们在一个运输公司,需要创建一个应用程序来帮助为我们的司机创建时间表。对于每个司机,我们存储一些信息,例如:他们拥有的驾驶执照和紧急情况下的电话号码。

    在C语言中,我们可以这样写:

    struct PhoneNumber { ... };
    struct MotorbikeLicence { ... };
    struct CarLicence { ... };
    struct TruckLicence { ... };
    
    struct Driver {
      char name[32]; /* Null terminated */
      struct PhoneNumber * emergency_phone_number;
      struct MotorbikeLicence * motorbike_licence;
      struct CarLicence * car_licence;
      struct TruckLicence * truck_licence;
    };
    

    正如您所观察到的,在我们处理司机列表时,我们必须检查空指针。编译器无法为您提供帮助,程序的安全性取决于您自己。

    在OCaml中,相同的代码看起来像这样:

    type phone_number = { ... }
    type motorbike_licence = { ... }
    type car_licence = { ... }
    type truck_licence = { ... }
    
    type driver = {
      name: string;
      emergency_phone_number: phone_number option;
      motorbike_licence: motorbike_licence option;
      car_licence: car_licence option;
      truck_licence: truck_licence option;
    }
    
    让我们假设现在我们想要打印出所有司机的名字以及他们的卡车驾驶证号码。
    在C语言中:
    #include <stdio.h>
    
    void print_driver_with_truck_licence_number(struct Driver * driver) {
      /* Check may be redundant but better be safe than sorry */
      if (driver != NULL) {
        printf("driver %s has ", driver->name);
        if (driver->truck_licence != NULL) {
          printf("truck licence %04d-%04d-%08d\n",
            driver->truck_licence->area_code
            driver->truck_licence->year
            driver->truck_licence->num_in_year);
        } else {
          printf("no truck licence\n");
        }
      }
    }
    
    void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
      if (drivers != NULL && nb >= 0) {
        int i;
        for (i = 0; i < nb; ++i) {
          struct Driver * driver = drivers[i];
          if (driver) {
            print_driver_with_truck_licence_number(driver);
          } else {
            /* Huh ? We got a null inside the array, meaning it probably got
               corrupt somehow, what do we do ? Ignore ? Assert ? */
          }
        }
      } else {
        /* Caller provided us with erroneous input, what do we do ?
           Ignore ? Assert ? */
      }
    }
    

    在OCaml中这样写:

    open Printf
    
    (* Here we are guaranteed to have a driver instance *)
    let print_driver_with_truck_licence_number driver =
      printf "driver %s has " driver.name;
      match driver.truck_licence with
        | None ->
            printf "no truck licence\n"
        | Some licence ->
            (* Here we are guaranteed to have a licence *)
            printf "truck licence %04d-%04d-%08d\n"
              licence.area_code
              licence.year
              licence.num_in_year
    
    (* Here we are guaranteed to have a valid list of drivers *)
    let print_drivers_with_truck_licence_numbers drivers =
      List.iter print_driver_with_truck_licence_number drivers
    

    正如您在这个简单的例子中所看到的,安全版本中没有任何复杂之处:

    • 它更加简洁。
    • 您可以获得更好的保证,而且根本不需要进行空值检查。
    • 编译器确保你正确处理了选项。

    然而,在 C 语言中,你可能会忘记一个空值检查,然后就崩溃了......

    注意:这些代码示例未经编译,但我希望您能理解其中的思想。


    我从未尝试过,但http://en.wikipedia.org/wiki/Cyclone_%28programming_language%29声称允许C语言使用非空指针。 - Roman A. Taycher
    1
    我不同意你的说法,即没有人对第一种情况感兴趣。许多人,特别是函数式语言社区的人,对此非常感兴趣,并且要么反对使用未初始化的变量,要么完全禁止使用它们。 - Stephen Swensen
    1
    @Stephen:我们可能想表达的是同样的意思。对我来说,他们不鼓励或禁止使用未初始化的事物,正是因为讨论未定义的事物毫无意义,我们无法对它们进行理智或有用的操作。这完全没有任何意义。 - bltxd
    2
    正如@tc所说,null与汇编语言无关。在汇编语言中,类型通常*不可为空。加载到通用寄存器中的值可能为零,也可能为非零整数。但它永远不可能是null。即使将内存地址加载到寄存器中,在大多数常见的架构中,也没有单独表示“空指针”的方式。这是在高级语言(如C)中引入的概念。 - jalf
    @DaveGriffith 这是Tony Hoare说的,不是John Backus。 - David Conrad
    显示剩余5条评论

    5

    微软研究院有一个有趣的项目叫做

    Spec#

    它是C#的扩展,具有非空类型和一些机制来检查您的对象是否为空,尽管在我看来,应用契约设计原则可能更合适,并且对于由空引用引起的许多麻烦情况更有帮助。


    4

    作为一个来自.NET背景的人,我一直认为null是有用的。直到我了解了结构体以及使用它们避免了很多样板代码的简便性。Tony Hoare在2009年的QCon London演讲中道歉发明了空引用。引用他的话:

    我称之为我的十亿美元错误。它是空引用在1965年的发明。当时,我正在设计面向对象语言(ALGOL W)中引用的第一个全面类型系统。我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但我无法抵制放置空引用的诱惑,仅仅因为它很容易实现。这导致了无数的错误、漏洞和系统崩溃,在过去四十年里可能造成了十亿美元的痛苦和损失。近年来,一些程序分析器如Microsoft的PREfix和PREfast已被用于检查引用,并在存在非空风险时发出警告。更近期的编程语言如Spec#已经引入了非空引用的声明。这就是解决方案,而我在1965年拒绝了这个方案。
    另请参见此问题at programmers

    4

    1

    我一直认为Null(或nil)是“值的缺失”。

    有时候你需要它,有时候你不需要。这取决于你所处理的领域。如果缺失是有意义的:没有中间名,那么你的应用程序可以相应地采取行动。另一方面,如果null值不应该存在:名字为空,那么开发人员就会接到谚语般的凌晨2点电话。

    我也看到过代码因检查null而过载和过于复杂化。对我来说,这意味着两件事:
    a)应用程序树上方的错误
    b)不良/不完整的设计

    从积极的一面来看-Null可能是检查某些东西是否不存在的更有用的概念之一,而没有null概念的语言在进行数据验证时将会变得过于复杂。在这种情况下,如果一个新变量没有初始化,那么这些语言通常会将变量设置为空字符串、0或空集合。然而,如果空字符串或0或空集合是你的应用程序的有效值-那么你就有问题了。

    有时候,为了表示未初始化的状态,会为字段发明特殊/奇怪的值来规避这个问题。但是,如果一个好心的用户输入了这个特殊值,那么会发生什么呢?更不用说这将对数据验证程序造成的混乱了。 如果语言支持 null 概念,所有这些问题都将消失。

    嗨@Jon,我有点难以跟上你的思路。我终于意识到,你所说的“特殊/奇怪”的值可能是指类似于Javascript的'undefined'或IEEE的'NaN'之类的东西。但除此之外,你并没有真正回答OP提出的任何问题。而且,“Null可能是检查某些东西是否不存在的最有用概念”这一说法几乎肯定是错误的。选项类型是一个备受推崇的、类型安全的替代null的选择。 - Stephen Swensen
    @Stephen - 实际上回顾我的留言后,我认为整个下半部分都应该移动到一个尚未提出的问题中。但我仍然认为null对于检查某些东西是否缺失非常有用。 - Jon

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