每隔一段时间,当程序员抱怨空错误/异常时,有人问我们在没有零的情况下做什么。

我对期权类型的凉爽有一些基本概念,但是我没有最能表达它的知识或语言技能。什么是 伟大的 用以下方式解释了以下书面的方式,我们可以指向我们可以指向该人的普通程序员?

  • 默认情况下,没有参考/指示器的不良性是无效的
  • 选项类型如何工作,包括放松检查无效案件的策略,例如
    • 图案匹配和
    • 单一的综合
  • 替代解决方案,例如消息零食
  • (我错过的其他方面)
有帮助吗?

解决方案

我认为为什么无效的原因是 毫无意义的状态不应代表.

假设我正在建模一扇门。它可以在三个状态之一中:打开,关闭但解锁并关闭并锁定。现在我可以按照

class Door
    private bool isShut
    private bool isLocked

很明显,如何将我的三个状态映射到这两个布尔变量中。但这留下了第四个不希望的状态: isShut==false && isLocked==true. 。因为我选择的类型作为我的代表承认这种状态,所以我必须花费心理努力,以确保班级永远不会进入该状态(也许是通过明确编码不变的)。相反,如果我使用的是具有代数数据类型的语言或列举的枚举,可以定义我

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

那我可以定义

class Door
    private DoorState state

而且不再担心。类型系统将确保一个实例只有三个可能的状态 class Door 进入。这是哪种类型系统擅长 - 在编译时间时明确排除一类错误。

问题 null 是否每种参考类型都在其空间中获得这种额外的状态,通常是不希望的。一个 string 变量可能是任何字符的序列,也可能是这个疯狂的额外 null 值不会映射到我的问题域。一个 Triangle 对象有三个 PointS,本身 XY 值,但不幸的是 PointS或 Triangle 本身可能是这个疯狂的零值,对于我正在工作的图形域而言毫无意义。

当您打算建模可能存在的值时,您应该明确选择它。如果我打算为人建模的方式是每个人 Person 有个 FirstNameLastName, ,但是只有一些人 MiddleNameS,然后我想说的话

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

不用担心。相比之下,用字符串类型的语言具有无效的引用,然后假设

class Person
    private string FirstName
    private string MiddleName
    private string LastName

您最终创作了像

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

如果传入的人对象没有任何事物的不变性,或者

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 确保第一/最后一个存在,但是中间可以是无效的,或者您确实检查了会丢弃不同类型的异常或知道什么。所有这些疯狂的实施选择和要考虑的事情,因为您不需要或不需要的愚蠢的代表性价值。

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

(请注意,即使是这些简单的示例也更加复杂。 FirstName 不可能是 null, , 一个 string 可以代表 "" (空字符串),这可能也不是我们打算建模的人名称。因此,即使使用了不可解开的字符串,我们也可能“代表毫无意义的价值”。同样,您可以选择通过不变式和有条件的代码在运行时或使用类型系统(例如 NonEmptyString 类型)。后者也许是不明智的(“良好”类型通常是在一组普通操作上“封闭的”,例如 NonEmptyString 没有关闭 .SubString(0,0)),但它在设计空间中显示了更多点。归根结底,在任何给定类型的系统中,都会有一些复杂性,它将非常擅长摆脱,而其他复杂性在本质上更难以摆脱。这个话题的关键是几乎 每一个 类型系统,从“默认情况下的无效引用”更改为“默认情况下的不可删除的引用”几乎总是一个简单的更改,它使该类型系统在与复杂性作斗争并排除某些类型的错误和毫无意义的状态方面更好。因此,如此多的语言不断一次又一次地重复此错误是非常疯狂的。

其他提示

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

有时, ,我们需要能够代表一种“零”状态。有时,我们必须表示变量可能采用的“无值”选项以及其他可能的值。因此,一种扁平化的语言会有些残废。

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

通常,它永远不会 实际上 为null,因为程序员会构造代码,以免它发生。但是编译器无法验证这一点,每当您看到它时,您都必须问自己:“这可以是空的吗?我需要在这里检查null吗?”

理想情况下,在许多情况下无意义的情况下, 不应该允许.

在.NET中实现这一点很棘手,几乎所有内容都可以无效。您必须依靠您所要求的代码的作者才能获得100%的纪律和一致,并清楚地记录了什么可以和不能无效的,否则您必须偏执并检查 一切.

但是,如果类型不能无效 默认, ,那么您无需检查它们是否为空。您知道它们永远不会是无效的,因为编译器/类型的Checker为您强制执行。

然后,我们只需要在极少数情况下的后门 需要处理无效状态。然后可以使用“选项”类型。然后,在我们做出有意识地决定我们需要能够表示“无价”案例的情况下,我们允许NULL,并且在其他所有情况下,我们都知道该值永远不会为null。

正如其他人所提到的,例如,在C#或Java中,Null可能意味着两件事之一:

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

必须保留第二个含义,但应完全消除第一个含义。甚至第二个含义也不应该是默认值。这是我们可以选择的 如果以及我们需要的时候. 。但是,当我们不需要可选的东西时,我们希望类型的检查器 保证 它永远不会是无效的。

到目前为止,所有答案都集中在为什么 null 是一件坏事,如果语言能够保证某些价值观将会有些方便 绝不 为null。

然后,他们继续暗示,如果您强制执行非删除性,那将是一个非常整洁的主意 全部 值,如果您添加类似的概念,可以做到 Option 或者 Maybe 表示可能并不总是具有定义值的类型。这是哈斯克尔采取的方法。

都是好东西!但这并不排除使用明确无效 /非壁画类型的使用来实现相同的效果。那么,为什么选择仍然是一件好事?毕竟,scala支持无效的值(IS 到,因此可以与Java库一起使用),但支持 Options 也是。

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

一个。 作品

如果您从无效代码中进行幼稚的翻译

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)

