为什么是可空类型?

2020-12-08 10:58:51

几周前,我们发布了Dart null安全测试版,这是一项主要的生产力功能,旨在帮助您避免null错误。说到空值,用户最​​近在/ r / dart_lang subreddit中询问:

但是,为什么我们仍然有/想要空值?为什么不彻底摆脱它呢?我目前也在玩Rust,它根本没有null。因此,似乎没有它就可以生存。

我喜欢这个问题。为什么不完全摆脱null?本文是我在该线程上回答的扩展版本。

简短的答案是,是的,完全有可能没有null,Rust这样的语言也可以。但是程序员确实会使用null,因此在我们将其取消之前,我们需要了解为什么使用它。当我们在拥有null的语言中使用null时,通常会做什么?

事实证明,空值通常用于表示缺少值,这非常有用。有些人没有中间名。有些邮寄地址没有公寓号码。杀死某些怪物时,它们不会掉落任何宝藏。

在这种情况下,我们需要一种表达方式,“此变量可能具有类型X的值,或者可能根本没有任何值。”那么问题是我们如何建模呢?

一种选择是说变量可以包含期望类型的值,或者可以包含幻值null。如果尝试使用该值为null的值,则会导致运行时失败。这是Dart在执行null安全之前所做的事情,SQL所做的事情,Java对非原始类型所做的事情以及C#对类类型所做的事情。

但是在运行时失败很糟糕。这意味着我们的用户会遇到该错误。我们程序员宁愿先找到那些失败,然后再去发现。实际上,如果在运行程序之前就可以找到错误,我们将非常高兴。那么,如何以类型系统可以理解的方式对缺少值进行建模?换句话说,我们如何给“可能不存在”的值和“绝对存在”的值提供不同的静态类型?

这就是ML和从ML派生的大多数功能语言(包括Rust,Scala和Swift)所做的事情。当我们知道肯定会有一个值时,我们只使用基础类型。如果我们将int写为int,则表示“这里肯定有一个整数”。

为了表示一个可能不存在的值,我们将基础类型包装在一个选项类型中。因此Option< int>表示一个可能是整数或根本没有值的值。就像一个集合类型,可以包含零个或一个项目。

从类型系统的角度来看,int和Option< int>之间没有方向关系。将它们视为不同的类型意味着我们不会意外地传递可能不存在的Option< int>期待真正的诠释。我们也不能偶然尝试使用Option< int>好像是整数,因为它不支持任何这些操作。我们无法对Option< int>执行算术运算比我们在List< int>上所能提供的更多。

要从基础类型的当前值(例如3)创建选项类型的值,您可以构建类似于Some(3)的选项。要在缺少值时创建选项类型,请编写类似None()的内容。

为了使用存储在Option< int>中的可能不存在的整数,我们必须首先检查并查看该值是否存在。如果是这样,我们可以从选项中提取整数并使用它,就像从集合中读取值一样。具有选项类型的语言通常也具有很好的模式匹配语法,这为我们提供了一种优雅的方式来检查值是否存在,如果有,则使用它。

另一个选项(heh)是Kotlin,TypeScript和现在的Dart所做的。可空类型是联合类型的特殊情况。

(Tangent:这里的命名确实让人感到困惑。选项类型-ML和朋友在上面所做的事情-是代数数据类型的特例。代数数据类型的另一个名称是“歧视联合”。但是,尽管有“工会”的名字, “有区别的工会”与“工会类型”大不相同。正如菲尔·卡尔顿所说的那样,计算机科学中只有两个难题:缓存失效和事物命名。)

与期权类型方法类似,我们使用基础类型表示绝对现值。所以int再次表示我们绝对有一个整数。如果我们想要一个可能不存在的整数,则使用int吗?可为null的类型。小问号是语法糖,用于编写本质上像int |的联合类型。空值。

与选项类型一样,可为空的类型不支持与基础类型相同的操作。类型系统不允许我们尝试对可为null的int进行算术运算,因为那是不安全的。同样,我们无法将可为空的整数传递给需要实际整数的对象。

但是,类型系统比选项类型更具灵活性。类型系统理解联合类型是其分支的超类型。换句话说,int是int?的子类型。这意味着我们可以将肯定存在的整数传递给期望可能存在的整数,因为这样做是安全的。就像我们可以将String传递给采用Object的函数一样,这是一个失败。 Dart仅禁止我们采取其他方式-从可为空到不可为空-因为这将是一个失败的选择,并且可能会失败。

当我们有一个可为null的类型的值并且想要查看是否存在实际值或为null时,我们就必须像在C或Java中那样自然地对值进行强制检查:

然后,该语言使用流程分析来确定程序的哪些部分在这些检查之后受到保护。分析确定只有在变量不为null时才能访问代码,因此在这些区域内,类型系统将变量的类型限制为不可为null。因此,在这里,它将i视为在if语句中具有int类型。

