不带 null 的语言的最佳解释

Best explanation for languages without null

提问人:Roman A. Taycher 提问时间:10/21/2010 最后编辑:M. GruberRoman A. Taycher 更新时间:2/16/2018 访问量:46648

问:

每隔一段时间,当程序员抱怨 null 错误/异常时,就会有人问我们没有 null 怎么办。

我对选项类型的酷有一些基本的想法,但我没有知识或语言技能来最好地表达它。对以下内容的一个很好的解释是什么,以一种普通程序员可以理解的方式编写,我们可以将这个人指向?

  • 默认情况下,引用/指针可为空是不可取的
  • 选项类型的工作原理,包括简化检查 null 情况的策略,例如
    • 模式匹配和
    • 一元推导
  • 替代解决方案,例如消息吞噬 nil
  • (我错过的其他方面)
语言 函数式编程 null nullpointerexception 不可为 null

评论

11赞 Stephen Swensen 10/22/2010
如果你为函数式编程或 F# 的问题添加标签,你一定会得到一些奇妙的答案。
0赞 Roman A. Taycher 10/22/2010
我添加了函数式编程标签,因为选项类型确实来自 ml 世界。我宁愿不将其标记为F#(太具体)。顺便说一句,具有分类能力的人需要添加 maybe-type 或 option-type 标签。
4赞 jalf 10/22/2010
我怀疑几乎不需要这种特定的标签。标签主要是为了让人们找到相关的问题(例如,“我非常了解的问题,并且能够回答”,而“函数式编程”在那里非常有帮助。但是像“null”或“option-type”这样的东西就没那么有用了。很少有人会监控“选项类型”标签,寻找他们可以回答的问题。;)
0赞 stevendesu 10/30/2010
我们不要忘记,null 的主要原因之一是计算机的发展与集合论密切相关。Null 是所有集合论中最重要的集合之一。没有它,整个算法就会崩溃。例如,执行合并排序。这涉及多次将列表分成两半。如果列表有 7 个项目怎么办?首先,将其拆分为 4 和 3。然后是 2、2、2 和 1。然后是 1、1、1、1、1、1、1 和......零!Null 有一个目的,只是一个你在实践中看不到的目的。它更多地存在于理论领域。
6赞 stusmith 11/12/2010
@steven_desu - 我不同意。在“可为空”语言中,可以引用空列表 [],也可以引用空列表。这个问题与两者之间的混淆有关。

答:

16赞 Stephen Swensen 10/22/2010 #1

默认情况下,引用/指针可为 null 的不可取性。

我不认为这是空值的主要问题,空值的主要问题是它们可能意味着两件事:

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

支持 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 的“消息吃零”与其说是一种解决方案,不如说是试图减轻 null 检查的头疼。基本上,表达式在尝试对 null 对象调用方法时不会引发运行时异常,而是计算结果为 null 本身。暂停怀疑,就好像每个实例方法都以 开头。但随之而来的是信息丢失:你不知道该方法返回 null 是因为它是有效的返回值,还是因为对象实际上是 null。这很像异常吞噬,并且在解决前面概述的 null 问题方面没有任何进展。if (this == null) return null;

评论

0赞 Roman A. Taycher 10/22/2010
这是一个小小的烦恼,但 c# 几乎不是一种类似 C 的语言。
4赞 Stephen Swensen 10/22/2010
我在这里要用 Java,因为 C# 可能会有更好的解决方案......但我很欣赏你的烦恼,人们真正的意思是“一种受 C 启发的语法语言”。我继续替换了“c-like”语句。
0赞 Roman A. Taycher 10/22/2010
使用 linq,对。我在想c#,但没有注意到这一点。
1赞 Roman A. Taycher 10/22/2010
是的,主要是受 c 启发的语法,但我想我也听说过像 python/ruby 这样的命令式编程语言,而函数式程序员很少使用类似 c 的语法。
11赞 bltxd 10/22/2010 #2

Assembly 为我们带来了地址,也称为无类型指针。C 将它们直接映射为类型化指针,但引入了 Algol 的 null 作为唯一的指针值,与所有类型化指针兼容。C 语言中 null 的最大问题是,由于每个指针都可以为 null,因此如果不进行手动检查,则永远无法安全地使用指针。

