Dart Record(记录元组)

一、什么是Record?

Record 是匿名、不可变、复合聚合类型,用来把多个不同类型数据打包成单个对象。 和 List / Set / Map 对比核心区别:

  • 固定长度:一旦创建,字段数量不能增减 异构存储:
  • 同一个 Record 里可以放 int、String、bool 等完全不同类型
  • 强类型约束:每个字段类型被编译器识别,不会丢失类型安全
  • 只读不可改:创建后字段值无法重新赋值

Record 是完整合法变量,支持所有常规操作:存变量、函数传参 / 多返回值、嵌套、放进 List/Map/Set。

二、Record 基础语法

1. Record 字面量

括号包裹,逗号分隔,分为位置字段命名字段两类;位置字段写最前面,命名字段带 key: 写在后面。

// 位置字段 + 命名字段混合
var info = ('Jim', age: 20, flag: true, 'Student');Code language: JavaScript (javascript)

2. Record 用法

1 位置字段

// 声明:两个位置字段,依次 String、int
(String, int) user;

// 赋值匹配结构
user = ('李四', 22);
Code language: JavaScript (javascript)

2 命名字段

{} 包裹类型声明

// 声明两个命名字段 a(int)、b(bool)
({int a, bool b}) data;

data = (a: 100, b: false);Code language: JavaScript (javascript)

3 混合使用

位置字段 + 命名字段混合标注

// 1个位置String,2个命名字段score(int)、pass(bool)
(String, {int score, bool pass}) exam;
exam = ('期末考', score: 90, pass: true);
Code language: JavaScript (javascript)

三、易错

1:命名字段的名字属于类型结构,名字不一样就是两种完全不同类型
// 类型:({int a, int b})
({int a, int b}) pointAB = (a: 1, b: 2);
// 类型:({int x, int y})
({int x, int y}) pointXY = (x: 3, y: 4);

// 报错!类型不匹配,不能互相赋值
// pointAB = pointXY;
Code language: JavaScript (javascript)

对于命名字段,虽然都是int int 但是因为名字不应用 一个是ab 一个是xy 所以不能互相赋值。除非是位置字段,看下面

2:位置字段可以在类型注解里写别名,仅做注释,不影响类型匹配

括号里给位置字段起的名字只是给人看的文档标注,编译器忽略,结构一致就能互相赋值:

(int a, int b) p1 = (10, 20);
(int x, int y) p2 = (30, 40);

// 合法,底层都是两个int位置字段,类型完全一致
p1 = p2;Code language: JavaScript (javascript)

类比函数形参:函数参数起名不改变函数签名,逻辑一致。

四、读取 Record 内部字段

Record 不可变,只有 getter 没有 setter,无法修改字段值。

  1. 命名字段:直接 .字段名 获取
  2. 位置字段:用 $数字 获取,数字跳过所有命名字段,只统计位置字段顺序
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // 第一个位置字段:first
print(record.a);  // 命名字段a:2
print(record.b);  // 命名字段b:true
print(record.$2); // 第二个位置字段:last
Code language: PHP (php)

五、Record 结构类型判定规则

Record 没有单独 class 定义,依靠结构形状判定同类型:

形状 = 字段数量 + 每个字段的类型 + 命名字段的名称

只要形状完全一致,就算来自不同文件、不同库,也属于同一个类型。

每个字段静态类型编译器全程识别,不会丢失类型信息:

// 位置1 num类型,位置2 Object类型
(num, Object) pair = (42, '一段文字');

var firstVal = pair.$1; // 静态类型 num,运行实际 int
var secondVal = pair.$2;// 静态类型 Object,运行实际 StringCode language: JavaScript (javascript)

六、Record 相等判断 ==

满足两点才会相等:

  1. 两个 Record 形状完全一致
  2. 对应位置 / 命名字段的值全部相等

命名字段书写顺序不影响相等判断;但命名字段名称不同直接判定类型不同,一定不相等。

// 位置字段,形状相同,数值相等 → true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // true

// 命名字段名字不一样,类型不同 → false
({int x, int y, int z}) p = (x:1, y:2, z:3);
({int r, int g, int b}) c = (r:1, g:2, b:3);
print(p == c); // falseCode language: PHP (php)

