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 欄位,但命名欄位名稱一個是 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 無法修改欄位數值。

  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 相等判斷 ==

兩個 Record 滿足兩個條件才會判定相等:

  1. 兩者結構外型完全一致
  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)

不用 Record 的缺點對照

若參考 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. 方案 2:使用 Extension Type 包裝原有 Record
  2. 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 取值、命名欄位透過 .欄位名 取值;
    4. 型別判定規則:由結構外型決定型別,命名欄位名稱會參與外型判斷;位置欄位的別名僅作備註,不影響型別;
    5. 相等判斷:外型完全相同、所有欄位數值相等才會回傳 true;
    6. 核心優勢:實現函式多組回傳值、輕量資料容器、全程保有型別安全;
    7. 擴充方案:使用 typedef 簡化長型別,後續可無痛替換為 class / extension type。

  1. 方案 1:直接替換為一般 Class
  2. 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)

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *