实用类型系统 (PTS) 中的 Null 安全





5.00/5 (3投票s)
“值的缺失”是类型系统必须处理的最重要概念之一。
目录
注意
这是系列文章《如何设计一个实用类型系统以最大限度地提高软件开发项目的可靠性、可维护性和生产力》的第5部分。
建议(但对于有经验的程序员不是必需的)按出版顺序阅读这些文章,从《第1部分:是什么?为什么?如何?》开始。
请注意,PTS 是一种新的,尚未实现的范式。正如文章《实用类型系统 (PTS) 的本质与基础》的《历史》部分所解释的,PTS 已在一个概念验证项目中实现,但公共的 PTS 实现尚未可用——您无法尝试本文中显示的 PTS 源代码示例。
欲快速回顾前几篇文章,您可以阅读 实践类型系统 (PTS) 文章系列摘要。
引言
“值的缺失”是类型系统必须处理的最重要概念之一——以这样或那样的方式。
考虑这样一个场景:给定订单的交货日期仍未知。在这种情况下,我们不能为delivery_date
分配日期,因为没有可用的日期。我们必须处理“值的缺失”。
大多数软件应用程序必须处理许多类似的情况,其中某些对象引用没有值可用。
因此,一个实用的类型系统应该提供一流的支持来优雅地处理所有情况。处理“值的缺失”应该容易、可靠且可维护。
本文解释了 PTS 如何实现这一点。
处理“值的缺失”的方法
在展示 PTS 如何处理值的缺失之前,我们首先看看常见方法。
注意
只对 PTS 方法感兴趣的读者可以跳过下一节。
常见方法
据我所知,处理值缺失有三种常见方法。
null
许多编程语言使用符号
null
来表示值的缺失。一些语言使用nil
、void
、nothing
、missing
等代替null
,但基本思想是一样的。注意
关于
null
的基本介绍,您可以阅读我的文章《null 快速而全面的指南》。- 空对象模式
在某些语言中,没有原生支持处理值的缺失。
在这种环境下,可以使用空对象模式。维基百科指出:
不是使用
null
引用来表示对象的缺失(例如,不存在的客户),而是使用一个实现了预期接口但其方法体为空的对象。使用空对象的一个主要目的是避免各种条件判断,从而使代码更集中,更容易阅读和理解——即提高了可读性。这种方法相对于一个可工作的默认实现的一个优点是,空对象非常可预测且没有副作用:它什么都不做。应用空对象模式的简单示例是,对于数字使用零,对于
string
类型的对象引用使用空字符串,对于集合类型使用空集合。起初,这似乎是一个很好的解决方案,因为不使用
null
也意味着摆脱了可怕的空指针错误。然而,事实证明,空对象模式带来的问题多于解决的问题,并且使调试更加困难——这是一种穷人的解决方案。有关更多信息和示例,您可以阅读我的文章《为什么我们应该喜欢“null”》中的“使用零而不是null”和“空对象模式”部分。
Option
/Maybe
类型这种方法基于以下前提:
-
类型系统不支持
null
。 -
使用
Option
类型来处理值的缺失。这种类型也被称为Maybe
、Optional
等,但基本思想是一样的。
可以将
Option
/Maybe
看作一个容器,它要么为空,要么包含一个值。大多数
Option
/Maybe
实现还提供了一些在此类型上下文中非常有用的特定函数/方法。在某些语言中,
Option
/Maybe
类型也是一个单子(monad)。例如,Haskell 提供了Maybe单子。注意
有关单子(为不熟悉函数式语言的程序员量身定制)的介绍,您可以阅读我的文章《单子简单介绍 — 附 Java 示例》。
-
PTS 方法
PTS 使用 null
。没错,就是 null
!
这可能令人惊讶,因为现代编程语言倾向于采用 Option
/Maybe
方法。
讨论 null
与 Option
/Maybe
的优缺点超出了本文的范围,但有关深入讨论,您可以阅读我的文章《空安全 vs Maybe/Option — 彻底比较》。简而言之,该文章表明:
-
(如果实现得好)空处理对于软件开发者来说比
Option
/Maybe
更方便和实用,但作为语言的本地特性实现起来更具挑战性。 -
Option
/Maybe
类型在标准库中相对容易实现。
如果在方便“应用程序开发人员”和“语言工程师和编译器开发人员”之间做出选择,那么在 PTS 的上下文中,偏向于“应用程序开发人员”。因此,在 PTS 中,采用null
是一种更好的方法。
选择null
的更多原因已在上一篇文章《实用类型系统中的联合类型》中揭示。
然而,只有在保证“空安全”的情况下,null
才是一种可行的方法,正如下一节所解释的。
为什么我们需要空安全?
历史表明,许多软件应用程序中最常见的错误是臭名昭著的空指针错误,如今它已成为“十亿美元的错误”的代名词,这个术语是由null
的发明者、名誉教授 Tony Hoare 爵士创造的。
曼彻斯特大学计算机科学系的 John Sargeant 教授在他的文章《空指针异常》中这样说道:
在Java程序运行时可能出现的问题中,
null
指针异常是最常见的。— 约翰·萨金特教授
以下是 Java 中空指针异常的示例:
String name = null;
int length = name.length();
运行此代码将报告以下运行时错误,因为方法length()
不能在null
对象上执行:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
...
报告错误后,应用程序立即中止。生产环境中的后果是不可预测的——从无害到灾难性不等。
除了 Java,其他流行的编程语言也容易受到null
指针错误的影响,例如 C、C++、Go、JavaScript、Python 和 Ruby。虽然现代 C# 具有非null
类型和特定的null
检查功能(自版本 8 起),但它仍然允许使用null
引用,这可能导致null
指针错误。
实践表明:
许多用非空安全语言编写的软件应用程序都
充斥着空指针错误,
潜藏在代码中,
等待在未来某个不可预测的时间爆发,
导致不确定的结果,
从无害到代价高昂的灾难不等。
我们需要根除空指针错误。
我们需要空安全。
想一想:空安全**根除了支持null
的语言中最常见的错误**。因此,空安全是一个最受欢迎的特性——是我们追求增强可靠性、可维护性和生产力的关键里程碑。
让我们看看如何在 PTS 中实现空安全。
它是如何工作的?
基本原则
PTS 空安全基于以下原则:
-
PTS 提供了一个内置类型,专门用于表示“值的缺失”。此类型名为
null
,它只有一个值:null
。 -
虽然
null
是null
类型的一个有效值,但对于所有其他类型来说都是无效的。换句话说,所有其他类型都是非空的。例如,一个输入参数类型为
string
的函数不能以null
作为输入来调用。 -
联合类型用于声明可空对象引用(已在上一篇题为《实用类型系统中的联合类型》的文章中介绍过)。
null
可以赋值给可空对象引用。例如,一个输入参数类型为
string or null
的函数可以以null
作为输入来调用。 -
编译器确保只有当对象引用在运行时**保证**为非空时,对对象引用的方法调用才有效。因此,空指针错误不会发生。类型系统是**空安全的**。
例如,
foo.bar()
只有在foo
在运行时保证为非null
时才有效。 -
一组专用的运算符和语句有助于空处理和空安全代码的开发。
这些是基本的设计原则。
现在让我们深入一点。
空类型
null
类型是内置类型。
这种类型在其允许值的集合中只有一个值,也称为null
。因此,null
类型的基数是一。
null
用于表示值的缺失。
例如,赋值:
delivery_date = null
...表示delivery_date
“缺少日期”。没有日期分配给delivery_date
。
注意:
代码
delivery_date = null
并没有告诉我们delivery_date
指向null
的**原因**。原因**可能**是交货日期尚未可用。但原因也**可能**是交货日期尚未输入数据库。null
仅仅意味着**没有可用值**。除非在特定上下文中明确指定了特定含义,否则它不意味着任何其他东西。
类型层级
以下类图显示了 PTS 类型层次结构中的顶级类型:
注意
在类图中,约定是以*斜体*显示不可实例化/抽象类型(稍后解释)。
在层次结构的顶端是类型any
——PTS 中所有类型的根。non_null
和null
类型都继承自any
。所有其他类型都继承自non_null
。
注意
**类型继承**已在之前的 PTS 文章《实用类型系统中的记录类型》的“类型继承”部分进行了简要介绍。本文不涵盖此主题——假定本文读者熟悉类型继承的概念。
现在让我们仔细看看这些类型。
任意类型 (any)
根类型any
的允许值集合为空——其基数为零。简单来说,any
类型没有值。
这意味着无法创建any
类型的对象。对象只能为any
的某些子类型/后代创建。因此,any
类型被称为**不可实例化**(或**抽象**)类型,并在上图中以*斜体*表示。
此外,任何人都不允许定义any
的子类型。any
类型是一种所谓的**密封**类型。它的子类型是固定的:non_null
和null
。
any
的 PTS 源代码如下:
type any \
factories: none \ (1)
child_types: non_null, null (2)
.
(1) 无法创建any
的实例。
(2) non_null
和null
是唯一的子类型。
非空类型 (non_null)
non_null
类型是any
类型的子类型。
non_null
类型的允许值集合也为空——其基数为零。简单来说,non_null
类型没有值。
与any
类型一样,non_null
类型也是一个**不可实例化**类型。它也是一个**密封**类型,将在下一篇 PTS 文章中解释。
空类型 (null)
null
类型是 any
类型的子类型。
如前一节所述,null
类型在其允许值集合中只有一个值,称为null
。null
类型的基数是一。简单来说,null
类型只有一个值:null
。
与any
和non_null
类型不同,null
类型是一种**可实例化**(或**具体**)类型。
与any
和non_null
一样,null
也是一个**密封**类型。null
没有子类型,也不允许任何人创建子类型。
其他类型
图中未显示的所有其余内置类型(string
、number
、list
等)以及所有用户定义类型(例如,customer
、supplier
、product
)都继承自non_null
。
规则
根据重要的Liskov 替换原则,类型继承树中的任何类型都与其所有父类型兼容。
将此原则应用于上述类型继承树意味着以下规则:
-
任何类型的值都可以赋值给声明为
any
类型的对象引用。例如,一个输入参数类型为
any
的函数可以被调用,其值可以是null
、"foo"
、123
或任何其他类型的值。 -
null
对于non_null
类型(或其任何后代)的对象引用来说是无效值。例如
-
输入参数类型为
non_null
的函数不能以null
作为输入来调用。但任何其他类型的值("foo"
、123
等)都允许作为输入。 -
其输入参数类型是
non_null
后代(例如,string
、customer
)的函数不能以null
作为输入来调用。注意
这与许多流行语言(如 C、Java、JavaScript、Python、Ruby)中允许的情况恰好相反,在这些语言中,每个对象引用都可以为
null
。例如,在 Java 中,null
可以赋值给String
类型的输入参数。
-
-
声明
null
类型的对象引用是没有意义的。因此编译器不允许这样做。例如,函数输入参数不能是
null
类型。null
类型只能作为联合类型的成员使用,如下一节所述。
可空对象引用
联合类型(参见《实用类型系统中的联合类型》)用于声明**可空**对象引用。如果对象引用的类型是包含成员null
的联合类型(例如,string or null
),则该对象引用是**可空**的。在这种情况下,值null
是有效的。
例如,如果函数foo
的输入参数类型为string or null
,则以null
作为输入调用它是有效的,如下代码所示:
function foo ( string or null )
// body
.
foo ( "bar" ) // ok
foo ( null ) // ok
另一方面,如果输入参数类型只是string
,则不允许使用null
:
function foo ( string )
// body
.
foo ( "bar" ) // ok
foo ( null ) // compile-time error
有用的运算符
在本节中,我们将介绍有助于空处理的有用运算符。
is 运算符
注意
is
运算符已在上一篇 PTS 文章《实用类型系统中的联合类型》的《is
运算符》部分中介绍过。本节只介绍其在空处理上下文中的用法。
is
运算符检查表达式是否为给定类型。结果是一个boolean
值。例如,"foo" is string
的计算结果为true
,而123 is string
的计算结果为false
。
因此,此运算符对于检查给定表达式是否计算为null
非常有用。下面是一个例子:
if customer.email_address is null then
write_line ( "No email address!" )
else
send_email ( email_address )
.
我们可以使用is not
来反转检查,而不是is
。因此,上面的代码也可以这样写:
if customer.email_address is not null then
send_email ( email_address )
else
write_line ( "No email address!" )
.
除了在if
*语句*中使用is
,我们还可以在if
*表达式*中使用它:
const has_email = if customer.email_address is null then "no" else "yes"
write_line ( """Customer has email: {{has_email}}""" )
安全导航运算符 (?)
维基百科文章《安全导航运算符》指出:
在面向对象编程中,**安全导航运算符**(也称为**可选链运算符**、**安全调用运算符**、**空条件运算符**、**空传播运算符**)是一个二元运算符,如果其第一个参数为 null,则返回 null;否则,它执行由第二个参数指定的解引用操作(通常是对象成员访问、数组索引或 Lambda 调用)。
在了解此运算符在 PTS 中如何工作之前,我们先看看为什么我们需要它。
假设我们想通过首先获取员工的部门,然后获取该部门的经理,最后获取该经理的电话号码来获取员工经理的电话号码。
如果链中的任何对象都不能为null
(即,它们都是不可空的),那么 Java 中的代码可能看起来像这样:
final String phoneNumber = employee.getDepartment().getManager().getPhoneNumber();
以下是相应的 PTS 代码:
const phone_number = employee.department.manager.phone_number
现在假设数据模型后来发生了变化,链中的每个对象(即employee
、department
和manager
)现在都可以为null
(即,它们从不可空变为可空)。在这种情况下:
-
上述 Java 代码仍然可以编译,但如果在链中的任何对象为
null
,则会在运行时抛出NullPointerException
。 -
由于内置的空安全,上述 PTS 代码不再能编译。运行时没有空指针错误的风险。
Java 代码可以修复如下:
String phoneNumber = null;
if ( employee != null ) {
Department department = employee.getDepartment();
if ( department != null ) {
Manager manager = department.getManager();
if ( manager != null ) {
phoneNumber = manager.getPhoneNumber();
}
}
}
PTS 代码也演变成一个if
-怪兽,phone_number
不能再是常量,它必须是一个变量:
variable phone_number = null
if employee is not null then
const department = employee.department
if department is not null then
const manager = department.manager
if manager is not null then
phone_number = manager.phone_number
.
.
.
这个 PTS 代码可以通过使用*安全导航运算符* (?
) 后跟null
来简化。代码现在又变成了一行:
const phone_number = employee?null.department?null.manager?null.phone_number
除了写?null
,我们也可以简单地写?
:
const phone_number = employee?.department?.manager?.phone_number
这段代码在语义上等同于其使用嵌套if
语句的冗长版本。一旦在链中遇到null
,复合表达式的评估就会中止。常量phone_number
的推断类型是string or null
。
在 PTS 中,?
运算符的使用不限于null
类型——它可以与任何类型一起使用(例如?error
、?string
、?list<string>
)。
如果未指定类型,则类型默认为null
,因为这是最常见的使用情况。因此,我们可以简单地写?
而不是?null
。
可以链式使用多个?
运算符,以检查不同的类型。例如,如果链中的中间值是从可能返回null
或错误的函数中检索的,我们可以这样写:
const phone_number = get_employee()?null?error \
.get_department()?null?error \
.get_manager()?null?error \
.get_phone_number()
现在,一旦中间结果评估为null
或error
类型,表达式的评估就会中止。phone_number
的推断类型是string
或null
或error
。
if_is 运算符
if_is
运算符提供两个可能值之一,具体取决于表达式的类型。因此,if_is
用于执行涉及类型的三元操作。
其语法如下:
<expression_1> "if_is" <type> ":" <expression_2>
如果<expression_1>
评估为<type>
类型的实例,则结果为<expression_2>
,否则结果为<expression_1>
。
if_is
运算符通常用于在表达式评估为null
时提供默认值。
假设product.comment
的类型为string or null
。现在考虑以下将字符串赋值给常量text
的代码:
const text = if product.comment is not null then product.comment else "No comment"
使用if_is
运算符可以简化代码:
const text = product.comment if_is null : "No comment"
此语句在语义上等同于第一个使用if then else
表达式的语句。如果product.comment
评估为字符串,则该字符串被赋值给text
。如果product.comment
评估为null
,则字符串"No comment"
被赋值给text
。
可以使用简写if_null :
(或if_null:
)代替if_is null :
:
const text = product.comment if_null: "No comment"
注意
if_null:
是一个二元运算符,其工作方式类似于其他语言中的空合并运算符。例如,C# 和 JavaScript 支持空合并运算符,使用符号
??
。这是维基百科上显示的一个 C# 示例:string pageTitle = suppliedTitle ?? "Default Title";在 PTS 中,这将写成:
const page_title = supplied_title if_null: "Default Title"
除了代码简洁的明显好处外,使用if_null
还提供了其他优势:
-
消除了代码重复,因为
product.comment
在使用if
表达式的语句中出现两次,但在使用if_null
的代码中只出现一次。 -
代码执行速度更快,因为
product.comment
只被评估一次。 -
代码不会陷入可能在
product.comment
是**可变**的情况下发生的糟糕(有时非常难以调试)运行时错误。考虑第一个版本:
const text = if product.comment is not null then product.comment else "No comment"
设想一个多线程应用程序,其中
product.comment
是可变的,并且product.comment
的第一次评估结果是一个string
,而第二次评估结果是null
,因为另一个线程在两次评估之间更改了它的值。在这种情况下,null
将被错误地赋值给text
。像这样的问题(又称竞态条件)通常非常不可能发生。很可能在开发/测试期间从未发生过。但后来(也许是很久以后),当代码在生产环境中每天执行数百万次时,可能性增加了,突然出现了一个令人难以置信的讨厌的随机错误——非常难以调试。
注意
一位经验丰富且勤奋的开发人员,深知
if
表达式的潜在问题,会通过编写以下代码来消除上述三个问题(代码重复、性能和竞态条件风险):const comment = product.comment const text = if comment is not null then comment else "No comment"如果
product.comment
是可变的,那么一个旨在检测潜在竞态条件的勤奋编译器会因为product.comment
在if
表达式中出现两次而生成错误(或至少是警告)。一个勤奋的 IDE 会建议将使用
if
表达式的代码转换为简洁、快速且可靠的惯用 PTS 代码:const text = product.comment if_null: "No comment"
if_is
运算符可以与任何类型一起使用,而不仅仅是null
。
const object_displayed = object if_is password: "secret"
多个if_is
运算符可以链式使用。这对于,例如,一旦遇到给定类型的值就停止表达式的求值很有用。
考虑一个创建数字文档的应用程序。假设用于渲染文档的字体以级联方式确定:如果字体由文档中的选项明确定义,则使用该字体。如果文档中没有定义字体,则应用程序会在共享的*config*文件中查找定义的字体。如果*config*文件没有指定字体,则使用硬编码的默认字体作为备用。
如果没有if_is
运算符,我们将需要编写如下冗长的 PTS 代码:
fn get_font ( context ) -> font
variable font = context.document_font
if font is not null then
return font
.
font = context.config_font
if font is not null then
return font
.
return context.default_font
}
if_is
运算符简化了代码:
fn get_font ( context ) -> font =
context.document_font if_null: context.config_font if_null: context.default_font
有用的语句
if ... is null 语句
在is
运算符一节中,我们已经看到了is
运算符如何在if
语句中使用。这里重申一个之前的例子:
if customer.email_address is not null then
send_email ( email_address )
else
write_line ( "No email address!" )
.
case type of 语句
case type of
语句已在上一篇 PTS 文章《实用类型系统中的联合类型》的《case type of
语句》部分中介绍过。
以下是该部分中显示的一个示例的重申:
case type of read_text_file ( file_path.create ( "example.txt" ) )
is string as text // the string is stored in constant 'text'
write_line ( "Content of file:" )
write_line ( text ) // the previously defined constant 'text' is now used
is null
write_line ( "The file is empty." )
is file_error as error
write_line ( """The following error occurred: {{error.message}}""" )
.
如您所见,当函数read_text_file
返回null
时,执行第二个分支(is null
)。
注意
除了
case type of
*语句*,PTS 还提供了case type of
*表达式*:const message = case type of read_text_file ( file_path.create ( "example.txt" ) ) \ is string: "text" \ is null: "no text" \ is error: "file read error" write_line ( "Result: " + message )
assert 语句
有时,我们知道的比编译器多。例如,我们可能知道一个声明返回string or null
类型值的函数在给定上下文中永远不会返回null
。
在这种情况下,我们可以使用assert
语句来表达我们的假设:
const result string or null = get_string_or_null ( ... )
assert result is not null
...
const size = result.size // valid because 'result' has been asserted to be non-null
在上面的代码中,assert
用于声明存储在result
中的值永远不会是null
。
众所周知,“人非圣贤孰能无过”。因此,假设result is not null
在运行时被检查,如果假设结果错误,则会生成错误。
除了断言not null
,我们还可以断言null
,或断言给定表达式的任何其他类型:
assert problem is null
assert names is list<string>
assert result is not error
与其他语言一样,assert
关键字后面跟着任何布尔表达式。因此,除了断言表达式的类型之外,assert
还可以用于表达各种假设。它可用于断言对象/状态条件、循环不变量或任何其他有助于可靠地文档化代码、简化代码或优化其性能的条件。
注意
assert
语句必须始终无副作用,尤其是在编译器标志允许禁用它们以获得更好性能的情况下。
我们可以提供一条特定的错误消息,在运行时显示,使用语句末尾的error_message:
属性。这里有几个例子:
assert employee.name.size <= 50
assert customer.phone_number.starts_with ( "+" ) \
error_message: "Phone number doesn't start with '+'"
assert index >= 1 and index <= list.size \
error_message: """Index ({{index}}) out of bounds (1..{{list.size}})"""
on 子句
on
子句用于在表达式评估为指定类型时执行return
或throw
语句。
on
子句的通用语法如下:
<expression> "on" <type> ( "as" <identifier> ) ? ":" <return_or_throw_statement>
如果<expression>
匹配<type>
,则执行<return_or_throw_statement>
。可选的<identifier>
可用于将<expression>
的结果存储在一个常量中,然后可在<return_or_throw_statement>
中使用(参见以下示例)。
让我们看看这为什么有用。
这是一个重复代码模式的例子:
const value = get_value_or_null()
if value is null then
return null
.
我们可以使用on
子句来编写语义等效但更短的代码:
const value = get_value_or_null() on null : return null
除了return
语句,也可以使用throw
语句来中止程序执行:
const value = get_value_or_null() on null : throw application_error.create (
"Unexpected 'null' returned by 'get_value_or_null'." )
任何类型都可以在on
子句中使用,并且可以链接多个on
子句:
const value = get_value_or_null_or_error() \
on null : return null \
on error as e : return e
on null : return null
和 on error as e : return e
在实践中都经常使用。因此,可以使用简写 ^on_null
和 ^on_error
代替。上面的代码变成:
const value = get_value_or_null_or_error() ^on_null ^on_error
...这在语义上也等同于以下冗长的代码:
const value = get_value_or_null_or_error()
if value is null then
return null
.
if value is error then
return value
.
on
子句可用于常量赋值、变量赋值或函数调用的末尾:
// constant assignment
const c number = get_string_or_number() on string : return null
// variable assignment
variable v = get_string_or_number() on string : return null
v = get_number_or_null() ^null
// function call (return value is null or an error)
close_connections() ^error
流敏感类型
流敏感类型(也称为流式类型或出现类型)意味着编译器会根据对象引用(常量、变量等)在代码中被访问的位置,跟踪并动态更改它们的类型。
流式类型已在上一篇文章《实用类型系统中的联合类型》的《is
运算符》部分中简要介绍过。
在空处理的上下文中,流式类型很方便,因为它减少了代码中所需的空检查次数,从而使代码更小、更快。
例如,考虑以下代码(其中size
是string
类型的一个方法):
variable name string or null = null
variable size = name.size // invalid
name = "Bob"
size = name.size // valid
第二行明显无效,因此被编译器拒绝。但最后一行有效,因为此时值"Bob"
存储在name
中。然而,如果没有流类型,编译器会生成一个错误,因为name
的类型被声明为string or null
。流类型消除了这个假阳性,因为编译器跟踪分配给name
的值,动态更改其类型,并得出结论:最后一条语句是有效的。
现在考虑这个涉及控制流的例子:
variable name string or null = null
if foo() then
name = "Bob"
else
name = "Alice"
.
const size = name.size // valid
最后一行是有效的,因为在then
和else
两个分支中都分配了一个string
。
现在我们给之前的代码增加一些复杂性:
variable name string or null = null
if foo() then
if bar() then
name = "Bob"
.
else
name = "Alice"
.
const size = name.size // invalid
现在最后一行不再有效,因为如果foo()
返回true
,而bar()
返回false
,那么name
将是null
。这将生成编译时错误。
为了实现流类型,编译器会分析源代码中的所有执行路径(考虑各种控制流语句,例如if
、case
、while
、return
、throw
),并为源代码中每个相关位置的范围内所有对象引用调整其类型。
为了覆盖代码中所有地方的流类型,编译器还必须分析表达式中的执行路径(除了分析语句)。
考虑以下代码:
const name string or null = ...
if ( name is not null and name.size > 50 )
write_line ( "Name too long" )
.
上述代码是有效的,因为当求值name.size
时,通过之前的name is not null
检查,name
被保证为非null
。
如果我们不小心颠倒了检查的顺序,或者使用了or
而不是and
,代码将无法编译:
if ( name.size > 50 and name is not null ) // compile-time error
if ( name is not null or name.size > 50 ) // compile-time error
摘要
PTS 使用null
来表示“值的缺失”。
联合类型用于声明可空对象引用(例如,string or null
)。
空安全原生内置于类型系统,因此不会发生空指针错误。
PTS 提供了一组专用运算符和语句,以尽可能简化空处理。
流式类型减少了代码中所需的空检查次数,从而使代码更小、更快。
关于 null 的结语
不再需要憎恨或恐惧null
。
null
是一个非常有用的概念,在许多软件应用程序中经常使用。
null
的发明本身并不是“十亿美元的错误”。错误在于缺乏确保空安全的类型系统,以及缺乏使空处理变得简单、安全和愉快的语言特性。
致谢
非常感谢Tristano Ajmone提供的宝贵反馈,以改进本文。
历史
- 2023年12月14日:初始版本