Dart Records&パターンマッチング
Dart 3.0 — 多値返却、分割代入、switchパターン
Dart 3.0で導入されたRecordsは、クラス定義なしで複数の値を括弧でグループ化する不変型です。位置ベース($1, $2)や名前付きフィールド(.name, .age)でアクセスし、分割代入で一行で複数変数を抽出できます。パターンマッチングと組み合わせればswitch/if-caseで型と条件を同時検査し、コードを大幅に簡素化できます。
Records란?
Dart 3.0에서 도입된 Records는 클래스 정의 없이 여러 값을 하나의 객체로 그룹화하는 불변(immutable) 컬렉션 타입입니다. 구조적 타이핑(structural typing)을 사용하며, 같은 필드 구조를 가지면 같은 타입으로 취급됩니다.
1. 위치 기반 레코드 (Positional Records)
괄호 안에 값을 나열하면 위치 기반 레코드가 됩니다. $1, $2로 접근합니다.
var person = ('홍길동', 30);
print(person); // (홍길동, 30)
print(person.$1); // 홍길동
print(person.$2); // 30
2. 명명된 레코드 (Named Records)
필드에 이름을 부여하면 가독성이 크게 향상됩니다. 위치 기반과 혼합 사용도 가능합니다.
// 명명된 필드
var person = (name: '홍길동', age: 30);
print(person.name); // 홍길동
print(person.age); // 30
// 위치 + 명명 혼합
var data = ('홍길동', age: 30, active: true);
print(data.$1); // 홍길동
print(data.age); // 30
print(data.active); // true
3. 타입 어노테이션
레코드 변수에 명시적 타입을 지정할 수 있습니다.
// 위치 기반 타입
(String, int) person = ('홍길동', 30);
// 명명된 필드 타입
({String name, int age}) person = (name: '홍길동', age: 30);
// 혼합 타입
(String, {int age, bool active}) data = ('홍길동', age: 30, active: true);
4. 함수에서 다중 값 반환
Records의 가장 실용적인 사용 사례입니다. 별도 클래스 없이 함수에서 여러 값을 반환할 수 있습니다.
(String, int) getUserInfo() {
return ('홍길동', 30);
}
void main() {
var (name, age) = getUserInfo();
print('이름: $name, 나이: $age');
// 이름: 홍길동, 나이: 30
}
5. 구조 분해 (Destructuring)
레코드의 각 필드를 개별 변수로 추출합니다. 명명된 레코드는 축약 구문도 지원합니다.
var person = (name: '홍길동', age: 30);
// 전체 구조 분해 (변수명 변경)
var (name: userName, age: userAge) = person;
print('이름: $userName, 나이: $userAge');
// 축약 구조 분해 (필드명 그대로)
var (:name, :age) = person;
print('이름: $name, 나이: $age');
6. 동등성 비교 (Equality)
Records는 값 기반 동등성을 지원합니다. 같은 구조와 값을 가지면 동일한 것으로 판단합니다.
var person1 = (name: '홍길동', age: 30);
var person2 = (name: '홍길동', age: 30);
var person3 = (name: '김철수', age: 25);
print(person1 == person2); // true
print(person1 == person3); // false
var p1 = ('홍길동', 30);
var p2 = ('홍길동', 30);
print(p1 == p2); // true
7. switch 패턴 매칭
Dart 3.0의 패턴 매칭은 switch문에서 타입 검사와 조건을 동시에 수행합니다. when 가드로 추가 조건도 걸 수 있습니다.
void describe(Object obj) {
switch (obj) {
case (String name, int age):
print('이름: $name, 나이: $age');
default:
print('기타 객체: $obj');
}
}
// when 가드 사용
void process(dynamic value) {
switch (value) {
case (String n, int a) when a >= 18:
print('성인: $n, $a살');
case (String n, int a):
print('미성년자: $n, $a살');
default:
print('기타 값: $value');
}
}
process(('홍길동', 30)); // 성인: 홍길동, 30살
process(('김영희', 15)); // 미성년자: 김영희, 15살
8. if-case 패턴 매칭
if문 안에서 패턴 매칭을 수행합니다. JSON 파싱이나 타입 검증에 특히 유용합니다.
void processValue(Object value) {
if (value case (String name, int age)) {
print('이름: $name, 나이: $age');
} else if (value case String s when s.length > 5) {
print('긴 문자열: $s');
} else {
print('처리할 수 없는 값: $value');
}
}
processValue(('홍길동', 30));
// 이름: 홍길동, 나이: 30
9. 중첩 패턴 매칭
리스트와 레코드를 조합한 복잡한 구조도 패턴으로 분해할 수 있습니다.
var data = [('홍길동', 30), ('김철수', 25)];
if (data case [(String s, int i), var rest]) {
print('첫 번째 사람: $s, $i살');
// 첫 번째 사람: 홍길동, 30살
print('나머지: $rest');
// 나머지: (김철수, 25)
}
10. 실전 예제: 통계 계산
Records로 최솟값, 최댓값, 평균을 한 번에 반환하는 함수입니다.
(double min, double max, double average) calculateStats(
List<double> values) {
if (values.isEmpty) return (0, 0, 0);
double sum = 0;
double min = values[0];
double max = values[0];
for (var value in values) {
sum += value;
if (value < min) min = value;
if (value > max) max = value;
}
return (min, max, sum / values.length);
}
void main() {
var numbers = [10.5, 25.3, 17.2, 8.7, 30.1];
var (min, max, avg) = calculateStats(numbers);
print('최소값: $min'); // 최소값: 8.7
print('최대값: $max'); // 최대값: 30.1
print('평균값: $avg'); // 평균값: 18.36
}
11. 실전 예제: API 응답 처리
Records와 패턴 매칭을 결합하면 API 응답을 안전하게 처리할 수 있습니다.
(bool success, {String? data, String? error})
fetchUserData(String userId) {
if (userId == 'user123') {
return (true,
data: '{"name": "홍길동"}',
error: null);
} else {
return (false,
data: null,
error: '사용자를 찾을 수 없습니다.');
}
}
void main() {
var result = fetchUserData('user123');
if (result.$1) {
print('데이터: ${result.data}');
}
var fail = fetchUserData('unknown');
if (!fail.$1) {
print('오류: ${fail.error}');
}
}
핵심 정리
- Records = 클래스 없는 불변 다중 값 컨테이너 (Dart 3.0+)
- 위치 기반($1, $2) vs 명명 기반(.name, .age) 선택 가능
- 구조 분해로 한 줄에 여러 변수 추출
- 값 기반 동등성 — 같은 구조+값이면 == true
- 패턴 매칭(switch/if-case)과 결합하면 타입+조건 동시 검사 가능
実装ステップ
位置ベースレコード — var result = ('成功', 200); → result.$1, result.$2でアクセス
名前付きレコード — var user = (name: 'ホン', age: 30); → user.name, user.ageで可読性向上
分割代入 — var (:name, :age) = user;で一行で複数変数抽出
switchパターンマッチング — case String s when s.isNotEmpty: 型と条件を同時検査、case _:でワイルドカード
if-case — if (json case {'name': String name}) { }形でJSON解析と型検証を一度に
メリット
- ✓ クラスなしで軽量に複数値をグループ化して返却可能
- ✓ パターンマッチングでif-elseチェーンをきれいなswitchに置換
デメリット
- ✗ Dart 3.0以上でのみ使用可能
- ✗ 複雑なネストパターンはむしろ可読性を害する可能性