一、什麼是 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 欄位,但命名欄位名稱一個是 a、b,另一個是 x、y,因此型別不互通;位置欄位則不受名稱影響,參考下方說明。
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 無法修改欄位數值。
- 命名欄位:直接使用
.欄位名取得數值 - 位置欄位:透過
$數字取得,數字只統計位置欄位順序,會跳過所有命名欄位
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 相等判斷 ==
兩個 Record 滿足兩個條件才會判定相等:
- 兩者結構外型完全一致
- 對應位置欄位、命名欄位的數值全部相等
命名欄位書寫順序不影響相等判斷;但命名欄位名稱不同會直接判定型別相異,一定不相等。
// 位置欄位,外型相同、數值相等 → 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)
不用 Record 的缺點對照
若參考 C++、C#、Java 等語言,不使用 Record 實現多回傳值會有以下問題:
- 新建 class:程式碼冗長,需手動撰寫建構子、成員欄位;
- 透過 List 回傳:失去型別安全,所有元素自動變為 dynamic;
- 透過 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)
後續擴充改造方案
若後續需要為資料新增方法、封裝邏輯,不用修改頁面遍歷邏輯,僅替換型別即可:
- 方案 2:使用 Extension Type 包裝原有 Record
- 版本需求:Dart 3.0 以上才支援;
- 三大核心特性:匿名、不可變、聚合;長度固定、異質存放、完整強型別;
- 欄位分兩類:位置欄位透過
$1/$2取值、命名欄位透過.欄位名取值; - 型別判定規則:由結構外型決定型別,命名欄位名稱會參與外型判斷;位置欄位的別名僅作備註,不影響型別;
- 相等判斷:外型完全相同、所有欄位數值相等才會回傳 true;
- 核心優勢:實現函式多組回傳值、輕量資料容器、全程保有型別安全;
- 擴充方案:使用 typedef 簡化長型別,後續可無痛替換為 class / extension type。
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:直接替換為一般 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)