当程序员经常抱怨null错误/异常时,有人会问没有null我们该怎么办。
我对选项类型的酷有一些基本的概念,但我没有最好地表达它的知识或语言技能。对于下面的内容,什么样的解释对普通程序员来说是可以理解的呢?
默认情况下,不希望引用/指针为空 选项类型如何工作,包括简化检查空情况的策略,如 模式匹配和 单细胞生物的理解 替代解决方案,如消息吃nil (我错过的其他方面)
当程序员经常抱怨null错误/异常时,有人会问没有null我们该怎么办。
我对选项类型的酷有一些基本的概念,但我没有最好地表达它的知识或语言技能。对于下面的内容,什么样的解释对普通程序员来说是可以理解的呢?
默认情况下,不希望引用/指针为空 选项类型如何工作,包括简化检查空情况的策略,如 模式匹配和 单细胞生物的理解 替代解决方案,如消息吃nil (我错过的其他方面)
当前回答
选项类型的好处并不在于它们是可选的。而是所有其他类型的人都不是。
有时,我们需要能够表示一种“null”状态。有时我们必须表示一个“无值”选项以及变量可能接受的其他可能值。因此,一种完全不允许这一点的语言将会有点残缺。
但通常情况下,我们并不需要它,并且允许这样的“null”状态只会导致歧义和混乱:每次在. net中访问引用类型变量时,我都必须考虑它可能是空的。
通常情况下,它实际上永远不会为空,因为程序员构造的代码使它永远不会发生。但是编译器不能验证它,每次你看到它,你都要问自己“这个可以为空吗?”我需要检查这里是否为空吗?”
理想情况下,在许多情况下,null没有意义,它不应该被允许。
在. net中实现这一点很棘手,因为几乎所有东西都可以为空。您必须依赖所调用代码的作者100%遵守纪律和一致,并清楚地记录了什么可以为空,什么不可以为空,否则您必须偏执狂,检查所有内容。
但是,如果默认情况下类型不能为空,则不需要检查它们是否为空。你知道它们永远不可能为空,因为编译器/类型检查器会强制你这样做。
然后,我们只需要一个后门,在极少数情况下,我们确实需要处理一个空状态。然后可以使用“选项”类型。然后,在我们有意识地决定我们需要能够表示“无值”的情况下,我们允许null,而在其他任何情况下,我们知道值永远不会为null。
正如其他人所提到的,在c#或Java中,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)。
我总是把Null(或nil)看作是一个值的缺失。
有时你想这样,有时又不想。这取决于您正在处理的域。如果缺少中间名是有意义的:没有中间名,那么您的应用程序可以相应地执行。另一方面,如果不应该有空值:第一个名字是空的,那么开发人员会在凌晨2点接到电话。
我还见过代码因检查null而超载和过于复杂。对我来说,这意味着两件事之一: A)在应用程序树中更高的错误 B)糟糕的/不完整的设计
从积极的方面来看——Null可能是检查是否缺少某些东西的更有用的概念之一,没有Null概念的语言在进行数据验证时最终会使事情过于复杂。在这种情况下,如果新变量没有初始化,所述语言通常会将变量设置为空字符串0或空集合。但是,如果空字符串或0或空集合是应用程序的有效值——那么就有问题了。
有时,通过为字段创建特殊/奇怪的值来表示未初始化的状态,可以避免这种情况。但是当一个好心的用户输入特殊值时会发生什么呢?让我们不要陷入数据验证例程的混乱。 如果语言支持空概念,那么所有的关注点都将消失。
选项类型的好处并不在于它们是可选的。而是所有其他类型的人都不是。
有时,我们需要能够表示一种“null”状态。有时我们必须表示一个“无值”选项以及变量可能接受的其他可能值。因此,一种完全不允许这一点的语言将会有点残缺。
但通常情况下,我们并不需要它,并且允许这样的“null”状态只会导致歧义和混乱:每次在. net中访问引用类型变量时,我都必须考虑它可能是空的。
通常情况下,它实际上永远不会为空,因为程序员构造的代码使它永远不会发生。但是编译器不能验证它,每次你看到它,你都要问自己“这个可以为空吗?”我需要检查这里是否为空吗?”
理想情况下,在许多情况下,null没有意义,它不应该被允许。
在. net中实现这一点很棘手,因为几乎所有东西都可以为空。您必须依赖所调用代码的作者100%遵守纪律和一致,并清楚地记录了什么可以为空,什么不可以为空,否则您必须偏执狂,检查所有内容。
但是,如果默认情况下类型不能为空,则不需要检查它们是否为空。你知道它们永远不可能为空,因为编译器/类型检查器会强制你这样做。
然后,我们只需要一个后门,在极少数情况下,我们确实需要处理一个空状态。然后可以使用“选项”类型。然后,在我们有意识地决定我们需要能够表示“无值”的情况下,我们允许null,而在其他任何情况下,我们知道值永远不会为null。
正如其他人所提到的,在c#或Java中,null可能意味着以下两种情况之一:
变量未初始化。理想情况下,这种情况不应该发生。一个变量不应该存在,除非它被初始化。 变量包含一些“可选”数据:它需要能够表示没有数据的情况。这有时是必要的。也许你试图在一个列表中找到一个对象,而你事先不知道它是否在那里。然后我们需要能够表示“没有找到对象”。
第二种意思必须保留,但第一种意思应该完全消除。甚至第二种意思也不应该是默认的。如果我们需要,我们可以选择加入。但当我们不需要某些东西为可选时,我们希望类型检查器保证它永远不会为空。
到目前为止,所有的答案都集中在为什么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和集合。
我认为为什么空是不可取的简单总结是,无意义的状态不应该是可表示的。
假设我在建模一扇门。它可以处于三种状态之一:打开、关闭但未解锁、关闭并锁定。现在我可以用
class Door
private bool isShut
private bool isLocked
如何将三个状态映射到这两个布尔变量是很清楚的。但这就留下了第四种不希望出现的状态:isShut==false && isLocked==true。因为我所选择的类型承认这种状态,所以我必须花费精力来确保类永远不会进入这种状态(可能通过显式地编码一个不变量)。相反,如果我使用一种具有代数数据类型的语言或允许我定义的受控枚举
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
那么我就可以定义
class Door
private DoorState state
再也没有什么担心了。类型系统将确保类Door的实例只有三种可能的状态。这正是类型系统所擅长的——在编译时显式地排除了一整类错误。
null的问题在于,每个引用类型都会在其空间中获得这个通常不希望的额外状态。字符串变量可以是任意字符序列,也可以是这个疯狂的额外空值,它没有映射到我的问题域。三角形对象有三个点,它们本身有X和Y值,但不幸的是,这些点或三角形本身可能是这个疯狂的空值,对我所处的绘图域毫无意义。等。
当您确实打算对一个可能不存在的值建模时,那么您应该显式地选择它。如果我打算对人建模的方式是每个人都有一个名字和一个姓,但只有一些人有中间名,那么我想说一些像这样的话
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
这里的string被假定为非空类型。这样,在尝试计算某人名字的长度时,就不需要建立棘手的不变量,也不会出现意外的nullreferenceexception。类型系统确保任何处理midlename的代码都考虑到它为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
如果传入的Person对象没有所有东西都是非空的不变式,那么它就会爆炸,或者
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确保first/last在那里,但middle可以为空,或者你可能会检查抛出不同类型的异常,或者谁知道呢。所有这些疯狂的实现选择和需要考虑的事情突然出现,因为有这种愚蠢的可代表价值,你不想要或不需要。
Null通常会增加不必要的复杂性。复杂性是所有软件的敌人,您应该在合理的情况下努力降低复杂性。
(Note well that there is more complexity to even these simple examples. Even if a FirstName cannot be null, a string can represent "" (the empty string), which is probably also not a person name that we intend to model. As such, even with non-nullable strings, it still might be the case that we are "representing meaningless values". Again, you could choose to battle this either via invariants and conditional code at runtime, or by using the type system (e.g. to have a NonEmptyString type). The latter is perhaps ill-advised ("good" types are often "closed" over a set of common operations, and e.g. NonEmptyString is not closed over .SubString(0,0)), but it demonstrates more points in the design space. At the end of the day, in any given type system, there is some complexity it will be very good at getting rid of, and other complexity that is just intrinsically harder to get rid of. The key for this topic is that in nearly every type system, the change from "nullable references by default" to "non-nullable references by default" is nearly always a simple change that makes the type system a great deal better at battling complexity and ruling out certain types of errors and meaningless states. So it is pretty crazy that so many languages keep repeating this error again and again.)