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

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

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


当前回答

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

spec#

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

其他回答

默认情况下,不希望引用/指针为空。

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

引用/指针是未初始化的:这里的问题与一般的可变性相同。首先,它使分析代码变得更加困难。 变量为空实际上意味着一些事情:这是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's "message eating nil" is not so much a solution as an attempt to lighten the head-ache of null checking. Basically, instead of throwing a runtime exception when trying to invoke a method on a null object, the expression instead evaluates to null itself. Suspending disbelief, it's as if each instance method begins with if (this == null) return null;. But then there is information loss: you don't know whether the method returned null because it is valid return value, or because the object is actually null. It's a lot like exception swallowing, and doesn't make any progress addressing the issues with null outlined before.

程序集为我们带来了地址,也称为无类型指针。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。

因为人们似乎忽略了它: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)。

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

spec#

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