当程序员经常抱怨null错误/异常时,有人会问没有null我们该怎么办。

我对选项类型的酷有一些基本的概念,但我没有最好地表达它的知识或语言技能。对于下面的内容,什么样的解释对普通程序员来说是可以理解的呢?

默认情况下,不希望引用/指针为空 选项类型如何工作,包括简化检查空情况的策略,如 模式匹配和 单细胞生物的理解 替代解决方案,如消息吃nil (我错过的其他方面)


当前回答

程序集为我们带来了地址,也称为无类型指针。C语言直接将它们映射为类型化指针,但引入Algol的null作为唯一指针值,与所有类型化指针兼容。在C语言中,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语言中,你可能只是忘记了一个空检查,然后……

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

其他回答

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

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

这些都是好东西!但它并不排除使用显式可空/非空类型来实现相同的效果。那么,为什么Option仍然是一个好东西呢?毕竟,Scala支持可空值(这是必须的,所以它可以与Java库一起工作),但也支持选项。

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

答:作文

如果从支持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

当你开始处理List of Options时,它会变得更好。假设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操作,它允许以一种可空值永远无法实现的方式嵌套理解Options和集合。

我总是把Null(或nil)看作是一个值的缺失。

有时你想这样,有时又不想。这取决于您正在处理的域。如果缺少中间名是有意义的:没有中间名,那么您的应用程序可以相应地执行。另一方面,如果不应该有空值:第一个名字是空的,那么开发人员会在凌晨2点接到电话。

我还见过代码因检查null而超载和过于复杂。对我来说,这意味着两件事之一: A)在应用程序树中更高的错误 B)糟糕的/不完整的设计

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

有时,通过为字段创建特殊/奇怪的值来表示未初始化的状态,可以避免这种情况。但是当一个好心的用户输入特殊值时会发生什么呢?让我们不要陷入数据验证例程的混乱。 如果语言支持空概念,那么所有的关注点都将消失。

Robert Nystrom在这里提供了一篇不错的文章:

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

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

矢量语言有时可以不使用null。

在本例中,空向量充当类型化null。

因为人们似乎忽略了它:null是模棱两可的。

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

Bob的死亡日期为空。这是什么意思?

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


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

null = null? nan = nan吗? 是否inf = inf? +0 = -0吗? +0/0 = -0/0?

答案通常分别是“是”、“否”、“是”、“是”、“否”、“是”。疯狂的“数学家”称NaN为“零”,并说它比较等于自身。SQL将null视为不等于任何东西(因此它们的行为类似于nan)。有人想知道,当您尝试将±∞、±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在大多数数据库的唯一性约束下是如何工作的(可能是为了更容易模拟“现实世界”的情况,例如没有两个人可以有相同的社会安全号码,但并不是所有人都有一个)。


FWIW,如果不首先调用未定义的行为,c++引用不能“指向”null,并且不可能构造带有未初始化引用成员变量的类(如果抛出异常,构造失败)。

旁注:偶尔你可能想要互斥指针(即只有一个可以是非null),例如在一个假设的iOS类型DialogState = notshow | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView |被驳回。相反,我被迫做一些事情,如assert((bool)actionSheet + (bool)alertView == 1)。