当程序员经常抱怨null错误/异常时,有人会问没有null我们该怎么办。
我对选项类型的酷有一些基本的概念,但我没有最好地表达它的知识或语言技能。对于下面的内容,什么样的解释对普通程序员来说是可以理解的呢?
默认情况下,不希望引用/指针为空 选项类型如何工作,包括简化检查空情况的策略,如 模式匹配和 单细胞生物的理解 替代解决方案,如消息吃nil (我错过的其他方面)
当程序员经常抱怨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)。