实用类型系统 (PTS) 中的记录类型





5.00/5 (2投票s)
本文将解释我们为何需要原生记录类型,以及它们在PTS中的工作原理。
目录
注意
这是系列文章《如何设计一个实用的类型系统以最大化软件开发项目的可靠性、可维护性和生产力》的第三部分。
建议(但不是强制要求经验丰富的程序员)按出版顺序阅读这些文章,从 第一部分:什么?为什么?如何做? 开始。
如需快速回顾之前的文章,您可以阅读 实用类型系统(PTS)文章系列摘要。

引言
记录类型在各种软件开发项目中都很常用。
例如,一个ERP应用程序可能具有记录类型customer
和supplier
,以及属性identifier
、name
、address
等。
注意
由于记录无处不在,一个实用的类型系统应该提供一套全面的、易于使用的功能,并涵盖与记录相关的所有方面。
本文首先解释了为何类型系统需要原生支持记录类型。然后,您将通过简单的源代码示例看到记录类型在PTS中的工作原理——每个特性都将得到说明。
我们为何需要记录类型?
假设我们需要一个数据结构来存储RGB颜色。
该结构有一个类型为string
的name
字段,用于存储颜色的名称。此外,它还有三个整数字段:red
、green
和blue
,用于存储RGB值。
为了使数据结构更可靠和实用,让我们添加以下条件:
-
字段
name
只能包含字母和空格,且长度必须在3到50个字符之间。 -
字段
red
、green
和blue
必须包含0到255之间的整数值。每个字段的默认值为0。例如,如果我们创建一个对象而不指定
red
的值,它的值将是0。 -
每个对象都必须是不可变的——创建后,无法更改字段的值。
-
为了完全符合PTS设计规则,无效的硬编码颜色对象应该在编译时报告。
例如,创建具有
red=1000
的无效对象将在编译时(而不仅仅是运行时)产生错误。
除了最后一个条件,这看起来是一个简单的练习,在任何编程语言中都应该很容易实现,不是吗?
你需要编写多少代码才能用你喜欢的编程语言实现这一点,代码又会是什么样子?
让我们先看看简单类型系统中容易出错的解决方案,然后看看用Java、Kotlin和PTS编写的代码。
注意
只对PTS代码感兴趣的读者可以跳过此部分。
列表
一种快速粗糙的解决方案是使用列表来存储RGB颜色对象。这几乎可以在任何编程语言中完成,因为列表类型是一个支持良好、基础的类型。
例如,我们可以这样做(使用JavaScript语法)
red = [ "red", 255, 0, 0 ];
不用说,这样的代码将是极其容易出错的。
使用第一个元素存储名称,第二个元素存储红色值等,仅仅是一种约定,需要在代码的每个地方都应用。没有任何机制可以防止意外违反这种位置约定。此外,如果列表是可变的(如大多数编程语言中那样),则无法防止在对象创建后添加、删除或更改元素。最后,如果以后更改了结构(例如,添加了字段alpha
,因为我们希望指定颜色的不透明度并使用RGBA颜色模型),我们将不得不手动更新代码库——有遗漏或不小心遗漏某些地方的风险。
我们不会浪费时间展示所有可能发生的丑陋情况。
映射
一个稍微好一点的方法是使用映射(map)而不是列表。
以下是一个JavaScript示例
red = {
"name": "red",
"red": 255,
"green": 0,
"blue": 0
};
使用映射而不是列表可以改进代码,并且不易出错,因为现在字段都有名称了。例如,要获取green
的值,我们可以写color["green"]
而不是color[2]
(索引从0开始)。
然而,使用映射仍然是非常容易出错的,特别是如果映射是可变的。没有任何机制可以防止将一个有效的颜色对象转换为任何其他可能最终无法识别的东西。例如,考虑以下代码
red = {
"name": "red",
"red": 255,
"green": 0,
"blue": 0
};
console.log("before:" + JSON.stringify(red));
red["name"] = undefined;
red["red"] = "I was a number, but now I'm a string.";
red["read"] = 255; // typo in field name
delete red.green;
delete Object.assign(red, { foo: red.blue }).blue;
console.log("after: " + JSON.stringify(red));
输出如下
before:{"name":"red","red":255,"green":0,"blue":0} after: {"red":"I was a number, but now I'm a string.","read":255,"foo":0}
此外,如果以后更改了数据结构(例如,添加、重命名或删除字段),则没有机制可以确保我们更新任何访问或创建这些对象的代码。与列表一样,所有这些都可能导致维护噩梦,尤其是在中大型项目中。
注意
ECMAScript 2015 (ES6) 在JavaScript中引入了类。
然而,在底层,JavaScript类是建立在原型之上的,这意味着它们本质上是伪装成类的可变、异构映射。因此,上述所有问题也存在于JavaScript类中。有关高级解释,请阅读Justen Robertson的文章作为JS开发者,ES6类让我彻夜难眠。
因此,所谓的JavaScript“类”与C、C++、C#、Go、Java、Python、Rust等语言中的类/记录/结构非常不同,也不那么健壮。
Java 中的记录
高级类型系统提供记录类型,也称为结构、struct或复合数据。
在Java中,我们可以使用记录类型并定义RGBColor
如下:
public record RGBColor (
String name,
int red,
int green,
int blue ) {
public String toString() {
return name + " (" + red + ", " + green + ", " + blue + ")";
}
}
注意
记录类型在Java 14(2020年3月)中引入。
在Java 14之前,RGBColor
的代码如下所示:
public class RGBColor {
private final String name;
public String getName() { return name; }
private final int red;
public int getRed() { return red; }
private final int green;
public int getGreen() { return green; }
private final int blue;
public int getBlue() { return blue; }
public RGBColor ( String name, int red, int green, int blue) {
this.name = name;
this.red = red;
this.green = green;
this.blue = blue;
}
// TODO equals(), hashCode(), toString()
}
两个Java版本在语义上是等效的(它们都代表具有不可变字段的记录),但新代码显然更易于阅读、编写和维护。
有关Java中记录的更多信息,您可以阅读Lokesh Gupta的文章Java 记录。
使用record
类型而不是map
是一个巨大的改进,因为现在每个RGBColor
的实例在其整个生命周期中都保证具有相同的字段集。
如果以后更改了记录类型(添加、删除、重命名字段或更改其类型),则使用该类型的所有代码都将由编译器自动检查。我们不必担心在记录类型更改时忘记重构代码。
此外,Java记录实例始终是不可变的,这使得它们不易出错。
然而,我们类型的基数比应有的要高几个数量级。最初为有效RGBColor
对象指定的条件尚未满足。我们尚未应用PTS编码规则。例如,每个颜色值(定义为32位整数)可以存储-2,147,483,648到2,147,483,647范围内的值,尽管我们指定值必须在0..255范围内。
注意
我们不能为RGB值使用byte
类型,因为(在Java中)byte
是一个有符号整数,范围是-128..127。
我们可以使用short
(16位整数)类型而不是int
(32位整数),但Java没有short
字面量。因此,我们需要写(short) 10
,而不是简单的10
,这是不切实际的。
我们可以使用单个32位整数来存储所有三个8位RGB值,然后使用位操作来提取这三个值,但这不符合本练习的目的(即如何处理整数范围)。
此外,字段name
缺乏约束,这意味着任何乱码(例如,$%7^
)——甚至长度不受限制(例如,<script>malicious ...</script>
)的恶意HTML或SQL代码——都可以存储在其中。
为什么我们要关心这个?因为实践告诉我们:**在软件应用程序中未捕获的无效数据值的影响通常是无法预料和不可预测的,并且它们因领域不同而差异很大,从无害到巨大的灾难不等。**
例如,如果RGBColor
没有自动防止无效值,那么下面的消息
……可能会突然显示为这样:
怎么可能?消息为什么消失了?
嗯,它实际上并没有消失。假设我们没有写
RGBColor textColor = new RGBColor ( "red", 255, 0, 0 );
……我们意外地引入了一个加一错误(256而不是255)
RGBColor textColor = new RGBColor ( "red", 256, 0, 0 );
^
如果红色值(256)稍后转换为8位值(仅保留32位值的右侧八位),则8位值将是00000000
(八个零),因为256
的二进制表示是100000000
(一个1后面跟着八个零)。因此,文本颜色将是黑色,而不是红色。因此,文本并没有消失——它只是因为在黑色背景上显示黑色文本而变得不可见。
注意
有人可能会争辩说,诸如textColor = new RGBColor ( "red", 0, 0, 0 )
之类的语句也会出现黑色文本在黑色背景上的问题,这显然是错误的,但被编译器接受,因为它不知道颜色。
这是真的。然而,如果32位整数未被检查,此问题的概率要高几个数量级。所有八位右侧为零的值从256到2,147,483,647,以及所有八位右侧为零的负值,也将导致文本颜色为黑色。
当然,其他有效RGB值(都在0-255范围内,例如(1, 1, 1)
)也可能出现类似问题,因为非常暗的文本在黑色背景上对人类来说是不可读的。但同样,如果整数未被检查,概率会高得多。稍后我们将看到如何完全消除糟糕的文本/背景颜色组合的风险。
更可靠的 Java 记录
为了满足本练习开始时指定的所有条件,让我们改进我们的Java记录,使其更可靠、更实用。我们需要保护所有记录字段免受无效值的影响,并且RGB字段应具有0的默认值。
重构我们之前的记录示例有多种方法。在下面的代码中,定义了可重用类来存储颜色名称及其值,并使用(Java中常见的)构建器模式来创建具有默认值的对象。我将跳过细节,因为理解此代码与当前主题无关。只需看看新代码的大小。
public record RGBColor (
ColorName name,
RGBValue red,
RGBValue green,
RGBValue blue ) {
public record ColorName ( String value ) {
private static final Pattern NAME_REGEX = Pattern.compile ( "[a-zA-Z ]{3,100}" );
public ColorName {
Objects.requireNonNull ( value );
if ( !NAME_REGEX.matcher ( value ).matches () ) {
throw new IllegalArgumentException ( "'" + value + "' is invalid. The name must match the regex " + NAME_REGEX + "." );
}
}
public String toString() { return value; }
}
public record RGBValue ( int value ) {
public RGBValue {
if ( value < 0 || value > 255 ) {
throw new IllegalArgumentException ( "'" + value + "' is invalid. The value must be in the range 0 to 255." );
}
}
public String toString() { return String.valueOf ( value ); }
}
public static class Builder {
private ColorName name;
private RGBValue red = new RGBValue ( 0 );
private RGBValue green = new RGBValue ( 0 );
private RGBValue blue = new RGBValue ( 0 );
public Builder() {}
public Builder name ( String name ) {
this.name = new ColorName ( name );
return this;
}
public Builder red ( int red ) {
this.red = new RGBValue ( red );
return this;
}
public Builder green ( int green ) {
this.green = new RGBValue ( green );
return this;
}
public Builder blue ( int blue ) {
this.blue = new RGBValue ( blue );
return this;
}
public RGBColor build() { return new RGBColor ( name, red, green, blue ); }
}
public RGBColor {
Objects.requireNonNull ( name );
Objects.requireNonNull ( red );
Objects.requireNonNull ( green );
Objects.requireNonNull ( blue );
}
public static Builder builder() { return new Builder(); }
public String toString() {
return name.value + " (" + red.value + ", " + green.value + ", " + blue.value + ");";
}
}
如果您认为这是大量代码,难以理解(特别是对Java新手),并且没有人想编写和维护这样的代码,那么您并不孤单!我们为第一个不可靠版本输入了196个字符(包括空格),现在我们的代码已经演变成一个包含1,993个字符的混乱体。这意味着要阅读、编写和维护的代码量大约增加了10倍!更重要的是,如果我们提供更合适的equals
和hashCode
方法版本(仅考虑颜色值,而不是名称),代码会更长(有关更多信息,请阅读Java equals()和hashCode()约定)。
此外,现在我们违反了不要重复自己(DRY)原则。例如,重命名字段name
将需要我们在八个不同的地方编辑代码,并且编译器不会检查我们是否一致地替换了所有出现的地方。
无论如何,从积极的一面来看,RGBColor
现在更可靠、更实用。现在可以像这样创建颜色对象:
RGBColor black = RGBColor.builder()
.name("black")
.build();
RGBColor red = RGBColor.builder()
.name("red")
.red(255)
.build();
RGBColor backgroundColor = RGBColor.builder()
.name("yellowgreen")
.red(255)
.green(205)
.blue(50)
.build();
此代码也很冗长——但至少它更可靠。尝试创建具有无效值的颜色现在将导致运行时错误。red=256
导致黑色屏幕的风险已被消除——将显示以下运行时错误:
Exception in thread "main" java.lang.IllegalArgumentException: Invalid value for 'red': 256. The value must be in the range 0 to 255. ...
有趣的是,PTS设计规则的条件1(代码易于编写)在我们编写RGBColor
记录类型的第一个Java版本时得到了满足,但条件2(无无效值)被违反了。现在我们遇到了完全相反的情况:条件1被违反,但条件2已满足。
然而,这并不令人意外,因为我们知道编写可靠和安全的代码通常很困难,有时非常困难。
我们完成了?
不,还没呢!
我们宁愿收到编译时错误而不是运行时错误,来处理无效值。
除了导致更可靠的代码外,在编译时检测到的错误还可以在IDE中得到很好的报告,一旦键入错误即可。因此,如果我们键入256
而不是255
,非法值将立即显示为错误,并附带一个对人类友好的错误消息、一个修复错误的有用提示,以及(锦上添花)一个可点击的解决方案列表。
注意
不幸的是,我们在此练习中处理的错误(无效字符串或超出指定范围的整数文字)无法在编译时报告——至少据我所知,在Java或其他流行编程语言中都无法报告。
也许有第三方静态源代码分析器能够报告此类错误——在正确配置该工具之后。但即使存在这样的工具,它也不是理想的解决方案,因为它需要安装、正确配置和学习如何使用;而且它将依赖于第三方开发者的维护,以确保它与新版本编程语言保持同步。这也不是我们想要的——我们希望这种级别的支持是开箱即用的。
注意
为了支持丰富的源代码编辑功能,如自动补全、转到定义和错误检测,现代语言通常提供语言服务器协议(LSP)的实现。
LSP实现通常使用(可能是LSP优化的)编译器来检测错误。因此,只有在编译时检测到的错误才能在IDE中报告。
另一个新兴的有趣技术是使用集成在IDE中的AI算法来检测错误——这是一个过于广泛而无法在此讨论的主题。
Kotlin 代码
以下是使用Kotlin(一种现代JVM语言)编写的解决方案:
data class RGBColor (
val name:ColorName,
val red:RGBValue = RGBValue(0),
val green:RGBValue = RGBValue(0),
val blue:RGBValue = RGBValue(0) ) {
data class ColorName ( val name:String ) {
companion object {
val NAME_REGEX = Regex("[a-zA-Z ]{3,100}")
}
init {
require ( name.matches(NAME_REGEX))
{ "'$name' is invalid. The value must match the regular expression $NAME_REGEX." }
}
override fun toString(): String = name
}
data class RGBValue (val value:Short ) {
init {
require (value in 0..255 )
{ "'$value' is invalid. The value must be in the range 0 .. 255." }
}
override fun toString(): String = value.toString()
}
constructor ( name: String, red: Short = 0, green: Short = 0, blue: Short = 0 ) :
this ( ColorName(name), RGBValue(red), RGBValue(green), RGBValue(blue) )
override fun toString(): String = "$name ($red, $green, $blue)"
}
注意
Kotlin有一个无符号8位整数类型(值从0到255),称为UByte
,它很幸运地适合我们的特定用例,并且可以代替包装在RGBValue
中的Short
值。但我没有在此示例中使用UByte
,因为我想展示一个更通用的解决方案来将整数值约束在范围(min .. max
)内——这个解决方案也可以应用于其他情况。
我不是Kotlin专家。如果有一种更好的编写此代码的方法,请留下评论。
与Java不同,Kotlin不需要Builder
类,因为它支持命名参数赋值和记录字段的默认值(而Java不支持)。此外,Kotlin类型默认是不可空的,无需进行空检查。因此,Kotlin代码的大小约是其Java对应代码的50%——这是一个显著的改进。然而,上面的代码仍然看起来“代码太多了”。
客户端代码如下所示:
val black = RGBColor(name = "black")
val red = RGBColor(name = "red", red = 255)
val backgroundColor = RGBColor (
name = "yellowgreen",
red = 154,
green = 205,
blue = 50 )
好!
然而,硬编码的无效值(例如red=256
)仍然只在运行时被检测到,而不是在编译时。
PTS 代码
这是用PTS编写的代码:
type color_name = string ( pattern = "[a-zA-Z ]{3,50}" )
type color_value = integer ( range = 0 .. 255 )
type RGB_color
att name color_name
atts type:color_value default:0
red
green
blue
.
fn to_string = """{{name}} ({{red}}, {{green}}, {{blue}})"""
.
我们很快就会探讨语法,但这段代码可以总结如下:
-
首先,我们定义类型
color_name
,这是一个受约束的字符串,必须匹配正则表达式[a-zA-Z ]{3,50}
(仅字母和空格;最少3个字符,最多50个)。 -
然后,我们定义类型
color_value
——一个在0..255范围内的整数。 -
然后,这些类型用于类型
RGB_color
——一个由属性name
、red
、green
和blue
组成的记录类型。
上面的代码满足了所有指定的条件。
代码大小(314个字符)约为Java代码的15%,约为Kotlin代码的30%。