在高级语言中,null 很尴尬,因为它实际上传达了两个不同的概念:

  • 告诉某些事情是不确定的。
  • 告诉某事是可选的。

拥有未定义的变量几乎是无用的,并且每当它们发生时都会产生未定义的行为。我想每个人都会同意,应该不惜一切代价避免未定义的事情。

第二种情况是可选性,最好显式提供,例如使用选项类型


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

在 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 语言中,您可能忘记了空检查和繁荣......

注意:这些代码示例没有编译,但我希望你明白了。

评论

0赞 Roman A. Taycher 10/22/2010
我从未尝试过,但 en.wikipedia.org/wiki/Cyclone_%28programming_language%29 声称允许 c 的非空指针。
1赞 Stephen Swensen 10/22/2010
我不同意你的说法,即没有人对第一种情况感兴趣。许多人,尤其是功能语言社区中的人,对此非常感兴趣,并且不鼓励或完全禁止使用未初始化的变量。
0赞 10/22/2010
我相信“可能不指向任何东西的参考”是为某些 Algol 语言发明的(维基百科同意,见 en.wikipedia.org/wiki/Null_pointer#Null_pointer)。但是,当然,程序集程序员很可能初始化了指向无效地址的指针(读取:Null = 0)。NULL
1赞 bltxd 10/22/2010
@Stephen:我们的意思可能是一样的。对我来说,他们不鼓励或禁止使用未初始化的东西,正是因为讨论未定义的东西是没有意义的,因为我们不能用它们做任何理智或有用的事情。它不会有任何兴趣。
2赞 jalf 10/22/2010
如@tc。说,null 与汇编无关。在程序集中,类型通常不可为 null。加载到通用寄存器中的值可能为零,也可能是某个非零整数。但它永远不能为空。即使将内存地址加载到寄存器中,在大多数常见的体系结构中,也没有单独的表示“空指针”。这是在高级语言(如 C)中引入的概念。
440赞 Brian 10/22/2010 #3

我认为为什么 null 是不可取的简洁总结是,无意义的状态不应该是可表示的。

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

class Door
    private bool isShut
    private bool isLocked

很清楚如何将我的三种状态映射到这两个布尔变量中。但这留下了第四个不需要的状态:。因为我选择的类型作为我的表示形式允许这种状态,所以我必须花费精力来确保类永远不会进入这种状态(也许通过显式编码一个不变量)。相反,如果我使用一种具有代数数据类型或已检验枚举的语言,则可以定义isShut==false && isLocked==true

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

然后我可以定义

class Door
    private DoorState state

而且没有更多的后顾之忧。类型系统将确保 的实例只有三种可能的状态。这就是类型系统所擅长的——在编译时明确排除一整类错误。class Door

问题在于,每个引用类型在其空间中都会获得这种通常不需要的额外状态。变量可以是任何字符序列,也可以是这个疯狂的额外值,它没有映射到我的问题域中。一个对象有三个 s,它们本身有 和 值,但不幸的是,s 或 本身可能是这个疯狂的 null 值,对我正在研究的图形域毫无意义。等。nullstringnullTrianglePointXYPointTriangle

当您确实打算对可能不存在的值进行建模时,您应该明确选择加入它。如果我打算对人进行建模的方式是每个人都有 a 和 a,但只有一些人有 s,那么我想这样说PersonFirstNameLastNameMiddleName

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

其中假定此处为不可为 null 的类型。然后,在尝试计算某人名字的长度时,没有棘手的不变量需要建立,也没有意外的 s。类型系统确保任何处理帐户的代码都有可能,而任何处理 的代码都可以安全地假设那里有一个值。stringNullReferenceExceptionMiddleNameNoneFirstName

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

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

不用担心。相反,在对字符串等类型具有可为 null 引用的语言中,则假设

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 对象没有所有内容都为非 null 的不变性,则该对象会爆炸,或者

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

假设确保第一个/最后一个存在,但中间可以为 null,或者您可能执行抛出不同类型的异常的检查,或者谁知道是什么。所有这些疯狂的实现选择和需要考虑的事情都会出现,因为有你不想要或不需要的愚蠢的可表示值。p

Null 通常会增加不必要的复杂性。复杂性是所有软件的敌人,您应该在合理的情况下努力降低复杂性。

(请注意,即使是这些简单的例子也更加复杂。即使 a 不能 ,a 也可以表示(空字符串),这可能也不是我们打算建模的人名。因此,即使使用不可为 null 的字符串,我们仍然可能“表示无意义的值”。同样,你可以选择在运行时通过不变量和条件代码,或者使用类型系统(例如,有一个类型)来解决这个问题。后者可能是不明智的(“好”类型通常被“关闭”在一组常见操作上,例如 不是封闭的),但它在设计空间中展示了更多的点。归根结底,在任何给定的类型系统中,都有一些复杂性是它非常擅长摆脱的,而其他复杂性则本质上是更难摆脱的。本主题的关键是,在几乎每个类型系统中,从“默认可为 null 的引用”到“默认不可为 null 的引用”的更改几乎总是一个简单的更改,它使类型系统在应对复杂性和排除某些类型的错误和无意义状态方面做得更好。因此,如此多的语言一次又一次地重复这个错误是非常疯狂的。FirstNamenullstring""NonEmptyStringNonEmptyString.SubString(0,0)

评论

31赞 Brian 10/22/2010
回复:名称 - 确实。也许您确实关心对一扇敞开但锁门栓伸出的门进行建模,以防止门关闭。世界上有很多复杂性。关键是在软件中实现“世界状态”和“程序状态”之间的映射时不要增加复杂性
61赞 Joshua 10/22/2010
什么,你从来没有锁过门?
59赞 akaphenom 10/23/2010
我不明白为什么人们会为特定领域的语义而烦恼。Brian 以简洁明了的方式用 null 表示缺陷,是的,他在示例中简化了问题域,说每个人都有名字和姓氏。布莱恩,这个问题得到了“T”的回答——如果你曾经在波士顿,我欠你一杯啤酒,因为你在这里所做的所有帖子!
67赞 Brian 10/23/2010
@akaphenom:谢谢,但请注意,并非所有人都喝啤酒(我不喝酒)。但我很欣赏你只是为了表达感激之情而使用一个简化的世界模型,所以我不会对你的世界模型的有缺陷的假设进行更多的狡辩。:P(现实世界太复杂了! :) )
4赞 comonad 10/24/2010
奇怪的是,这个世界上有三扇门!它们在一些酒店被用作厕所门。按钮从内部充当钥匙,从外部锁定门。一旦闩锁螺栓移动,它就会自动解锁。
68赞 jalf 10/22/2010 #4

选项类型的好处不在于它们是可选的。所有其他类型都不是

有时,我们需要能够表示一种“空”状态。有时我们必须表示一个“无值”选项以及变量可能采用的其他可能值。因此,完全不允许这样做的语言会有点残缺。

通常,我们不需要它,允许这样的“null”状态只会导致歧义和混淆:每次我在 .NET 中访问引用类型变量时,我都必须考虑它可能是 null

通常,它实际上永远不会为空,因为程序员对代码进行结构化,使其永远不会发生。但是编译器无法验证这一点,每次看到它时,你都必须问自己“这会是空的吗?我需要在这里检查空吗?

理想情况下,在许多 null 没有意义的情况下,不应允许它

这在 .NET 中很难实现,因为在 .NET 中,几乎所有内容都可能为 null。你必须依靠你所调用的代码的作者是 100% 有纪律和一致的,并清楚地记录了什么可以是空的,什么不能是空的,或者你必须偏执并检查一切

但是,如果类型在默认情况下不可为 null,则无需检查它们是否为 null。您知道它们永远不能为 null,因为编译器/类型检查器会为您强制执行。

然后,对于我们确实需要处理 null 状态的极少数情况,我们只需要一个后门。然后可以使用“选项”类型。然后,当我们有意识地决定需要能够表示“无值”情况时,我们允许 null,并且在所有其他情况下,我们知道该值永远不会为 null。

正如其他人所提到的,例如,在 C# 或 Java 中,null 可能表示以下两种情况之一:

  1. 变量未初始化。理想情况下,这应该永远不会发生。除非初始化变量,否则变量不应存在
  2. 该变量包含一些“可选”数据:它需要能够表示没有数据的情况。这有时是必要的。也许您正在尝试在列表中查找一个对象,但您事先不知道它是否存在。然后,我们需要能够表示“未找到对象”。

第二种含义必须保留,但第一种含义应该完全消除。甚至第二种含义也不应该是默认值。如果需要,我们可以选择加入。但是,当我们不需要某些东西是可选的时,我们希望类型检查器保证它永远不会为空。

评论

1赞 Ohad Schneider 12/5/2017
在第二种含义中,我们希望编译器警告(停止?)我们,如果我们尝试访问这些变量而不先检查无效。这是一篇关于即将推出的 null/non-null C# 功能的精彩文章(终于!blogs.msdn.microsoft.com/dotnet/2017/11/15/......
39赞 tc. 10/22/2010 #5

由于人们似乎错过了它:是模棱两可的。null

爱丽丝的出生日期是。这是什么意思?null

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

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


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

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

答案通常分别是“是”、“否”、“是”、“是”、“否”、“是”。疯狂的“数学家”称NaN为“无效性”,并说它与自身相等。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 中的工作方式,我可以插入 and 并且不违反第一个唯一性约束(因为)。(1, 2, NULL, 3, t)(1, 2, NULL, 4, t)(1, 2, NULL) != (1, 2, NULL)

这之所以有效,是因为 NULL 在大多数数据库的唯一性约束下工作(大概是为了更容易模拟“真实世界”的情况,例如,没有两个人可以拥有相同的社会安全号码,但并非所有人都有一个)。


FWIW,如果不首先调用未定义的行为,C++ 引用就无法“指向”null,并且不可能使用未初始化的引用成员变量构造类(如果引发异常,则构造失败)。

旁注:有时您可能需要互斥指针(即其中只有一个可以是非 NULL),例如在假设的 iOS 中。相反,我被迫做类似的事情。type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissedassert((bool)actionSheet + (bool)alertView == 1)

评论

0赞 Noldorin 8/21/2012
不过,请放心,真正的数学家不会使用“NaN”的概念。
0赞 I. J. Kennedy 8/25/2012
@Noldorin:确实如此,但他们使用“不确定形式”一词。
0赞 Noldorin 8/25/2012
@I.J.肯尼迪:那是一所不同的大学,我非常了解,谢谢。一些“NaN”可能代表不确定的形式,但由于 FPA 不进行符号推理,因此将其等同于不确定形式是相当具有误导性的!
0赞 cat 6/23/2016
怎么了?或者你的语言不能XOR布尔斯?assert(actionSheet ^ alertView)
0赞 Joshua 10/22/2010 #6

向量语言有时可以避免没有 null。

在本例中,空向量用作类型化的 null。

评论

0赞 Roman A. Taycher 10/31/2010
我想我明白你在说什么,但你能举一些例子吗?特别是将多个函数应用于可能的 null 值?
0赞 Joshua 11/1/2010
好吧,将向量变换应用于空向量会导致另一个空向量。仅供参考,SQL主要是一种向量语言。
1赞 Joshua 11/1/2010
好的,我最好澄清一下。SQL 是行的向量语言和列的值语言。
4赞 Corbin March 10/22/2010 #7

罗伯特·尼斯特罗姆(Robert Nystrom)在这里提供了一篇不错的文章:

http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/

描述了他在喜鹊编程语言中添加对缺席和失败的支持时的思维过程。

1赞 Jon 10/26/2010 #8

我一直认为 Null(或 nil)是没有的。

有时你想要这个,有时你不想要。这取决于您正在使用的域。如果缺席是有意义的:没有中间名,那么你的应用程序可以采取相应的行动。另一方面,如果 null 值不应该存在:名字为 null,则开发人员会接到众所周知的凌晨 2 点的电话。

我还看到代码过载且检查 null 过于复杂。对我来说,这意味着两件事之一:
a)应用程序树
中更高的错误b)糟糕/不完整的设计