Record 自动内置重写 ==hashCode,不用手动实现。

七、函数多返回值

Dart 函数只能单个返回值,用 Record 打包多组不同类型数据,搭配模式解构快速拆分成独立变量,完美替代 List/Map(不会丢失类型)。

1位置字段解构

// 返回 姓名(String)、年龄(int)
(String name, int age) getUserInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10, 'color': 'blue'};
  // 直接解构,一行拆分两个变量
  var (userName, userAge) = getUserInfo(userJson);
  print(userName); // Dash
  print(userAge);  // 10
}
Code language: JavaScript (javascript)

2命名字段解构

:字段名

({String name, int age}) getUserInfo(Map<String, dynamic> json) {
  return (name: json['name'] as String, age: json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10};
  // 命名解构写法
  final (:name, :age) = getUserInfo(userJson);
  print(name);
}
Code language: JavaScript (javascript)

对比劣势

如果以前,像其他的编程语言,例如 C++ C# JAVA 这些 不用 Record 需要如下操作:

  1. 新建 class:代码冗余,要写构造、字段;
  2. List 返回:丢失类型安全,全部变成 dynamic;
  3. Map 返回:键容易写错,类型无法静态校验。

八、轻量结构化数据

不用新建类,只需存储数据、不需要方法时,直接用 Record,省去定义 class 的冗余代码,适合批量列表数据。

示例

页面按钮配置列表(如下例子是flutter中的例子,读者了解就可以,等学了flutter,就会理解)

import 'package:flutter/material.dart';

void main() {
  final buttonList = [
    (
      label: "上传文件",
      icon: const Icon(Icons.upload_file),
      onPressed: () => print("点击上传按钮"),
    ),
    (
      label: "查看详情",
      icon: const Icon(Icons.info),
      onPressed: () => print("点击详情按钮"),
    )
  ];
}
Code language: JavaScript (javascript)

九、typedef 给 Record 类型起别名

简化长类型

Record 类型书写较长,可以用 typedef 定义类型别名,复用、方便后期统一修改字段结构。

基础使用
// 给按钮Record起别名,onPressed允许为空
typedef ButtonConfig = ({
  String label,
  Icon icon,
  void Function()? onPressed
});

// 直接使用别名声明列表
List<ButtonConfig> buttons = [
  (label: "提交", icon: const Icon(Icons.check), onPressed: () {}),
  (label: "取消", icon: const Icon(Icons.close), onPressed: null),
];
Code language: PHP (php)
后续

如果后续需要给数据增加方法、封装逻辑,不用修改业务遍历代码,仅替换类型即可:

  1. 方案 1:替换为普通 Class
class ButtonConfig {
  final String label;
  final Icon icon;
  final void Function()? onPressed;
  ButtonConfig({required this.label, required this.icon, this.onPressed});
  // 新增自定义方法
  bool get hasClickEvent => onPressed != null;
}
Code language: PHP (php)
  1. 方案 2:替换为 Extension Type 包装原有 Record
extension type ButtonConfig._(({String label, Icon icon, void Function()? onPressed}) _) {
  String get label => _.label;
  Icon get icon => _.icon;
  void Function()? get onPressed => _.onPressed;

  ButtonConfig({required String label, required Icon icon, void Function()? onPressed})
      : this._((label: label, icon: icon, onPressed: onPressed));

  bool get hasClickEvent => _.onPressed != null;
}
Code language: PHP (php)

两种改造方式下,页面循环渲染按钮的业务代码完全不用改动。

十、总结

  1. 版本门槛:Dart 3.0+;
  2. 三大核心属性:匿名、不可变、聚合;固定长度、异构、强类型;
  3. 字段两类:位置字段 $1/$2、命名字段 .name
  4. 类型判定:结构形状决定类型,命名字段名称参与形状判定;位置字段别名仅注释;
  5. 相等判断:形状相同、所有字段值相等即为 true;
  6. 核心优势:函数多返回值、轻量数据容器、完整类型安全;
  7. 拓展方案:typedef 简化类型别名,后期可无缝替换 class /extension type。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注