패턴 타입
패턴 타입 (Pattern types)
이 페이지는 Dart의 여러 종류의 패턴을 정리한 레퍼런스예요. 패턴이 어떻게 동작하는지, 어디에 쓸 수 있는지, 흔한 사용 사례에 대한 개요는 메인 'Patterns' 페이지를 참고해 주세요.
본문
패턴 우선순위 (Pattern precedence)
연산자 우선순위와 비슷하게, 패턴 평가도 우선순위 규칙을 따라요. 더 낮은 우선순위의 패턴을 먼저 평가하려면 괄호로 감싼 패턴(parenthesized pattern)을 쓰면 되죠.
이 문서는 패턴 타입을 **우선순위가 낮은 순서(오름차순)**로 나열해요.
- 논리합(logical-or) 패턴은 논리곱(logical-and)보다 우선순위가 낮고, 논리곱은 관계(relational) 패턴보다 우선순위가 낮아요. 이렇게 아래로 갈수록 우선순위가 높아져요.
- 후치(post-fix) 단항 패턴(cast, null-check, null-assert)은 같은 우선순위를 공유해요.
- 나머지 기본 패턴들은 가장 높은 우선순위를 공유해요. 컬렉션 타입(record, list, map)과 Object 패턴은 다른 데이터를 포함하므로 바깥 패턴(outer-pattern)으로 먼저 평가돼요.
논리합 (Logical-or)
subpattern1 || subpattern2
논리합 패턴은 subpattern들을 ||로 구분하고, 어느 브랜치 하나라도 매치되면 매치돼요. 브랜치는 왼쪽부터 오른쪽으로 평가되고, 한 브랜치가 매치되면 나머지는 평가되지 않아요.
var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false,
};
논리합 패턴의 subpattern은 변수를 바인딩할 수 있지만, 브랜치들이 같은 변수 집합을 정의해야 해요. 패턴이 매치될 때 브랜치 하나만 평가되기 때문이죠.
논리곱 (Logical-and)
subpattern1 && subpattern2
&&로 구분된 패턴 쌍은 두 subpattern이 모두 매치될 때만 매치돼요. 왼쪽 브랜치가 매치되지 않으면 오른쪽 브랜치는 평가되지 않아요.
논리곱 패턴의 subpattern은 변수를 바인딩할 수 있지만, 각 subpattern의 변수는 서로 겹치면 안 돼요. 패턴이 매치되면 두 변수가 모두 바인딩되기 때문이죠.
switch ((1, 2)) {
// Error, both subpatterns attempt to bind 'b'.
case (var a, var b) && (var b, var c): // ...
}
관계 (Relational)
== expression
< expression
관계 패턴은 매치된 값을 주어진 상수와 같음/관계 연산자(==, !=, <, >, <=, >=)로 비교해요.
매치된 값에 해당 연산자를 호출했을 때(그리고 상수를 인자로 넘겼을 때) true가 반환되면 패턴이 매치돼요.
관계 패턴은 숫자 범위 매칭에 유용해요. 특히 논리곱 패턴과 함께 쓸 때 빛을 발하죠.
String asciiCharType(int char) {
const space = 32;
const zero = 48;
const nine = 57;
return switch (char) {
< space => 'control',
== space => 'space',
> space && < zero => 'punctuation',
>= zero && <= nine => 'digit',
_ => '',
};
}
Cast
foo as String
cast 패턴은 구조 분해(destructuring) 중간에 타입 캐스트를 끼워 넣어, 값을 다른 subpattern에 넘기기 전에 타입을 변환할 수 있게 해줘요.
(num, Object) record = (1, 's');
var (i as int, s as String) = record;
cast 패턴은 값이 명시된 타입이 아니면 예외를 던져요. null-assert 패턴처럼, 구조 분해된 값의 예상 타입을 강제로 주장(assert)할 수 있는 거죠.
Null-check
subpattern?
null-check 패턴은 값이 null이 아닐 때 먼저 매치하고, 그런 다음 같은 값에 대해 안쪽 패턴을 매치해요. 이 패턴은 매치되는 nullable 값의 non-nullable 기본 타입을 가진 변수를 바인딩할 수 있게 해줘요.
null 값을 예외를 던지지 않고 매치 실패로 취급하고 싶다면 null-check 패턴을 써요.
String? maybeString = 'nullable with base type String';
switch (maybeString) {
case var s?:
// 's' has type non-nullable String here.
}
값이 null일 때를 매치하려면 **상수 패턴 null**을 쓰면 돼요.
Null-assert
subpattern!
null-assert 패턴은 객체가 null이 아닐 때 먼저 매치한 다음 값에 대해 매치해요. non-null 값은 통과시키지만, 매치된 값이 null이면 예외를 던져요.
null 값이 조용히 매치 실패로 처리되지 않도록 하려면, 매치할 때 null-assert 패턴을 쓰면 돼요.
List<String?> row = ['user', null];
switch (row) {
case ['user', var name!]: // ...
// 'name' is a non-nullable string here.
}
변수 선언 패턴에서 null 값을 없애려고 할 때도 null-assert 패턴을 쓸 수 있어요.
(int?, int?) position = (2, 3);
var (x!, y!) = position;
값이 null일 때를 매치하려면 상수 패턴 null을 쓰면 돼요.
상수 (Constant)
123, null, 'string', math.pi, SomeClass.constant, const Thing(1, 2), const (1 + 2)
상수 패턴은 값이 상수와 같을 때 매치돼요.
switch (number) {
// Matches if 1 == number.
case 1: // ...
}
단순 리터럴과 이름 있는 상수에 대한 참조는 상수 패턴으로 바로 사용할 수 있어요.
- 숫자 리터럴 (
123,45.56) - 불리언 리터럴 (
true) - 문자열 리터럴 (
'string') - 이름 있는 상수 (
someConstant,math.pi,double.infinity) - 상수 생성자 (
const Point(0, 0)) - 상수 컬렉션 리터럴 (
const [],const {1, 2})
더 복잡한 상수 표현식은 괄호로 감싸고 const를 접두어로 붙여야 해요 (const (1 + 2)).
// List or map pattern:
case [a, b]: // ...
// List or map literal:
case const [a, b]: // ...
변수 (Variable)
var bar, String str, final int _
변수 패턴은 매치되거나 구조 분해된 값에 새 변수를 바인딩해요. 보통 구조 분해 패턴의 일부로, 구조 분해된 값을 포착(capture)하기 위해 쓰여요.
변수들은 패턴이 매치된 경우에만 도달할 수 있는 코드 영역에서 스코프를 가져요.
switch ((1, 2)) {
// 'var a' and 'var b' are variable patterns that bind to 1 and 2, respectively.
case (var a, var b): // ...
// 'a' and 'b' are in scope in the case body.
}
타입이 붙은 변수 패턴은 매치된 값이 선언된 타입일 때만 매치되고, 그렇지 않으면 실패해요.
switch ((1, 2)) {
// Does not match.
case (int a, String b): // ...
}
변수 패턴으로 와일드카드 패턴(_)을 쓸 수도 있어요.
식별자 (Identifier)
foo, _
식별자 패턴은 등장하는 문맥에 따라 상수 패턴처럼 또는 변수 패턴처럼 동작해요.
const c = 1;
switch (2) {
case c:
print('match $c');
default:
print('no match'); // Prints "no match".
}
- 선언 문맥 (Declaration context): 식별자 이름으로 새 변수를 선언해요.
var (a, b) = (1, 2); - 할당 문맥 (Assignment context): 식별자 이름의 기존 변수에 할당해요.
(a, b) = (3, 4); - 매치 문맥 (Matching context): 이름 있는 상수 패턴으로 취급돼요 (이름이
_가 아닌 경우). - 어떤 문맥에서든 와일드카드 식별자: 어떤 값이든 매치하고 그 값을 버려요.
case [_, var y, _]: print('The middle element is $y');
괄호 (Parenthesized)
(subpattern)
괄호로 감싼 표현식처럼, 패턴에서 괄호는 패턴 우선순위를 제어하고, 더 높은 우선순위가 기대되는 자리에 더 낮은 우선순위의 패턴을 넣을 수 있게 해줘요.
예를 들어 불리언 상수 x, y, z가 각각 true, true, false라고 해볼게요. 다음 예시는 불리언 표현식 평가처럼 보이지만, 실제로는 패턴을 매치하는 예시예요.
// ...
x || y => 'matches true',
x || y && z => 'matches true',
x || (y && z) => 'matches true',
// `x || y && z` is the same thing as `x || (y && z)`.
(x || y) && z => 'matches nothing',
// ...
Dart는 패턴을 왼쪽에서 오른쪽으로 매치하기 시작해요. 각 경우를 따라가 볼게요.
- 첫 번째 패턴은
x가true라서true와 매치돼요. - 두 번째 패턴도
x가true라서true와 매치돼요. - 세 번째 패턴도
x가true라서true와 매치돼요. (x || y && z는x || (y && z)와 같은 뜻이죠.) - 마지막 패턴
(x || y) && z는true와 매치되지 않아요. 왜냐하면(x || y)는true에 매치되지만,z가true가 아니기 때문이에요. 그리고(x || y)가false에도 매치되지 않으므로,false와의 매치에서도 Dart는z를 시도조차 하지 않아요. 결론적으로(x || y) && z는 어떤 값에도 매치가 없어요.
다시 정리하면, x가 true에 매치되므로 Dart는 y를 시도조차 하지 않아요. (x || y)는 true에 매치되지만 z는 true와 매치되지 않죠. 그래서 패턴 (x || y) && z는 true와 매치되지 않아요. 그리고 (x || y)는 false에 매치되지 않으니 Dart는 z를 시도하지 않고, 결국 (x || y) && z는 매치가 없어요.
목록 (List)
[subpattern1, subpattern2]
목록 패턴은 List를 구현하는 값을 매치하고, 그런 다음 리스트의 요소들에 대해 subpattern을 재귀적으로 매치해 위치별로 구조 분해해요.
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 요소를 자리 표시자로 사용해서, 목록의 임의 개수 요소를 처리할 수 있어요.
Rest 요소 (Rest element)
목록 패턴은 rest 요소(...) 하나를 포함할 수 있어, 임의 길이의 목록을 매치할 수 있게 해줘요.
var [a, b, ..., c, d] = [1, 2, 3, 4, 5, 6, 7];
// Prints "1 2 6 7".
print('$a $b $c $d');
rest 요소에는 목록의 다른 subpattern에 매치되지 않는 요소들을 새 목록으로 모으는 subpattern도 붙일 수 있어요.
var [a, b, ...rest, c, d] = [1, 2, 3, 4, 5, 6, 7];
// Prints "1 2 [3, 4, 5] 6 7".
print('$a $b $rest $c $d');
맵 (Map)
{"key": subpattern1, someConst: subpattern2}
맵 패턴은 Map을 구현하는 값을 매치하고, 그런 다음 맵의 키들에 대해 subpattern을 재귀적으로 매치해 구조 분해해요.
맵 패턴은 패턴이 맵 전체와 일치할 것을 요구하지 않아요. 맵 패턴은 패턴으로 매치되지 않는 키는 무시해요. 다만 맵에 존재하지 않는 키를 매치하려 하면 **StateError**가 던져져요.
final {'foo': int? foo} = {};
레코드 (Record)
(subpattern1, subpattern2)
(x: subpattern1, y: subpattern2)
레코드 패턴은 레코드 객체를 매치하고 그 필드를 구조 분해해요. 값이 패턴과 같은 모양의 레코드가 아니면 매치가 실패해요. 그렇지 않으면 필드 subpattern들이 레코드의 해당 필드와 매치돼요.
레코드 패턴은 패턴이 레코드 전체와 일치할 것을 요구해요. 이름 있는 필드를 가진 레코드를 패턴으로 구조 분해하려면 패턴에 필드 이름을 포함해야 해요.
var (myString: foo, myNumber: bar) = (myString: 'string', myNumber: 1);
getter 이름은 생략할 수 있고, 필드 subpattern의 변수 패턴이나 식별자 패턴에서 추론돼요. 다음 패턴 쌍들은 서로 동등해요.
// Record pattern with variable subpatterns:
var (untyped: untyped, typed: int typed) = record;
var (:untyped, :int typed) = record;
switch (record) {
case (untyped: var untyped, typed: int typed): // ...
case (:var untyped, :int typed): // ...
}
// Record pattern with null-check and null-assert subpatterns:
switch (record) {
case (checked: var checked?, asserted: var asserted!): // ...
case (:var checked?, :var asserted!): // ...
}
// Record pattern with cast subpattern:
var (untyped: untyped as int, typed: typed as String) = record;
var (:untyped as int, :typed as String) = record;
객체 (Object)
SomeClass(x: subpattern1, y: subpattern2)
객체 패턴은 매치된 값을 주어진 이름 있는 타입과 대조해 확인하고, 객체의 프로퍼티에 있는 getter를 사용해 데이터를 구조 분해해요. 값이 같은 타입이 아니면 **반박(refuted)**돼요. 반박 가능한 패턴이란 값을 기준으로 검사될 수 있는 패턴을 뜻해요.
switch (shape) {
// Matches if shape is of type Rect, and then against the properties of Rect.
case Rect(width: var w, height: var h): // ...
}
getter 이름은 생략할 수 있고, 필드 subpattern의 변수 패턴이나 식별자 패턴에서 추론돼요.
// Binds new variables x and y to the values of Point's x and y properties.
var Point(:x, :y) = Point(1, 2);
객체 패턴은 패턴이 객체 전체와 일치할 것을 요구하지 않아요. 객체에 패턴이 구조 분해하지 않는 추가 필드가 있어도 여전히 매치될 수 있어요.
와일드카드 (Wildcard)
_
이름이 _인 패턴은 와일드카드예요. 변수 패턴이거나 식별자 패턴인데, 어떤 변수에도 바인딩하거나 할당하지 않아요.
이후의 위치 값을 구조 분해하기 위해 subpattern이 필요한 자리의 자리 표시자로 유용해요.
var list = [1, 2, 3];
var [_, two, _] = list;
타입 표기가 붙은 와일드카드 이름은, 값을 이름에 바인딩하지 않고 값의 타입을 검사하고 싶을 때 유용해요.
switch (record) {
case (int _, String _):
print('First field is int and second is String.');
}
더 알아보기 (Learn more)
- Dart 공식 문서 - Pattern types 원문 살펴보기
- 패턴의 전반적인 개념과 쓰임새는 메인 'Patterns' 페이지에서, 패턴을 쓰는 자세한 문법은 'Patterns' 문서를 함께 보면 좋아요.