从积极的一面来看,Null 可能是检查是否缺少某些东西的更有用的概念之一,而没有 null 概念的语言在进行数据验证时最终会使事情变得过于复杂。在这种情况下,如果未初始化新变量,则所述语言通常会将变量设置为空字符串、0 或空集合。但是,如果空字符串或 0 或空集合是应用程序的有效值,则说明存在问题。

有时,通过为字段发明特殊/奇怪的值来表示未初始化的状态来规避这种情况。但是,当善意的用户输入特殊值时会发生什么?我们不要陷入数据验证例程的混乱之中。 如果语言支持空概念,那么所有问题都会消失。

评论

0赞 Stephen Swensen 10/26/2010
嗨,@Jon,在这里跟着你有点难。我终于意识到,你所说的“特殊/奇怪”值可能是指 Javascript 的“undefined”或 IEEE 的“NaN”。但除此之外,你并没有真正解决 OP 提出的任何问题。“Null 可能是检查某些东西是否不存在的最有用的概念”的说法几乎可以肯定是错误的。选项类型是 null 的一种备受推崇的类型安全替代方法。
0赞 Jon 10/27/2010
@Stephen - 实际上回顾我的信息,我认为整个下半场应该移到一个尚未被问到的问题上。但我仍然说 null 对于检查是否缺少某些东西非常有用。
45赞 Kevin Wright 10/28/2010 #9

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