显然,这些数字只是建议性的——在其他情况下它们可能差异很大。但它们对于说明在保持代码简洁的同时,提高可靠性和可维护性的潜力很有用。
我们的RGB_color
记录的使用方式如下:
const black = RGB_color.create ( name = "black" ) const red = RGB_color.create ( name = "red", red = 255 ) const background_color = RGB_color.create ( name = "yellowgreen" red = 154 green = 205 blue = 50 ) write_line ( black.to_string ) write_line ( red.to_string ) write_line ( background_color.to_string )
输出
black (0, 0, 0) red (255, 0, 0) yellowgreen (154, 205, 50)
硬编码的无效值(例如red=256
)在编译时被检测到。
总结
引用如果建筑工人像程序员写程序一样盖房子,那么第一只啄木鸟就会毁掉文明。
在许多编程语言中,编写可靠的软件是一项艰巨的任务,通常需要大量的时间和精力投入。这种困难可能导致程序员优先考虑开发速度和便利性而不是可靠性,从而导致代码的可靠性不如其本应有的水平。因此,许多项目被生产环境中的bug所困扰,但这些bug本来可以通过更好的类型系统轻松避免。
众所周知,在开发/维护周期的后期发现和修复bug的成本呈指数级增长。在生产环境中检测到的bug比在编译时发现的相同bug修复成本高几个数量级。
因此,类型系统应该简化编写可靠代码的过程,以便开发人员更加高效,并能专注于代码的逻辑。这在大/复杂项目和安全关键环境(如为航空航天、医疗设施、交通信号灯自动化、自动驾驶汽车等编写的软件)中尤其重要。
它是如何工作的?
本节提供PTS记录类型的概述——展示功能,而不是全面的规范和实现细节。
我们将查看大量简单的代码示例,以说明每个功能。
基本语法
PTS记录类型由一个名称和一组属性组成。每个属性由一组属性定义:唯一的名称、类型和其他可选属性。
以下是记录类型RGB_color
的初步版本,包含四个属性:
record type RGB_color att name type:string att red type:integer att green type:integer att blue type:integer .
注意
PTS属性在某些编程语言中被称为字段或属性。
在att
行中,可以省略type:
(属性名+分隔符)。
record type RGB_color att name string att red integer att green integer att blue integer .
属性也可以定义在atts
块中,在这种情况下,每个属性前面的att
关键字可以省略。
record type RGB_color atts name string red integer green integer blue integer . .
如果几个属性具有相同的属性(例如,两个属性类型相同),则可以通过在atts
行中一次定义公共属性来保持代码简洁。
type RGB_color att name string atts type:integer red green blue . .
函数 to_string
每个PTS类型都有一个名为to_string
的方法,该方法返回一个字符串,表示实例/对象的简短、人类可读的描述。
编译器提供to_string
的默认实现。默认情况下,to_string
将为颜色绿色返回以下字符串:
[RGB_color [name green][red 0][green 255][blue 0]]
要提供自定义描述,我们可以通过显式定义函数to_string
来覆盖默认实现,如下所示:
type RGB_color att name string atts type:integer red green blue . // example output: green (0, 255, 0) fn to_string = """{{name}} ({{red}}, {{green}}, {{blue}})""" .
上面的函数使用字符串插值,它通过所谓的三引号字符串("""..."""
)支持。可以通过将属性(或任何复杂度的表达式)嵌入一对双花括号({{...}}
)中来插入它们。
创建记录
可以如下创建RGB_color
类型的实例/对象:
const green RGB_color = RGB_color.create ( name="green" red=0 green=255 blue=0 ) // PTS supports type inference. // Therefore the type of the constant (RGB_color) can be omitted. const yellow = RGB_color.create ( name = "yellow" red = 255 green = 255 blue = 0 ) write_line ( green ) write_line ( yellow )
输出
green (0, 255, 0) yellow (255, 255, 0)
命名输入参数赋值(如上面RGB_color.create
函数调用中所用)比位置赋值更不容易出错且更易读。因此,PTS要求函数调用使用命名赋值,但以下两种情况除外:
-
调用了具有单个输入参数的函数。
以下语句都允许:
greet ( message = "Hello" ) greet ( "Hello" )
-
作为参数传递的对象引用与输入参数同名。示例:
const title = "Info" const message = "All is well." show_info ( title = title, message = message, modal = true ) // Alternative show_info ( title, message, modal = true )
因此,以下代码是无效的,因为它使用了位置赋值:
const red = RGB_color.create ( "red", 255, 0, 0 ) // INVALID!
为了理解为什么不支持这种更短的语法,让我们假设以后在类型定义中更改了颜色值的顺序。将顺序从red
、green
、blue
改为blue
、green
、red
(出于任何神秘原因)。那么表达式RGB_color.create ( "red", 255, 0, 0 )
将不再创建一个红色对象——它将错误地创建一个蓝色对象,并且既没有编译时错误也没有运行时错误。
使用蓝色而不是红色可能不是世界末日。但想象一下以下函数签名:
fn transfer_money ( amount decimal, to_account string, from_account string )
……以及以下语句(在PTS中无效,因为它使用位置赋值):
transfer_money ( 1_000_000.00, "bob_account", "alice_account" )
请注意,上述语句不会告诉我们哪个账户被贷记,除非我们查看函数定义。现在想象一下,以后函数签名更改为更直观的输入参数顺序:
fn transfer_money ( amount integer, from_account string, to_account string )
如果我们忘记调整函数调用,那么一百万将被错误地从Bob转给Alice,而不是从Alice转给Bob。
使用命名赋值时,此类错误不会发生,因为编译器会根据匹配的名称自动重新排列顺序。命名赋值也清楚地告诉我们哪个账户被贷记:
transfer_money ( amount = 1_000_000.00 from_account = "alice_account" to_account = "bob_account" )
此外,命名赋值消除了在构造函数重载不能具有相同输入参数类型的语言中可能出现的问题。例如,在Java中,开发人员有时无法使用惯用的构造函数——他们被要求使用静态方法创建对象,或使用构建器模式,正如我们在更可靠的Java记录部分中所做的那样。这个问题在Stackoverflow问题构造函数重载相同参数中得到了体现。
命名赋值与默认值(在下一节中解释)相结合,消除了这些问题——从而提高了代码的可读性、可靠性和可维护性。
默认值
可选属性default
用于指定属性的默认值。
这使我们能够为属性red
、green
和blue
定义零的默认值,正如我们最初的练习规范所要求的:
type RGB_color att name string atts type:integer default:0 red green blue . .
而不是写:
const red = RGB_color.create ( name = "red" red = 255 green = 0 blue = 0 )
……我们现在可以简单地写:
const red = RGB_color.create ( name = "red" red = 255 )
如果定义了默认值,则可以在运行时以编程方式检索它。例如,要获取属性red
的默认值,我们可以编写:
const default_red = RGB_color.atts.red.default write_line ( """Default value for red: {{default_red}}""" ) // Same in one line: write_line ( """Default value for red: {{RGB_color.atts.red.default}}""" )
输出
Default value for red: 0
在运行时以编程方式获取默认值的功能在各种情况下都很有用。例如,在GUI应用程序中,我们可以用类型中指定的默认值预填充文本输入字段。
数据验证
在PTS中,数据验证是一个关键方面,因为它使我们能够遵守PTS编码规则(在本PTS文章系列的第一部分什么、为什么和如何?中介绍)。
引用软件项目中的所有数据类型都应具有最低可能的基数。
— PTS编码规则
最终,严格的数据验证可以产生更健壮、更可靠和更安全的代码,并减少查找和修复bug所花费的时间。
以下各节将展示如何将记录的数据验证应用于单个属性以及整个记录。
属性验证
有两种方法可以验证属性:
- 向属性的类型添加约束
示例
type RGB_color att name string ( pattern: "[a-zA-Z ]{3,50}" ) atts type:integer ( range: 0 .. 255 ) default:0 red green blue . .
在这里,我们将属性
name
声明为string
类型——但我们添加了值必须匹配正则表达式[a-zA-Z ]{3,50}
的约束,正如我们最初的规范所要求的。此外,我们将三个颜色值约束在0..255范围内。
注意
受约束的类型将在后续的PTS文章中全面介绍。
- 使用显式定义的受约束类型
如果相同的受约束类型在代码的其他地方使用(以遵守DRY原则),则应使用此选项。在下面的代码中,我们首先创建新的、可重用类型,称为
color_name
和color_value
。然后,在类型RGB_color
中使用这些类型。type color_name = string ( pattern: "[a-zA-Z ]{3,50}" ) type color_value = integer ( range: 0 .. 255 ) type RGB_color att name color_name atts type:color_value default:0 red green blue . .
函数 check
在研究定义属性验证规则的不同方法之前,首先了解验证在后台如何工作很有帮助。
注意
不对此部分感兴趣的读者可以跳过此部分。
本节显示的函数是简化示例,仅用于说明PTS数据验证*如何*实现。
考虑以下语句,它定义了类型color_name
——一个受约束为匹配正则表达式[a-zA-Z ]{3,50}
的string
:
type color_name = string ( pattern: "[a-zA-Z ]{3,50}" )
每次声明受约束类型时,编译器都会隐式创建一个名为check
的函数。该函数接受一个值作为输入,如果值有效则返回null
,否则返回invalid_value_error
类型的对象(PTS实现中定义的标准库类型)。如果值无效,该函数返回的错误对象包含额外信息(错误消息、标识符等),客户端代码可以对其进行探索——例如,向用户显示错误消息。
以下是check
函数的示例,该函数由编译器专门为上述color_name
类型声明创建,该声明使用了pattern
属性:
fn check ( value string ) -> invalid_value_error or null const pattern = pattern.create ( "[a-zA-Z ]{3,50}" ) if value.matches_pattern ( pattern ) then return null else return invalid_value_error.create ( message = """'{{value}}' is invalid because it does not match the regular expression '{{pattern}}'.""" id = "INVALID_VALUE" ) . .
请注意,编译器创建的check
函数的实际主体代码各不相同——它取决于源代码中定义的属性。例如,正如我们稍后将看到的,函数返回的错误消息可以通过error_message
属性在源代码中自定义。
每次创建color_name
的实例时,都会隐式调用check
函数。
我们也可以显式调用此函数,例如,检查给定值是否有效。以下示例代码检查"<script>"
是否是有效的颜色名称:
const error = color_name.check ( "<script>" ) if error is null then write_line ( "OK" ) else write_line ( error.message ) .
输出
'<script>' is invalid because it does not match the regular expression '[a-zA-Z ]{3,50}'.
好的。现在考虑类型color_name
的属性name
的定义:
att name color_name
为了检查属性name
,编译器还隐式创建了一个名为check_name
(即前缀check_
,后跟属性标识符)的特定函数。该函数定义在包含该属性的类型RGB_color
中。它有一个类型为string
的输入参数name
,如果值有效则返回null
,否则返回invalid_object_attribute_error
类型的对象(PTS实现中定义的另一个标准库类型)。该函数的主体将检查委托给color_name.check
,并将任何invalid_value_error
对象转换为invalid_object_attribute_error
对象。check_name
的简化版本可能如下所示:
fn check_name ( name string ) -> invalid_object_attribute_error or null const type_error = color_name.check ( name ) if type_error is null then return null else return invalid_object_attribute_error.create ( message = type_error.message id = "INVALID_OBJECT_ATTRIBUTE" ) . .
每次创建RGB_color
的实例时,都会隐式调用check_name
函数。
我们也可以显式调用此函数,例如,检查给定值是否有效。以下代码显示了如何检查"<script>"
是否是属性name
的有效值:
const error = RGB_color.check_name ( "<script>" ) if error is null then write_line ( "OK" ) else write_line ( error.message ) .
输出
'<script>' is invalid because it does not match the regular expression '[a-zA-Z ]{3,50}'.
自定义错误消息
如上一节所示,无效的颜色名称会产生类似这样的错误消息:
'<script>' is invalid because it does not match the regular expression '[a-zA-Z ]{3,50}'.
虽然这种通用的错误消息可以被软件开发人员理解,并且在原型设计期间可能可以接受,但对于不熟悉正则表达式的最终用户来说,它帮助不大。因此,我们可以通过error_message
属性提供自定义的错误消息。此外,我们可以提供自定义错误标识符。这是一个例子:
type color_name = string ( pattern: "[a-zA-Z ]{3,50}" error_message: "A color name must contain between 3 and 50 characters. Only letters and spaces are allowed." error_id: "INVALID_COLOR_NAME" )
注意
在后台,编译器在创建color_name.check
函数时(在函数check
部分描述)会考虑pattern
、error_message
和error_id
属性。
除了硬编码的pattern
、error_message
和error_id
值之外,我们还可以提供一个求值为字符串的表达式。例如,我们可以调用一个函数来从资源文件中检索本地化的错误消息。
属性 Property check
如果字符串确实需要通过正则表达式进行限制,那么使用pattern
属性是可以的。如果我们想要一种更通用的方法来约束字符串,我们可以使用check
属性。此属性需要一个布尔表达式来检查给定的值是否有效。如果布尔表达式求值为true
,则该值有效——否则无效。因此,而不是编写:
type color_name = string ( pattern: "[a-zA-Z ]{3,50}" )
……我们也可以这样写:
type color_name = string ( check: value.matches ( pattern.create ( "[a-zA-Z ]{3,50}" ) ) )
注意
在后台,编译器将使用check
属性来创建一个适当的color_name.check
函数(在函数check
部分描述)。
属性check
可用于约束任何类型——不仅限于字符串,还包括所有其他标量类型、集合、记录类型等。
假设我们需要一个表示素数的类型。首先,我们需要一个函数来检查整数是否是素数。该函数接受一个integer
作为输入并返回一个boolean
。假设函数is_prime_number
已存在于模块math
中。那么类型prime_number
可以轻松定义如下:
type prime_number = integer ( check: math.is_prime_number ( value ) )
现在可以使用prime_number.create ( 17 )
创建一个有效的素数。但是,prime_number.create ( 2 )
将导致编译时错误。
现在假设我们需要一个包含10到20个素数的列表。代码如下:
type prime_numbers = list<prime_number> ( size_range: 10 .. 20 )
在此代码中,size_range
是list
类型支持的特定属性,用于约束列表中元素的数量。
属性 Property check_code
属性check
仅支持可以通过布尔表达式表达的类型约束。这通常足够了。然而,如果我们对属性验证和/或生成的错误需要更精细地控制,我们可以改用check_code
属性。
check_code
的值是在函数check
部分描述的隐式创建函数的函数体。该函数接受属性值作为输入,如果值有效则返回null
,否则返回invalid_object_attribute_error
类型的对象。代码可以执行任何需要的检查,并根据遇到的错误返回更具体、更用户友好的错误消息。
以下是如何使用check_code
属性对属性name
进行操作的示例:
type RGB_color att name string ( check_code: if name.matches ( pattern.create ( "[a-zA-Z ]{3,50}" ) ) then return null . const length = name.length const error_message = case when length < 3: """Name is invalid because it has only {{length}} characters. It must at least have 3 characters.""" when length > 50: """Name is invalid because it has {{length}} characters. More than 50 characters are not allowed.""" otherwise: "Name can only contain letters and spaces." . return invalid_object_attribute_error.create ( message = error_message id = "INVALID_COLOR_NAME" ) . ) ... .
程序化属性验证
如果属性使用受约束的类型,我们可以通过调用编译器为该属性隐式创建的check_{attribute_name}
函数(如函数check
部分所述),在运行时以编程方式检查给定值是否有效。该函数接受属性值作为输入,如果值有效则返回null
,否则返回invalid_object_attribute_error
类型的对象。在这种情况下,函数返回的错误对象包含额外信息(错误消息、标识符等),客户端代码可以对其进行探索——例如,向用户显示即席错误消息。
以下代码显示了如何检查运行时用户提供的值是否对属性name
有效:
const user_value = GUI_dialogs.ask_string ( message = "Please enter a color name" ) if RGB_color.check_name ( user_value ) as input_error is null then GUI_dialogs.info ( message = "Value is OK." ) else GUI_dialogs.error ( title = "Invalid color name" message = input_error.message ) .
记录验证
除了约束单个属性之外,有时还需要约束整个记录实例,使用两个或多个相互关联的属性。
这可以通过在记录级别定义的check
或check_code
属性来实现。
记录 Property check
考虑二维空间中的一个点。假设不允许点(0, 0)
。也就是说,x
和y
不能同时为零。我们可以使用check
属性来指定这一点:
record type point_2D atts type:integer x y . check: not ( x =v 0 and y =v 0 ) .
注意
在PTS语法中,操作符=v
用于比较值,#v
用于否定比较。因此,而不是:
check: not ( x =v 0 and y =v 0 )
……我们也可以这样写:
check: x #v 0 or y #v 0
可以使用error_message
和error_id
属性指定自定义错误消息和标识符:
record type point_2D ... check: not ( x =v 0 and y =v 0 ) error_message: "x and y cannot both be zero." error_id: "INVALID_POINT_2D" .
注意
在后台,编译器会创建函数point_2D.check
,并考虑check
、error_message
和error_id
属性。
该函数每个属性接受一个输入参数,如果值有效则返回null
,否则返回invalid_object_error
类型的对象。
签名如下:
fn check ( x integer, y integer ) -> invalid_object_error or null
属性和记录验证可以组合。例如,如果x
和y
必须在-100..100范围内,我们可以这样做:
record type point_2D atts type:integer ( range: -100 .. 100 ) x y . check: not ( x =v 0 and y =v 0 ) .
记录 Property check_code
与用于单个属性验证的check_code
属性类似,还有一个check_code
属性可用于对记录验证进行更精细地控制。check_code
的值是check
函数的函数体,该函数接受每个属性的一个输入参数,如果值有效则返回null,否则返回invalid_object_error
类型的对象。
让我们看一个例子。
在Java中的记录部分末尾,我们提到了在黑色背景上显示暗文本的风险,导致文本不可读。为了消除这个问题,我们可以定义类型light_RGB_color
,并为此类型用于文本。类型light_RGB_color
具有与先前创建的类型RGB_color
相同的属性。因此,我们可以使用类型继承(稍后解释)。然后我们只需要添加一个check_code
属性来确保颜色是浅色的:
type light_RGB_color inherit RGB_color // inherit all attributes, and function 'to_string' check_code: if color_utils.is_light_color ( red, green, blue ) then return null else return invalid_object_error.create ( message = "The color is too dark." id = "INVALID_DARK_COLOR" ) . . .
注意
颜色算法可能相当复杂(并且该主题绝对超出了本文的范围)。因此,在上面的代码中,我只是假设代码可以访问具有函数is_light_color
的第三方color_utils
库。
到目前为止,我们假设背景色始终是黑色的。然而,如果背景色是可变的,那么我们就需要确保文本颜色和背景颜色的组合能使文本易于阅读。例如,如果文本颜色是(100, 100, 100),背景颜色是(100, 100, 110),那么对比度太低,不应该允许此颜色对。
type readable_RGB_color_pair att text_color RGB_color att background_color RGB_color check_code: if color_utils.is_readable_text ( text_color, background_color ) then return null else return invalid_object_error.create ( message = """Text color {{text_color}} on background color {{background_color}} is unreadable.""" id = "UNREADABLE_TEXT_COLORS" ) . . .
注意
在此代码中,我们假设函数color_utils.is_readable_text
检查对比度是否足够,并考虑了视觉障碍人士的颜色可访问性规则。
有关更多信息,您可以阅读对比度和颜色可访问性和具有良好对比度的颜色。您还可以查看Tristano Ajmone的注释丰富的Delta E 2000 (ΔE*00)算法实现。
程序化记录验证
如果记录的check
或check_code
属性已定义,我们可以通过调用编译器为该类型隐式创建的check
函数,在运行时以编程方式检查一组属性值是否有效以创建对象。以下代码显示了如何检查用户为readable_RGB_color_pair
对象提供的值是否有效:
const error = readable_RGB_color_pair.check ( text_color = user_provided_text_color background_color = user_provided_background_color ) if error is null then write_line ( "OK" ) else write_line ( """Invalid colors: {{error.message}}""" ) .
默认不可变
引用如果您不认为管理状态很棘手,请考虑这样一个事实:所有复杂系统中80%的问题都是通过重启解决的。
— Stuart Halloway
以下是PTS的一项基本规则:所有数据*默认都是不可变的*。
对于记录类型,这意味着在记录已创建/初始化为一组固定值后,重新为属性分配另一个值是无效的。
const black = RGB_color.create ( name = "Black" ) black.name = "Red" <<< INVALID!
属性可以通过mutable
关键字在att
之前显式声明为可变的。在下面的代码中,类型mutable_wrapper
包含一个可变的value
:
record type mutable_wrapper mutable att value type:any .
注意
按照惯例,可变类型的名称以mutable_
前缀开头(例如,mutable_wrapper
)。
创建mutable_wrapper
对象后,可以将另一个值赋给属性value
:
const item = mutable_wrapper.create ( "foo" ) write_object ( item.value ) ... item.value = 123 write_object ( item.value )
输出
foo 123
注意
规则“记录默认不可变”并不一定意味着实例的属性值不能更改。如果属性持有可变类型的值,那么值本身可以改变,但内存中的同一实例仍然被赋值给该属性。
例如,考虑一个持有可变列表的属性。如果向列表中添加一个元素,那么属性值就会改变,尽管该属性仍然指向同一个可变列表。
因此,默认情况下,记录类型中的属性保证是*浅层*不可变的,但不一定是*深层*不可变的。
类型参数
假设我们需要一个记录类型来存储一对字符串。我们可以这样做:
record type string_pair att item_1 string att item_2 string .
如果我们还需要一对整数,我们可以创建另一个类型:
record type integer_pair att item_1 integer att item_2 integer .
如果还需要其他类型的对,例如decimal
、boolean
、date
、time
等,那么这很快就会变得笨拙。
为了避免代码重复,我们可以(但不应该)定义一个*单一*类型pair
,它可以容纳任何类型的项:
record type pair att item_1 any att item_2 any .
然而,这是一个糟糕的解决方案,原因有两个:
-
我们失去了类型安全。
考虑以下代码,我们在对中意外地混合了两种不同的类型:
const pair = pair.create ( item_1="foo" item_2=123 )
没有生成*编译*时错误。更糟糕的是,代码也没有产生*运行时*错误。
这很容易出错。
-
当我们访问一个项时,我们必须将其强制转换为正确的类型(因为每个项都是
any
类型)。我们将不得不编写类似这样的代码:if pair.item_1 as item_1 is string then write_line ( item_1 ) else write_line ( "Error: Type string expected." ) .
这是不切实际的。
如果我们使用*类型参数*,这两个问题都会被消除,如下所示:
record type pair <item_type> att item_1 item_type att item_2 item_type .
在此代码中,我们声明了一个*类型参数*,特意命名为<item_type>
——它是创建pair
类型对象时将确定的具体类型的占位符。例如:
const string_pair pair<string> = pair<string>.create ( item_1="foo" item_2="bar" ) const integer_pair pair<integer> = pair<integer>.create ( item_1=100 item_2=200 )
类型推断允许我们缩短代码:
const string_pair = pair.create ( item_1="foo" item_2="bar" ) const integer_pair = pair.create ( item_1=100 item_2=200 )
如果我们意外地使用了两种不同的类型作为项,则会报告编译时错误。因此,我们再次具有类型安全。
访问项也变得很简单,因为编译器知道项的类型,并且在我们假设错误类型时会产生错误。我们不需要检查类型,只需编写如下代码:
const string_pair = pair.create ( item_1="foo" item_2="bar" ) ... const item_1 string = string_pair.item_1 // type safe! // const test integer = string_pair.item_1 // compile-time error
类型参数(也称为泛型编程、泛型类型参数、泛型等)是类型系统非常有用的补充,因为它们允许我们编写*类型安全*的泛型代码。然而,虽然基本思想很容易理解,但类型参数是一个复杂的主题——根据我的经验,这是类型系统中实现正确的挑战性最大的功能。上面的示例仅仅是冰山一角。为了保持本节简短,我们将不深入细节——让我们看一个例子来说明其好处。
在前一节中,我们定义了类型mutable_wrapper
如下:
record type mutable_wrapper mutable att value type:any .
为了提高类型安全性,我们可以再次使用泛型类型参数:
record type mutable_wrapper <value_type> mutable att value type:value_type .
使用示例:
const string_wrapper = mutable_wrapper.create ( "foo" ) ... string_wrapper.value = "bar" // ok // string_wrapper.value = 123 // compile-time error ... const value string = string_wrapper.value // ok // const test integer = string_wrapper.value // compile-time error
如果我们还需要一个包装器来容纳*任何*类型,我们可以这样做:
const any_wrapper = mutable_wrapper<any>.create ( "foo" ) ... any_wrapper.value = "bar" // ok any_wrapper.value = 123 // ok ... case type of any_wrapper.value when string write_line ( "It's a string." ) when number write_line ( "It's a number." ) otherwise write_line ( "It's something else." ) .
类型继承
注意
类型继承不应与实现继承混淆——这是不同的概念。PTS类型继承类似于C#和Java中的接口继承。本文不涵盖实现继承(例如C#、Java等语言中的类继承)。
考虑一家销售计算机硬件商店的软件应用程序。这里有一个非常简化的product
类型版本:
record type product atts identifier string name string price decimal . .
假设商店出售笔记本电脑和打印机。笔记本电脑有三个额外的string
属性:CPU
、RAM
和hard_disk
。对于打印机,假设我们需要一个额外的boolean
属性:is_color_capable
。当然,商店还出售其他产品(显示器、鼠标、键盘等),其中一些也需要额外的特定属性。
有几种编码方式,具体取决于类型系统支持的功能。然而,据我所知,只有支持*类型继承*才能进行类型安全且实用的编码。类型laptop
可以定义如下:
record type laptop inherit product atts type:string CPU RAM hard_disk . .
行inherit product
表示在类型product
中定义的所有内容也隐式定义在类型laptop
中。因此,类型laptop
还具有属性identifier
、name
和price
。
类型printer
如下所示:
record type printer inherit product att is_color_capable boolean .
除了提供类型安全性外,类型继承还为我们提供了类型可替换性——一个重要且独特的OO特性。类型为laptop
和printer
的对象与类型product
兼容。也就是说,每次需要product
对象时,我们也可以提供laptop
或printer
对象。因此,类型继承支持Liskov替换原则。
类型继承是一个庞大的主题——太庞大了,无法在此完全涵盖。
然而,有一个特定的PTS特性值得在此提及:在子类型中,我们可以重新定义从父类型继承的成员,只要保留Liskov替换原则。就记录类型而言,这允许我们例如减少继承的不可变属性的类型基数,从而提高类型安全性。
为了说明这一点,我们假设产品标识符由两个大写字母后跟6位数字组成:
record type product atts identifier string ( pattern: "[A-Z]{2}\d{6}" ) ... . .
我们还假设笔记本电脑标识符必须以"LT"
开头,打印机标识符必须以"PR"
开头。我们可以通过在类型laptop
和printer
中*重新定义*属性identifier
,并使用and_check
来添加进一步的约束来实现这一点:
record type laptop inherit product redefine att identifier and_check: identifier.starts_with ( "LT" ) . . ... . record type printer inherit product redefine att identifier and_check: identifier.starts_with ( "PR" ) . . ... .
现在,每当创建laptop
对象时,标识符必须通过类型product
中定义的检查*以及*类型laptop
中定义的附加检查。同样,printer
实例也必须通过类型product
*和*printer
中定义的检查。
with
操作符
有时我们需要创建一个不可变记录对象的副本,并修改一个或多个属性。
考虑以下代码:
record type point_3D atts type:integer x y z . . const location = point_3D.create ( x=100 y=200 z=300 )
假设我们想创建一个新的位置,并将z
属性增加100。没有with
操作符,我们可以编写:
const new_location = point_3D.create ( x = location.x y = location.y z = location.z + 100 )
这样的代码在阅读、编写和维护方面都很麻烦,特别是对于具有许多属性的记录类型。
with
操作符简化了代码:
const new_location = location with ( z = location.z + 100 )
如您所见,噪声被消除了。代码被简化为重要的部分,并且不需要更新,如果以后添加、删除或重命名其他属性。
结构化文档
几乎所有编程语言都提供了一种将注释插入源代码的方法。以下是一个PTS中注释的示例:
// single line comment index = 1 // comment at the end of a line /// A multiline comment /// Comments can be nested ./// .///
除了注释之外,我们还需要一种提供*结构化*文档的方法,该文档可以以编程方式检索。就记录类型而言,我们应该能够为整个类型以及每个属性单独添加文档。每个文档对象至少应提供title
和description
。
以下是带有类型和属性name
文档的类型RGB_color
的示例:
type RGB_color \ title: "Named RGB color" \ description: "A named color with red, green, and blue values." att name color_name \ title: "Color name" \ description: "The name of the color." ... .
这里我们使用内置的可选属性title
和description
用于结构化文档目的。
注意
行尾的反斜杠(\
)用作行继续符。没有分号(;
)来标记语句的结束——因此,对于跨越多行的语句需要行继续符。
源代码中的结构化文档可以通过不同方式检索(本文未涵盖)。例如,可以在运行时以编程方式访问文档,以构建用户友好的GUI数据录入表单。将文档检索到常量中的代码如下所示:
const record_title = RGB_color.doc.title const record_description = RGB_color.doc.description const name_doc = RGB_color.atts.name.doc const name_title = name_doc.title const name_description = name_doc.description
PTS实现应允许使用HTML或PML等标记代码对文本进行样式设置。例如,要使用PML以*斜体*显示字段名称,我们可以使用[i ...]
语法,如下所示:
description: "A named color with [i red], [i green], and [i blue] values."
文本呈现如下:
一个带有红色、绿色和蓝色值的命名颜色。
序列化/反序列化记录对象
当记录对象被序列化时,存储在内存中的数据被写入标准(或非标准)文本或二进制格式。
反序列化是逆向过程——它用于检索(读取)持久化在资源中的对象。
序列化/反序列化数据在各种项目中都很有用。例如,它可以用于:
-
通过网络发送数据
-
将对象持久化到文件等资源中
-
以文本形式显示内存中的数据(对调试目的非常有用)
-
在实际数据库不适合时,使用标准操作系统文件作为数据库
-
使用可编辑的文本文件来读取和写入结构化配置文件数据
不幸的是,序列化/反序列化数据的代码编写和维护起来很麻烦,而且容易出错,特别是当它需要针对许多不同的记录类型时。此外,读/写数据不仅对记录有用——它对所有类型的数据(标量值、集合等)都有用。因此,一个实用的类型系统必须提供编写*泛型*代码所需的底层功能。然后我们可以一次又一次地使用这段代码来序列化/反序列化*任何*数据。例如,将RGB_color
对象写入文本格式应该像这样简单:
const red = RGB_color.create ( name="red" red=255) write_line ( "XML:" ) XML_writer.write_object_to_STDOUT ( red ) write_line ( "JSON:" ) JSON_writer.write_object_to_STDOUT ( red ) write_line ( "PDML:" ) PDML_writer.write_object_to_STDOUT ( red )
输出
XML: <?xml version="1.0" encoding="UTF-8"?> <RGB_color> <name>red</name> <red>255</red> <green>0</green> <blue>0</blue> </RGB_color> JSON: { "RGB_color": { "name": "red", "red": 255, "green": 0, "blue": 0 } } PDML: [RGB_color [name red] [red 255] [green 0] [blue 0] ]
注意
有关PDML的更多信息,请访问pdml-lang.dev。
示例
以下是书店应用程序中记录类型的一个简化示例:
type name = string ( pattern: "[a-zA-Z ]{1,70}" ) type phone_number = string ( pattern: "\+?[0-9 ]{3,15}" ) record type author atts first_name name last_name name website URL . fn to_string = """{{first_name}} {{last_name}}""" . record type publisher atts name name phone phone_number . fn to_string = name . record type book atts title string ( length_range: 1 .. 250 ) authors list<author> publisher publisher price decimal ( range: 0.00 .. 1_000.00, decimals: 2 ) . variable att stars \ type: decimal ( range: 1.0 .. 5.0 ) or null \ default: null fn to_string = """{{title}} by {{authors.to_string}}""" .
摘要
PTS为记录类型提供了以下关键特性:
-
简洁明了的语法——没有噪音或代码重复
-
默认不可变
-
内置支持,易于灵活地进行数据验证,包括单个属性和整个记录
-
属性的默认值
-
命名属性赋值,以提高可读性、可靠性和可维护性
-
类型参数,用于编写泛型、类型安全的代码
-
类型继承
-
with
操作符,用于方便地创建具有修改值的记录副本 -
to_string
函数,用于生成记录对象的简短、人类可读的描述 -
结构化文档
-
支持序列化/反序列化记录对象
这些特性旨在简化和优化与记录类型的工作,同时提高可靠性、可维护性和生产力。
特别是,*数据验证*和*默认不可变*的结合有助于定义非常健壮的记录类型。记录对象只能在有效状态下创建,创建后状态不能更改。对象在其整个生命周期内都保持有效、不可变的状态。这消除了许多通常难以查找和修复的bug类别,例如由非原子或非同步状态更改、竞争条件以及并行/并发运行时环境中共享可变状态引起的其他棘手问题。
此外,*命名属性赋值*和*默认值*的结合使我们能够使用不同的输入参数组合可靠地创建记录,而无需定义重载构造函数或使用构建器模式。
下一步?
在下一篇文章中,我们将探讨*联合类型*,这是实用类型系统中一个出乎意料有用的特性。
致谢
非常感谢Tristano Ajmone提供的宝贵反馈,以改进本文。