因此,当我们Dart团队决定以一种更安全的方式使语言处理为空时,我们应该如何选择解决方案1或2?我们可以从观察用户开始。他们想如何编写代码来检查缺少的值?在功能语言中,模式匹配是主要的控制流程结构之一,用户对此非常满意。在这种风格中,使用选项类型和模式匹配是很自然的。

在从C派生的命令式语言中,像我之前的示例一样的代码是检查null的惯用方式。使用流分析和可为空的类型可以使熟悉的代码正确,安全地工作。实际上,借助Dart,我们发现对于新类型的系统,大多数现有代码已经静态地为null安全,因为新的流程分析可以正确分析已编写的代码。

(这在某些方面不足为奇。大多数代码在处理null方面已经是动态正确的。如果不是,那么它将一直崩溃。许多工作只是使类型系统足够智能,以至于可以看到该代码已经正确,因此用户的注意力会吸引到一些不正确的代码上。)

因此,如果我们的目标是最大程度地提高用户的熟悉度和用户舒适度(这是语言设计中的重要标准),那么我们应该遵循我们的语言控制流程结构为我们提供的路径。

根据选项类型和可为空类型的表示方式之间的差异,有一种更深层的方法可以解决此问题。这种表示差异会给我们带来一些关键的权衡,而这些权衡可能会使我们朝一个方向或另一个方向倾斜。

使用第一种方法,选项类型的值具有与基础值不同的运行时表示形式。假设我们在Dart中选择了选项类型,然后您创建了一个选项类型,然后将其向上转换为Object:

注意最后一行。 Option< int>值(即使存在)与基础类型的值也不一样。 Some(3)和3是不同的,可区分的值。

可空类型存在于静态类型系统中,但是值的运行时表示形式使用基础类型。如果您有一个“可空3”,那么在运行时它只是数字3。如果您缺少某个可空类型的值,那么在运行时,您将单独的魔术值设为空。

由于选项类型的值与基础类型不同,因此这为我们提供了一项重要功能:选项类型可以嵌套。

假设我们有一些网络服务,当提供带有整数ID的请求时,该服务会发出资源字符串。某些资源不存在,服务器将不响应该ID的任何数据。由于连接网络的速度很慢,因此我们希望在本地缓存已经执行的请求的结果。

因此,在向网络请求某些ID之前,我们使用缓存地图上的下标运算符来查找资源的ID。如果键不存在,则该操作符在Map上定义为返回null。但是键也可以存在并且与空值关联。如果我们进行查找并返回null,则可能意味着:

钥匙不在地图上。这意味着我们尚未完成请求,因此我们应要求服务器查找资源。

密钥存在并且与null关联。这意味着我们确实已经询问服务器,发现资源不存在,并将其存储在缓存中。我们应该使用该结果,而不要再次查询服务器。

由于整个系统中只有一个null值,因此我们没有可以区分这两种情况的运行时表示形式。这就是Map类具有单独的containsKey()方法的原因。该API提供了区分这两种情况的方法。

在我们的Map< int,Option< String>>的情况下,这意味着返回类型是Option< Option< String>>。注意嵌套!现在,当我们在缓存中查找键时,我们可以得到一些不同的结果:

Some(Some(string))表示资源确实存在于服务器上,现在我们将其保存在缓存中。

Some(None())意味着我们确实询问了服务器并且资源不存在,因此我们缓存了资源不存在的事实。

我们可以区分最后两种情况,因为选项始终将其基础值包装在某些额外状态中。在运行时,我们可以确定有多少层并将其单独剥离。

可空类型,因为它们没有显式的运行时表示形式,因此被隐式拉平。如此诠释?和诠释?是类型系统的等效类型,并且在运行时具有等效的值集。这就是为什么选项类型的爱好者将它们描述为“更具表现力”的原因:因为与可空类型相比,可选类型为您提供了一种表示更多类型的值的方式。

关于“表达能力”的另一种思考方式是,用户表达自己实际想要表达的内容要花费多少精力。如果用户可以通过较少的跳动而达到目标,那么语言将更具表现力。

对于可空类型没有明显表示的优点是,值可以更容易地从不可空上下文流向可空上下文。假设您有一个接受可选整数参数的函数。使用选项类型时,签名将类似于:

要使用已知整数调用此函数,必须先将其包装在选项中:

对于可为空的类型,由于没有表示形式的差异,因此可以直接传递基础类型的值:

您可以在类型系统中的任何地方获得这种灵活性。您可以重写返回可为null的类型的方法以返回不可为null的类型。您可以传递List< int>到需要List< int?>的函数。

因此,尽管可空类型失去嵌套和表示多种不同类型“缺席”的能力,但作为回报,它们却使使用一个受祝福的空概念更加容易。

Dart是一种命令式语言,人们已经在运行时使用if语句检查缺少的值。这也是一种面向对象的语言,我们已经有了一个特殊的null值及其自己的运行时表示形式。因此,解决方案2(可为空的类型)是我们的自然选择。它使我们的用户可以编写他们熟悉的代码类型,并利用运行时已经如何表示值的优势。

有关Dart中的可空性的更多信息,请查看Dart null安全文档的“从哪里了解更多信息”部分。