然后,他们继续建议,如果对所有值强制执行不可空性,那将是一个非常巧妙的想法,如果您添加一个概念,例如 或 来表示可能并不总是具有定义值的类型,则可以做到这一点。这就是 Haskell 所采用的方法。OptionMaybe

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

问。那么,除了能够从语言中完全删除空值之外,还有什么好处呢?

一个。组成

如果对 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
}

没有太大区别!但这也是使用选项的一种糟糕的方式......这种方法要简洁得多:

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

当您开始处理选项列表时,它会变得更好。想象一下,列表本身是可选的: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)

带有 null 检查(甚至 elvis ?: 运算符)的相应代码会很长。这里真正的诀窍是 flatMap 操作,它允许以可为 null 的值永远无法实现的方式嵌套理解 Options 和集合。

评论

8赞 C. A. McCann 10/30/2010
+1,这是一个很好的强调点。一个附录:在 Haskell 的土地上,将被称为 ,即单子的“绑定”运算符。没错,Haskellers 非常喜欢 ping 东西,以至于我们把它放在我们语言的标志中。flatMap(>>=)flatMap
1赞 10/31/2010
+1 希望 的表达式永远不会为空。可悲的是,Scala 是呃,仍然链接到 Java :-)(另一方面,如果 Scala 不能很好地与 Java 配合使用,谁会使用它?O.o)Option<T>
0赞 Kevin Wright 10/31/2010
很简单:'List(null).headOption'。请注意,这与返回值“None”的含义截然不同
4赞 Roman A. Taycher 10/31/2010
我给了你赏金,因为我真的很喜欢你说的关于作文的话,其他人似乎没有提到。
0赞 thSoft 8/30/2011
优秀的答案和很好的例子!
5赞 Jahan Zinedine 10/31/2010 #10

