How to Use Dart Records and Pattern Matching
Dart 3.0 — multiple returns, destructuring, switch patterns
Records, introduced in Dart 3.0, are immutable types that group multiple values with parentheses without defining classes. Access via positional ($1, $2) or named fields (.name, .age), with destructuring to extract multiple variables in one line. Combined with pattern matching, switch/if-case can check types and conditions simultaneously, greatly simplifying code.
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)과 결합하면 타입+조건 동시 검사 가능
Implementation Steps
Positional record — var result = ('success', 200); access via $1, $2
Named record — var user = (name: 'Hong', age: 30); → user.name, user.age for readability
Destructuring — var (:name, :age) = user; extracts multiple variables in one line
switch pattern matching — case String s when s.isNotEmpty: type+condition check, case _: wildcard
if-case — if (json case {'name': String name}) for JSON parsing and type validation in one step
Pros
- ✓ Lightweight grouping and returning of multiple values without classes
- ✓ Replace if-else chains with clean switch using pattern matching
Cons
- ✗ Only available in Dart 3.0+
- ✗ Complex nested patterns may actually hurt readability