Android学Dart学习笔记第五节 记录Records
与 List/Map 等集合也不同,它允许为其中每一个位置的元素指定独立的类型,提供了更强的类型安全性。对于使用类型别名作为引用的代码,类型别名不提供任何保护或保证,即被别名的值是一个记录。译文:如果两个记录具有相同的形状(字段集合),并且它们对应的字段具有相同的值,则它们是相等的。记录是基于字段类型的结构类型。记录的形状(字段的集合,字段的类型,以及它们的名称,如果有的话)唯一地决定了记录的类型
序言
对于Android程序员来说,这是一个全新的概念,需要好好学习。记录需要至少3.0的Dart语言版本。
文档描述
Records are an anonymous, immutable, aggregate type. Like other collection types, they let you bundle multiple objects into a single object. Unlike other collection types, records are fixed-sized, heterogeneous, and typed.
Records are real values; you can store them in variables, nest them, pass them to and from functions, and store them in data structures such as lists, maps, and sets.
译文:记录是一种匿名的、不可变的聚合类型。与其他集合类型一样,它们允许您将多个对象捆绑到单个对象中。与其他集合类型不同,记录是固定大小的、异构的和类型化的。
记录是真实的值;您可以将它们存储在变量中,嵌套它们,将它们传递给函数或从函数传递,并将它们存储在列表、映射和集合等数据结构中。
理解:匿名、不可变、固定大小、异构、类型化
接下来继续学习
Record syntax 记录的语法
Records expressions are comma-delimited lists of named or positional fields, enclosed in parentheses
译文:记录表达式是用逗号分隔的命名字段或位置字段列表,用圆括号括起来
下面是官方的例子:
var record = ('first', a: 2, b: true, 'last');
print(record);
//(first, last, a: 2, b: true)
看起来还是不容易理解,这个括号里的内容相当乱,有string,还有a:int,还有b:true,最后打印更是连带括号打出来了,并且打印出来的元素顺序高改变了。
接着看文档
Record type annotations are comma-delimited lists of types enclosed in parentheses. You can use record type annotations to define return types and parameter types
译文:记录类型注释是括在括号中的以逗号分隔的类型列表。您可以使用记录类型注解来定义返回类型和参数类型。
下面又是一个例子:
这是一个函数,他的参数被约束为(int, int)类型的record,返回也是。
(int, int) swap((int, int) record) {
var (a, b) = record;
return (b, a);
}
我想测试一下这个函数的运行效果,so:
var record = ('first', a: 2, b: true, 'last');
record = (1, 2);//record之前的类型是(String, String, {int a, bool b}),现在变成了(int, int)
print(record);
swap(record);
报错了,record的实际类型是由第一次创建时的子元素决定的,不可更改,所以改变一下代码
void main() {
// var record = ('first', a: 2, b: true, 'last');
var record = (1, 2);//record之前的类型是(String, String, {int a, bool b}),现在变成了(int, int)
print(record);//(1, 2)
var result = swap(record);
print(result);//(2, 1)
}
(int, int) swap((int, int) record) {
var (a, b) = record;
return (b, a);
}
可以看到,这只是一个用来交换record(int,int)类型内子元素顺序的函数。
继续看文档
Fields in record expressions and type annotations mirror how parameters and arguments work in functions
译文:记录表达式和类型注解中的字段反映了函数中形参和实参的工作方式
看下面给出的例子:
//Record type annotation in a variable declaration:
(int, int) record;//变量声明中的记录类型注释
//Initialize it with a record expression
record = (1, 2);//用一个记录表达式初始化它
print(record);//(1, 2)
这是record类型,声明和赋值的方式。
In a record type annotation, named fields go inside a curly brace-delimited section of type-and-name pairs, after all positional fields. In a record expression, the names go before each field value with a colon after:
译文:在记录类型注释中,命名字段放在以花括号分隔的类型和名称对中,在所有位置字段之后。在记录表达式中,名称放在每个字段值之前,后面有一个冒号:
看例子:
({int a, bool b}) record; //变量声明中的记录类型注释
// Initialize it with a record expression://用一个记录表达式初始化它
record = (a: 123, b: true);
当我们想要给record中的元素起名字时,就需要用到花括号。
结合我们前文中提到的元素顺序被改变,我们可以声明一个一部分有名字,一部分没名字的record,如下:
(int,{int a, bool b}) record2;//变量声明中的记录类型注释
// Initialize it with a record expression://用一个记录表达式初始化它
record2 = (123, a: 123, b: true);
那么这个字段名又有什么作用呢?我们尝试赋值时调换一下a、b的位置,答案是可行的,如下
record2 = (123, b: true, a: 123);//编译通过
The names of named fields in a record type are part of the record’s type definition, or its shape. Two records with named fields with different names have different types
译文:记录类型中命名字段的名称是记录类型定义或其形状的一部分。两个具有不同名称的命名字段的记录具有不同的类型。
很好理解,如果两个record的子元素类型和值完全一致(不可以多字段或者少字段),但元素字段命名不一致,则两者类型不相等。
请看下面的例子:
({int a}) record = (a: 123);
({int b}) record2 = (b: 123);
({int a}) record3 = (a: 123);
record = record3;//编译OK
record = record2;//编译错误,不能将({int b})类型的记录赋值给({int a})类型的变量
严谨起见,我们对比一起未命名的record,结果是同上的,如下:
var record = (1);
var record2 = (1);
var record3 = ("1");
record = record2;//编译OK
record = record3;//编译错误,不能将(String)类型的记录赋值给(int)类型的变量
In a record type annotation, you can also name the positional fields, but these names are purely for documentation and don’t affect the record’s type:
译文:在记录类型注解中,你也可以给位置字段命名,但这些名字纯粹是文档用的,不会影响记录的类型:
看下面的例子:
(int a, int b) record = (1, 2);
(int x, int y) record2 = (2, 3);
record = record2;//编译OK
意外的发现
(int a,) record = (1,);
意外的发现,如果record只有一个值,仍然需要以,分割
小结
record是一种异构的,固定的(不支持添加或删除元素)容器,支持匿名,或命名。当命名字段使用{}包裹时,赋值或比较时,会校验命名是否相同。
Record fields 记录的属性
Record fields are accessible through built-in getters. Records are immutable, so fields do not have setters.
译文:记录字段可以通过内置的getter访问。记录是不可变的,因此字段没有设置器。
Named fields expose getters of the same name.Positional fields expose getters of the name <position>,skippingnamedfields:译文:命名字段提供同名的获取方法。位置字段暴露名称为<position>, skipping named fields: 译文:命名字段提供同名的获取方法。位置字段暴露名称为<position>,skippingnamedfields:译文:命名字段提供同名的获取方法。位置字段暴露名称为的getter,跳过命名字段:
var record = ('first', a: 2, b: true, 'last');
print(record);//prints(first, last, a: 2, b: true)
print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'
下面是record提供的字段列表
注意看上面的例子
record.$1输出为first,说明这里的position是以1开始的而不是0;
record.$2输出为last,结合我们之前发现的顺序调整,顺序调整后,未命名字段会自动向前排列;
命名字段可以直接使用名称调用,不可以使用position。
Record types 记录的类型
There is no type declaration for individual record types. Records are structurally typed based on the types of their fields. A record’s shape (the set of its fields, the fields’ types, and their names, if any) uniquely determines the type of a record.
译文:没有针对单个记录类型的类型声明。记录是基于字段类型的结构类型。记录的形状(字段的集合,字段的类型,以及它们的名称,如果有的话)唯一地决定了记录的类型。
Each field in a record has its own type. Field types can differ within the same record. The type system is aware of each field’s type wherever it is accessed from the record:
译文:记录中的每个字段都有自己的类型。同一条记录中的字段类型可能不同。类型系统知道记录中每个字段的类型:
(num, Object) pair = (42, 'a');
var first = pair.$1; // Static type `num`, runtime type `int`.
var second = pair.$2; // Static type `Object`, runtime type `String`.
记录元素的类型实际取决于值的实际类型。
Consider two unrelated libraries that create records with the same set of fields. The type system understands that those records are the same type even though the libraries are not coupled to each other.
译文:考虑两个不相关的库,它们使用相同的字段集创建记录。即使库之间没有耦合,类型系统也知道这些记录是相同的类型。
Record equality 记录的相等
Two records are equal if they have the same shape (set of fields), and their corresponding fields have the same values. Since named field order is not part of a record’s shape, the order of named fields does not affect equality.
译文:如果两个记录具有相同的形状(字段集合),并且它们对应的字段具有相同的值,则它们是相等的。由于命名字段的顺序不是记录形状的一部分,因此命名字段的顺序并不影响相等性。
这一段我们在之前也有涉及过
看例子:
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // Prints 'true'.
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);
print(point == color); // Prints 'false'. Lint: Equals on unrelated types.
比较相等,对于非{}包含的内容只对比类型,而对于{}包含的内容还会对比命名是否相同。
Multiple returns 记录作为返回值可以一次返回多个值
Records allow functions to return multiple values bundled together. To retrieve record values from a return, destructure the values into local variables using pattern matching.
译文:记录允许函数返回多个捆绑在一起的值。要从返回值中检索记录值,可以使用模式匹配将值解构为局部变量。
看例子:
// Returns multiple values in a record:
(String name, int age) userInfo(Map<String, dynamic> json) {
return (json['name'] as String, json['age'] as int);
}
final json = <String, dynamic>{'name': 'Dash', 'age': 10, 'color': 'blue'};
// Destructures using a record pattern with positional fields:
var (name, age) = userInfo(json);
/* Equivalent to:
var info = userInfo(json);
var name = info.$1;
var age = info.$2;
*/
在这一段中,详细的描述了记录在实际应用中的使用场景。
我们常常涉及到修改函数之前的返回值来实现新的功能,之前只能使用容易包裹,现在记录是一种不错的灵活应用。因为他可以通过字段名约束,而不要求每个元素的类型统一,但每个元素都可以自己约束类型。
You can also destructure a record using its named fields, using the colon : syntax, which you can read more about on the Pattern types page:
你也可以使用冒号:语法,用它的命名字段解构记录,你可以在模式类型页面上阅读更多关于它的信息
支持通过命名字段结构。
看例子
({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// Destructures using a record pattern with named fields:
final (:name, :age) = userInfo(json);
You can return multiple values from a function without records, but other methods come with downsides. For example, creating a class is much more verbose, and using other collection types like List or Map loses type safety.
译文:你可以从一个没有记录的函数中返回多个值,但其他方法也有缺点。例如,创建类要冗长得多,使用其他集合类型(如List或Map)会失去类型安全性
官方说明了record的使用场景,和我们上文中感受一致。
Records as simple data structures 记录作为便捷的数据结构
Records only hold data. When that’s all you need, they’re immediately available and easy to use without needing to declare any new classes. For a simple list of data tuples that all have the same shape, a list of records is the most direct representation.
译文:记录只保存数据。当这就是你所需要的全部时,它们立即可用且易于使用,无需声明任何新类。对于具有相同形状的数据元组的简单列表,记录列表是最直接的表示方式。
接下来是例子
final buttons = [
(
label: "Button I",
icon: const Icon(Icons.upload_file),
onPressed: () => print("Action -> Button I"),
),
(
label: "Button II",
icon: const Icon(Icons.info),
onPressed: () => print("Action -> Button II"),
)
];
当我们仅仅需要临时存储一些数据时,record会更加灵活和便捷,无需声明对象而后再去初始化。
This code can be written directly without needing any additional declarations.
译文:这段代码可以直接编写,而不需要任何额外的声明。
Records and typedefs
You can choose to use typedefs to give the record type itself a name, and use that rather than writing out the full record type. This method allows you to state that some fields can be null (?), even if none of the current entries in the list have a null value.
译文:你可以选择使用typedefs来给记录类型本身一个名称,并使用它而不是写出完整的记录类型。这个方法允许你声明某些字段可以为null(?),即使列表中当前条目都没有null值。
看下面的例子
typedef ButtonItem = ({String label, Icon icon, void Function()? onPressed});
final List<ButtonItem> buttons = [
// ...
];
Because record types are structural types, giving a name like ButtonItem only introduces an alias that makes it easier to refer to the structural type
译文:你可以选择使用typedefs来给记录类型本身一个名称,并使用它而不是写出完整的记录类型。这个方法允许你声明某些字段可以为null(?),即使列表中当前条目都没有null值。
我们可以给记录本身起个别名,来使他更好的被引用。
Having all your code refer to a record type by its alias makes it easier to later change the record’s implementation without needing to update every reference.
译文:让你所有的代码都通过它的别名引用一个记录类型,这使得以后更改记录的实现更容易,而不需要更新每个引用。
Code can work with the given button definitions the same way it would with simple class instances:
译文:代码可以像处理简单的类实例一样处理给定的按钮定义:
List<Container> widget = [
for (var button in buttons)
Container(
margin: const EdgeInsets.all(4.0),
child: OutlinedButton.icon(//注意这里的值,和正常的对象没有引用区别
onPressed: button.onPressed,
icon: button.icon,
label: Text(button.label),
),
),
];
You could even decide to later change the record type to a class type to add methods:
译文:你甚至可以决定稍后将记录类型更改为类类型以添加方法:
class ButtonItem {
final String label;
final Icon icon;
final void Function()? onPressed;
ButtonItem({required this.label, required this.icon, this.onPressed});
bool get hasOnpressed => onPressed != null;
}
这部分是真无聊啊,总的意思就是变更为对象类非常容易,调用方都不需要变更。
Or to an extension type
或者扩展类型
extension type ButtonItem._(({String label, Icon icon, void Function()? onPressed}) _) {
String get label => _.label;
Icon get icon => _.icon;
void Function()? get onPressed => _.onPressed;
ButtonItem({required String label, required Icon icon, void Function()? onPressed})
: this._((label: label, icon: icon, onPressed: onPressed));
bool get hasOnpressed => _.onPressed != null;
}
And then create the list of button definitions using that type’s constructors:
然后使用该类型的构造函数创建按钮定义的列表:
final List<ButtonItem> buttons = [
ButtonItem(
label: "Button I",
icon: const Icon(Icons.upload_file),
onPressed: () => print("Action -> Button I"),
),
ButtonItem(
label: "Button II",
icon: const Icon(Icons.info),
onPressed: () => print("Action -> Button II"),
)
];
Again, all while not needing to change the code that uses that list.
译文:同样,所有这些都不需要更改使用该列表的代码。
Changing any type does require the code using it to be very careful about not making assumptions. A type alias does not offer any protection or guarantee, for the code using it as a reference, that the value being aliased is a record. Extension types, also, offer little protection. Only a class can provide full abstraction and encapsulation.
更改任何类型都要求使用它的代码非常小心,不要做任何假设。对于使用类型别名作为引用的代码,类型别名不提供任何保护或保证,即被别名的值是一个记录。扩展类型也提供很少的保护。只有类可以提供完整的抽象和封装。
结束
总算结束了,小小的总结下:
Dart 中的 Record 是一个灵活轻量的匿名结构化数据工具。与需要预定义的普通类不同,它无需声明即可直接使用;与 List/Map 等集合也不同,它允许为其中每一个位置的元素指定独立的类型,提供了更强的类型安全性。
在使用时需注意:Record 的相等性比较取决于其结构。使用 () 语法时,按字段位置和值进行比较;而使用 {} 命名字段语法时,则会按字段名称和值进行比较。
为了提升代码可读性和可维护性,建议使用 typedef 为其设置别名。这样做还有一个额外优势:当未来业务复杂、需要增加行为时,可以平滑地将 Record 别名升级为完整的 Class 或扩展类型。
最后,关键在于理解 Record 与 Class 的适用场景:Record 适用于临时性的数据组和多个返回值,而 Class 则用于封装数据和行为的复杂实体。应根据需求合理选择,避免滥用。
更多推荐



所有评论(0)