패턴
패턴 (Patterns)
값이 우리가 기대하는 모양인지 검사하고, 그 값을 구성 요소로 쪼개서 변수에 담고 싶을 때가 있어요. 패턴은 Dart 언어의 한 문법 범주로, 문과 표현식처럼 존재해요. 패턴은 실제 값에 대응할 수 있는 일련의 값들의 모양(shape)을 나타내죠.
출처: Dart 공식 문서
패턴은 언어 버전이 최소 3.0이어야 해요.
본문
이 페이지는 다음을 설명해요.
- 패턴이 무엇을 하는지
- Dart 코드에서 패턴이 어디에 허용되는지
- 패턴의 흔한 사용 사례가 무엇인지
패턴의 여러 종류에 대해 배우려면 패턴 타입(pattern types) 페이지를 방문하세요.
패턴이 하는 일 (What patterns do)
일반적으로 패턴은 값을 매칭(match) 하거나, 값을 구조 분해(destructure) 하거나, 둘 다 할 수 있어요. 문맥과 패턴의 모양에 따라 달라지죠.
먼저 패턴 매칭 은 주어진 값이 다음인지 검사하게 해줘요.
- 어떤 모양을 가지는지
- 어떤 상수인지
- 다른 것과 같은지
- 어떤 타입인지
그 다음 패턴 구조 분해 는 그 값을 구성 요소로 쪼개는 편리한 선언적 문법을 제공해요. 같은 패턴으로 그 일부 또는 전부에 변수를 바인딩할 수도 있죠.
매칭 (Matching)
패턴은 항상 값에 대해 테스트해서 값이 기대하는 형태인지 결정해요. 다시 말해 값이 패턴과 일치(matches) 하는지 검사하는 거죠.
무엇이 일치인지는 어떤 패턴을 쓰느냐에 달려 있어요. 예를 들어 상수 패턴은 값이 패턴의 상수와 같으면 일치해요.
switch (number) {
// Constant pattern matches if 1 == number.
case 1:
print('one');
}
많은 패턴은 하위 패턴(subpattern)을 사용해요. 때로는 각각 바깥(outer), 안(inner) 패턴이라고 부르죠. 패턴은 하위 패턴에 대해 재귀적으로 일치해요. 예를 들어 어떤 컬렉션 타입 패턴의 개별 필드가 변수 패턴이거나 상수 패턴일 수 있어요.
const a = 'a';
const b = 'b';
switch (obj) {
// List pattern [a, b] matches obj first if obj is a list with two fields,
// then if its fields match the constant subpatterns 'a' and 'b'.
case [a, b]:
print('$a, $b');
}
일치된 값의 일부를 무시하려면 와일드카드 패턴을 자리 표시자로 쓸 수 있어요. 리스트 패턴의 경우 rest 요소를 쓸 수 있죠.
구조 분해 (Destructuring)
객체와 패턴이 일치하면 패턴은 객체의 데이터에 접근해서 부분으로 추출할 수 있어요. 다시 말해 패턴이 객체를 구조 분해 하는 거죠.
var numList = [1, 2, 3];
// List pattern [a, b, c] destructures the three elements from numList...
var [a, b, c] = numList;
// ...and assigns them to new variables.
print(a + b + c);
구조 분해 패턴 안에는 어떤 종류의 패턴이든 중첩할 수 있어요. 예를 들어 이 case 패턴은 첫 번째 요소가 'a'나 'b'인 두 요소 리스트를 일치시키고 구조 분해해요.
switch (list) {
case ['a' || 'b', var c]:
print(c);
}
패턴이 나타날 수 있는 곳 (Places patterns can appear)
Dart 언어에서 패턴을 여러 곳에서 쓸 수 있어요.
- 지역 변수 선언과 할당
for와for-in루프if-case와switch-case- 컬렉션 리터럴의 흐름 제어
이 절은 패턴으로 매칭하고 구조 분해하는 흔한 사용 사례를 설명해요.
변수 선언 (Variable declaration)
Dart가 지역 변수 선언을 허용하는 어디서든 패턴 변수 선언 을 사용할 수 있어요. 패턴은 선언 오른쪽의 값에 대해 일치해요. 일치하면 값을 구조 분해하고 새 지역 변수에 바인딩하죠.
// Declares new variables a, b, and c.
var (a, [b, c]) = ('str', [1, 2]);
패턴 변수 선언은 var나 final로 시작하고, 그 뒤에 패턴이 와야 해요.
변수 할당 (Variable assignment)
변수 할당 패턴 은 할당의 왼쪽에 와요. 먼저 일치된 객체를 구조 분해해요. 그런 다음 새 변수를 바인딩하는 대신 값을 기존 변수에 할당하죠.
임시 변수를 세 번째로 선언하지 않고 두 변수의 값을 바꾸려면 변수 할당 패턴을 쓰세요.
var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap.
print('$a $b'); // Prints "right left".
switch 문과 식 (Switch statements and expressions)
모든 case 절은 패턴을 포함해요. switch 문과 식, 그리고 if-case 문 모두에 해당돼요. case에는 어떤 종류의 패턴이든 쓸 수 있어요.
case 패턴 은 반증 가능한(refutable) 패턴*이에요. 흐름 제어가 다음 중 하나를 하게 해주죠.
- switch 대상 객체를 일치시키고 구조 분해한다.
- 객체가 일치하지 않으면 실행을 계속한다.
*> 반증 가능한 패턴은 값에 대해 테스트할 수 있는 패턴이에요.
case에서 패턴이 구조 분해하는 값은 지역 변수가 돼요. 그 유효 범위는 그 case 본문 안에서만 있죠.
switch (obj) {
// Matches if 1 == obj.
case 1:
print('one');
// Matches if the value of obj is between the
// constant values of 'first' and 'last'.
case >= first && <= last:
print('in range');
// Matches if obj is a record with two fields,
// then assigns the fields to 'a' and 'b'.
case (var a, var b):
print('a = $a, b = $b');
default:
}
논리 OR 패턴은 switch 식이나 문에서 여러 case가 본문을 공유하게 하는 데 유용해요.
var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false,
};
switch 문은 논리 OR 패턴 없이도 여러 case가 본문을 공유할 수 있지만, 논리 OR 패턴은 여러 case가 가드를 공유하게 하는 데 여전히 독특하게 유용해요.
switch (shape) {
case Square(size: var s) || Circle(size: var s) when s > 0:
print('Non-empty symmetric shape');
}
가드 절은 case의 일부로 임의의 조건을 평가해요. 조건이 false여도 switch를 빠져나가지 않죠(case 본문에 if 문을 쓰면 그렇게 될 테지만요).
switch (pair) {
case (int a, int b):
if (a > b) print('First element greater');
// If false, prints nothing and exits the switch.
case (int a, int b) when a > b:
// If false, prints nothing but proceeds to next case.
print('First element greater');
case (int a, int b):
print('First element not greater');
}
가드(when a > b)가 false인 case는 다음 case로 진행하는 반면, case 본문 안의 if (a > b)가 false인 case는 switch를 통째로 빠져나가는 차이를 볼 수 있어요.
for와 for-in 루프 (For and for-in loops)
for와 for-in 루프에서 패턴을 써서 컬렉션의 값을 반복하고 구조 분해할 수 있어요.
이 예시는 for-in 루프에서 객체 구조 분해를 사용해 <Map>.entries 호출이 돌려주는 MapEntry 객체를 구조 분해해요.
Map<String, int> hist = {'a': 23, 'b': 100};
for (var MapEntry(key: key, value: count) in hist.entries) {
print('$key occurred $count times');
}
객체 패턴은 hist.entries가 이름 있는 타입 MapEntry인지 검사하고, 이름 있는 필드 하위 패턴 key와 value로 재귀해요. 각 반복에서 MapEntry의 key getter와 value getter를 호출하고, 그 결과를 각각 지역 변수 key, count에 바인딩하죠.
getter 호출 결과를 같은 이름의 변수에 바인딩하는 건 흔한 사용 사례라서, 객체 패턴은 변수 하위 패턴에서 getter 이름을 추론할 수도 있어요. key: key처럼 중복되는 변수 패턴을 :key로 단순화할 수 있게 해주죠.
for (var MapEntry(:key, value: count) in hist.entries) {
print('$key occurred $count times');
}
패턴의 사용 사례 (Use cases for patterns)
앞 절은 패턴이 다른 Dart 코드 구성에 어떻게 들어맞는지 설명했어요. 두 변수의 값을 바꾸거나 맵에서 키-값 쌍을 구조 분해하는 것 같은 흥미로운 사용 사례를 예시로 봤죠. 이 절은 더 많은 사용 사례를 설명하며, 언제 왜 패턴을 쓰고 싶은지, 어떤 종류의 문제를 푸는지, 어떤 관용구에 가장 잘 맞는지를 답해요.
여러 반환값 구조 분해하기 (Destructuring multiple returns)
레코드(record)는 단일 함수 호출에서 여러 값을 모아서 반환하게 해줘요. 패턴은 함수 호출과 함께 레코드의 필드를 지역 변수로 직접 구조 분해하는 능력을 추가해요.
각 레코드 필드에 대해 새 지역 변수를 개별적으로 선언하는 대신:
var info = userInfo(json);
var name = info.$1;
var age = info.$2;
변수 선언이나 할당 패턴과, 그것의 하위 패턴으로 레코드 패턴을 써서 함수가 반환한 레코드의 필드를 지역 변수로 구조 분해할 수 있어요.
var (name, age) = userInfo(json);
패턴으로 이름 있는 필드를 가진 레코드를 구조 분해하려면:
final (:name, :age) =
getData(); // For example, return (name: 'doug', age: 25);
클래스 인스턴스 구조 분해하기 (Destructuring class instances)
객체 패턴은 이름 있는 객체 타입에 대해 일치해서, 객체 클래스가 이미 노출하는 getter로 그 데이터를 구조 분해하게 해줘요.
클래스의 인스턴스를 구조 분해하려면 이름 있는 타입과, 그 뒤에 괄호로 감싼 구조 분해할 프로퍼티들을 써요.
final Foo myFoo = Foo(one: 'one', two: 2);
var Foo(:one, :two) = myFoo;
print('one $one, two $two');
대수 데이터 타입 (Algebraic data types)
객체 구조 분해와 switch case는 대수 데이터 타입(algebraic data type) 스타일로 코드를 쓰기에 좋아요. 이 방법은 다음 때 쓰세요.
- 관련된 타입 계열이 있고,
- 각 타입에 특정 동작이 필요한 연산이 있고,
- 그 동작을 모든 타입 정의에 흩뜨리지 않고 한 곳에 모으고 싶을 때.
연산을 모든 타입에 대한 인스턴스 메서드로 구현하는 대신, 연산의 변형들을 하위 타입에 대해 switch하는 단일 함수에 유지하세요.
sealed class Shape {}
class Square implements Shape {
final double length;
Square(this.length);
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
}
double calculateArea(Shape shape) => switch (shape) {
Square(length: var l) => l * l,
Circle(radius: var r) => math.pi * r * r,
};
들어오는 JSON 검증하기 (Validating incoming JSON)
맵과 리스트 패턴은 JSON에서 파싱한 데이터처럼 역직렬화된 데이터의 키-값 쌍을 구조 분해하는 데 잘 맞아요.
var data = {
'user': ['Lily', 13],
};
var {'user': [name, age]} = data;
JSON 데이터가 기대하는 구조를 가진다는 걸 안다면 앞 예시는 현실적이에요. 하지만 데이터는 보통 네트워크 같은 외부 소스에서 와요. 먼저 검증해서 구조를 확인해야 하죠.
패턴 없이 검증은 장황해요.
if (data is Map<String, Object?> &&
data.length == 1 &&
data.containsKey('user')) {
var user = data['user'];
if (user is List<Object> &&
user.length == 2 &&
user[0] is String &&
user[1] is int) {
var name = user[0] as String;
var age = user[1] as int;
print('User $name is $age years old.');
}
}
단일 case 패턴이 같은 검증을 할 수 있어요. 단일 case는 if-case 문으로 쓰는 게 가장 좋아요. 패턴은 JSON 검증의 더 선언적이고 훨씬 덜 장황한 방법을 제공하죠.
if (data case {'user': [String name, int age]}) {
print('User $name is $age years old.');
}
이 case 패턴은 동시에 다음을 검증해요.
json이 맵이다. 진행하려면 먼저 바깥 맵 패턴과 일치해야 하므로. 그리고 맵이므로json이 null이 아님도 확인한다.json이user키를 포함한다.user키가 두 값의 리스트와 짝을 이룬다.- 리스트 값들의 타입이
String과int다. - 값을 담을 새 지역 변수는
name과age다.