Microsoft Research有一个名为

规范#

它是一个具有非 null 类型的 C# 扩展,并且具有某种机制来检查对象是否为 null,尽管恕我直言,应用合同设计原则可能更合适,并且对于由 null 引用引起的许多麻烦情况更有帮助。

4赞 nawfal 2/4/2013 #11

来自.NET背景,我一直认为null是有道理的,它很有用。直到我了解了结构体,以及使用它们是多么容易,避免了大量的样板代码。2009 年,Tony Hoare 在伦敦 QCon 上发表演讲,为发明空引用而道歉。引用他的话:

我称之为我十亿美元的错误。这是 null 的发明 1965年参考。当时,我正在设计第一个 面向对象中引用的综合类型系统 语言 (ALGOL W)。我的目标是确保所有引用的使用 应该是绝对安全的,检查由自动执行 编译器。但我无法抗拒输入空的诱惑 参考,仅仅是因为它很容易实现。这导致了 无数的错误、漏洞和系统崩溃,这些错误、漏洞和系统崩溃 在过去的四十年里,可能造成了十亿美元的痛苦和损害 年。近年来,许多程序分析仪,如 PREfix 和 Microsoft 中的 PREfast 已用于检查引用,并给出 如果存在风险,则警告可能为非 null。最近 像 Spec# 这样的编程语言引入了 非 null 引用。这是我在 1965 年拒绝的解决方案。

在程序员那里也可以看到这个问题