带有空检查(甚至猫王?:操作员)的相应代码会很长。这里真正的窍门是Flatmap操作,它允许以无效的值永远无法实现的方式对选项和集合进行嵌套理解。

由于人们似乎缺少它: null 是模棱两可的。

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

鲍勃的死亡日期是 null. 。这意味着什么?

“合理的”解释可能是爱丽丝的出生日期存在,但尚不清楚,而鲍勃的死亡日期不存在(鲍勃仍然活着)。但是,为什么我们要找到不同的答案?


另一个问题: null 是边缘案例。

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

答案是 通常 “是”,“否”,“是”,“是”,“否”,“是”。疯狂的“数学家”称Nan“无效”,并说它与自身相比。 SQL将无效视为不等于任何东西(因此它们的行为就像NAN)。一个人想知道当您尝试将±∞,±0和NAN存储到同一数据库列时会发生什么(有2个53 NAN,一半是“负”)。

更糟糕的是,数据库在处理零的方式上有所不同,大多数数据库并不一致(请参阅 sqlite中的无效处理 概述)。太可怕了。


现在是必不可少的故事:

我最近设计了一个带有五列的(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)).

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


fwiw,如果没有先调用未定义的行为,C ++引用不能“指向” null,并且不可能构建具有非直接参考成员变量的类(如果引发异常,构造失败)。

Sidenote:有时您可能需要相互排他性的指针(即只有一个可以是无效的),例如在假设的iOS中 type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. 。相反,我被迫做这样的事情 assert((bool)actionSheet + (bool)alertView == 1).

默认情况下,拥有参考/指针的不良性是无效的。

我认为这不是零件的主要问题,无效的主要问题是,它们可以意味着两件事:

  1. 参考/指针是非初始化的:这里的问题与一般的Mutability相同。首先,这使得分析您的代码更加困难。
  2. 变量为null实际上意味着某种内容:这种选择类型实际上是正式化的情况。

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

选项类型如何工作,包括减轻检查无效案例(例如模式匹配)的策略。

为了有效,需要直接在语言中支持期权类型。否则,需要大量锅炉板代码来模拟它们。模式匹配和类型 - 引入是两个键语言功能,使选项类型易于使用。例如:

在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这样的语言而没有直接支持选项类型,我们将有类似的东西:

//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());

替代解决方案,例如消息零食

Objective-C的“食物零食”并不是一种解决方案,而是试图减轻无效检查的头痛。基本上,该表达式评估为null本身,而不是尝试在空对象上调用方法时,而不是抛出运行时异常。暂停怀疑,好像每个实例方法都从 if (this == null) return null;. 。但是,就会有信息丢失:您不知道该方法是否返回null是因为它是有效的返回值,或者因为对象实际上是null。这很像例外吞咽,并且没有任何进展来解决以前概述的无效问题。

大会给我们带来了也称为未型指针的地址。 C将它们直接映射为打字指针,但将Algol的空物作为独特的指针值,与所有打字指针兼容。 C中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中,您可能只是忘记了无效的支票和繁荣...

注意:这些未编译的代码样本,但我希望您能得到这些想法。

Microsoft Research有一个名为

规格#

这是一个c#扩展 非无效类型 还有一些机制 检查您的对象,以防止无效, ,虽然,恕我直言,应用 通过合同设计 原理可能对因无效引用引起的许多麻烦情况而更合适,更有帮助。

来自.NET背景,我一直认为Null有一点,它有用。直到我知道结构以及与它们一起避免使用大量样板代码的容易性。 托尼·霍尔(Tony Hoare) 2009年在伦敦QCON发言 为发明零参考而道歉. 。引用他:

我称它为我十亿美元的错误。这是1965年无效参考的发明。当时,我正在设计第一个以对象为导向语言(Algol W)的综合类型系统。我的目标是确保所有参考文献的使用都应绝对安全,并且编译器会自动执行检查。但是我忍不住诱惑将其引用为零参考,仅仅是因为它很容易实施。这导致了无数的错误,漏洞和系统崩溃,在过去的四十年中可能造成了十亿美元的痛苦和损害。近年来,Microsoft的前缀和Prefast等许多程序分析仪已被用于检查参考文献,并发出警告,如果有风险,它们可能是无效的。诸如Spec#之类的最新编程语言已引入了非无编号引用的声明。这是我在1965年拒绝的解决方案。

也看到这个问题 在程序员

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

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

在增加对缺席和失败的支持时描述他的思维过程 编程语言。

我一直把null(或零)视为 缺乏价值.

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

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

在积极方面 - 零可能是检查是否缺乏某些东西的最有用的概念之一,而没有null概念的语言在进行数据验证时会导致过度复杂的事物。在这种情况下,如果未初始化新变量,则说Languagues通常将变量设置为空字符串,0或空集合。但是,如果一个空字符串或0或空的集合为 有效值 对于您的应用程序 - 然后您有问题。

有时,通过发明特殊/怪异值来代表非初始化状态来避免这种情况。但是,当特殊的用户输入特殊值时会发生什么?而且,我们不要陷入混乱,这将使数据验证例程构成。如果语言支持无效的概念,那么所有问题将消失。

矢量语言有时可能没有零。

在这种情况下,空矢量用作打字的空。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top