패턴은 문장 및 표현식과 같은 Dart 언어의 구문 범주입니다. 패턴은 실제 값과 일치할 수 있는 값 집합의 모양을 나타냅니다.
일반적으로 패턴은 문맥과 패턴의 형태에 따라서 값을 일치(매칭)시키거나 구조분해 한다.
매칭(패턴을 통한 일치 여부 확인)
구조 분해
패턴은 값이 내가 원하는 형식과 일치하는지 항시 확인한다. 즉, 값이 패턴과 일치하는지 확인하는 것이다.
매칭(일치 여부 확인)의 구성요소는 내가 사용하는 패턴의 종류에 따라 나뉜다.
예를 들어서 아래 코드에서 보이듯 상수 패턴은 주어진 값이 패턴의 상수와 같으면 매칭 한다.
switch (number) {
// 1 == 숫자인 경우 상수 패턴 일치.
case 1:
print('one');
}
많은 패턴은 ‘서브 패턴’을 사용한다. 각각 외부 패턴 혹은 내부 패턴이라고 부르기도 하며 패턴은 서브 패턴에서 재귀적으로 일치한다.
예를 들어 컬렉션 유형 패턴의 개별 필드는 가변 패턴 또는 상수 패턴일 수 있습니다:
const a = 'a';
const b = 'b';
switch (obj) {
// List 패턴 [a, b]는 객체가 두 개의 필드가 있는 목록인 경우 객체를 먼저 일치시킵니다,
// 그 다음 필드가 상수(constant) 서브 패턴 'a'와 'b'와 일치하면 됩니다.
case [a, b]:
print('$a, $b');
}
일치하는 값의 일부를 무시하려면 와일드카드 패턴을 사용한다.
와일드 카드 패턴은 무시하고 싶은 값에 언더스코어 ( _ ) 를 사용한다.
그러면 List 패턴의 와일드카드를 제외한 나머지 요소를 사용할 수 있습니다.
패턴에 배치하는 언더스코어( _ )를 ‘와일드카드’라고 부른다.
이는 변수 혹은 식별자 패턴으로 특정 변수에 바인딩 하거나 할당하지 않는다.
그리고 구조 분해를 위해 하위 패턴이 필요한 위치에 배치할 수 있다.
var list = [1, 2, 3];
var [_, two, _] = list;
와일드카드에도 타입 어노테이션을 할 수 있는데, 그러면 값의 타입만 비교한다.
switch (record) {
case (int _, String _):
print('First field is int and second is String.');
}
(자바스크립트를 했다면 너무 익숙하다)
객체의 모양과 패턴이 일치하면 패턴은 객체 안에 있는 데이터에 접근, 값을 추출해낼수 있다.
var numList = [1, 2, 3];
// List 패턴 [a, b, c]는 numList...에서 세 요소를 구조 분해 한다.
var [a, b, c] = numList;
// ...그리고 새로운 변수에 할당했고 이를 사용할 수 있다.
print(a + b + c); // 6
구조 분해 패턴 안에 모든 종류의 패턴을 중첩할 수 있습니다.
예를 들어, 아래 패턴은 2개의 요소를 가진 list이며 첫 번째 요소가 a나 b인 경우 구조분해 해서
두 번째 요소를 print하고 있다.
switch (list) {
case ['a' || 'b', var c]:
print(c);
}
대표적으로 아래의 경우에서 패턴을 사용할 수 있다.
로컬 변수의 선언에 패턴 변수를 사용할 수 있다.
왼 쪽의 패턴과 오른 쪽의 값이 일치하면 구조분해 하여 로컬 변수에 바인딩 한다.
// a, b, c라는 새로운 변수가 선언되었다.
var (a, [b, c]) = ('str', [1, 2]);
패턴 변수 선언시 var 또는 final로 시작하고 그 뒤에 패턴을 적는다.
변수 할당은 임시 변수를 선언하지 않고도 구조 분해된 두 변수의 값을 바꾸게 해준다.
var (a, b) = ('left', 'right');
(b, a) = (a, b); // 임시변수 없이 두 변수의 값을 서로에게 할당
print('$a $b'); // "right left".
if문과 switch문에도 모두 패턴을 적용할 수 있다.
case 패턴이 일치한 경우만 구조 분해하여 할당하며, 그 이외의 경우에는 넘어간다.
패턴이 case안에서 구조 분해한 값은 로컬 변수가 되며 그 스코프는 해당 case문 안으로 한정한다.
switch (obj) {
// obj == 1 이면 일치한다.
case 1:
print('one');
// 객체의 값이 'first'와 'last'의 상수 값 사이에 있는 경우 일치한다.
case >= first && <= last:
print('in range');
// obj가 2개의 필드를 가진 record타입이면 일치, 그리고 2개의 필드를 a와 b에 할당한다.
case (var a, var b):
print('a = $a, b = $b');
default:
}
( || )를 쓰는 Logical-or 패턴도 여러개의 case들이 본문 공유할때 유용하다.
var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false
};
// 이렇게 3개의 case를 한 줄로 쓸 수 있다.
// 패턴을 채용하지 않으면 아래와 같다.
var isPrimary = switch (color) {
case Color.red :
return true;
case Color.yellow :
return true;
case Color.blue :
return true;
default:
return false;
};
switch문은 ||를 쓰지 않고도 본문을 공유하는 많은 case를 가질 수 있지만
다수의 case들이 guard
기능을 공유할때는 여전히 매우 유용하다.
switch (shape) {
case Square(size: var s) || Circle(size: var s) when s > 0:
print('Non-empty symmetric shape');
}
(영어로 guard가 무엇인가 모르겠지만, 예시 코드를 보면 when s > 0:
을 통해 size가 0혹은 0보다 작아지는 것을 막아주고 있다)
패턴을 통해 collection의 값을 반복하거나 구조 분해 할 수 있다.
아래 예시 코드에서는 <Map>.entries
를 통해 Map의 ****key와 value를 구조분해 해서 반복문을 돌린다.
(MapEntry와 <Map>.entries
는 [키, 값]을 한 쌍으로 하는 배열을 준다.)
Map<String, int> hist = {
'a': 23,
'b': 100,
};
for (var MapEntry(key: key, value: count) in hist.entries) {
print('$key occurred $count times');
// a occurred 23 times
// b occurred 100 times
}
위 코드에서 객체 패턴은 hist.entries에 MapEntry가 있는지 확인한 다음
명명된 필드 하위 패턴인 key와 value로 재귀한다.
각 반복에서 MapEntry의 key 및 value를 호출하고 결과를 각각 로컬 변수 key 및 count에 바인딩한다.
아래 경우에 대한 설명
Records 타입을 통해 한 번의 함수 호출로 여러개의 값을 집계하고 반환할 수 있다.
패턴은 함수 호출에 record안에 있는 필드를 로컬 변수로 구조 분해, 할당하는 기능을 추가해준다.
별도의 지역함수를 선언할 필요 없이 name과 age를 손쉽게 꺼내 쓸 수 있다.
var info = userInfo(json); // json을 해석, Records 타입의 ( var, var )등을 return
var name = info.$1;
var age = info.$2;
(여기서 한 걸음 더 나아가서 리액트 훅을 사용하는 것 처럼) 변수 선언 또는 할당 패턴과 그 하위 패턴으로 Records 패턴을 사용하여 함수가 반환하는 record의필드를 로컬 변수로 분해할 수 있습니다.
var (name, age) = userInfo(json);
객체 패턴(SomeClass(x: subpattern1, y: subpattern2))은 명명된 객체 유형과 매치(일치)하므로
객체의 클래스가 이미 노출한 getter를 사용하여 해당 데이터를 구조 분해 할 수 있다.
클래스를 구조 분해 하기 위해서는 각 프로퍼티의 타입을 맞춰 사용한 다음 괄호 안에 구조 분해할 프로퍼티를 입력한다.
final Foo myFoo = Foo(one: 'one', two: 2);
var Foo(:one, :two) = myFoo; // 변수 one과 two에 Foo클래스인 myFoo의 프로퍼티를 할당
print('one $one, two $two'); // one 'one', two 2
(myFoo.one, myFoo.two를 다음과 같이 줄이는 것)
(Algebraic data type : 곱타입, 합타입 등 다른 타입 값을 가지는 타입)
객체의 구조 분해와 switch-case구문은 대수적 데이터 타입 스타일로 코드를 작성하는데 도움이 된다.
아래와 같은 경우 사용한다.
모든 타입에 대해 하나씩 연산을 구현하는 대신, 단일 함수에서 하위 타입별로 알아서 연산하게 한다.
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
};
예시 코드의 Square도 Circle도 모두 Shape의 하위 타입이기 때문에 calculateArea는 이 두 타입 모두를 파라미터로 받을 수 있으며, switch문을 통해 타입을 알아서 구분하고 그에 맞게 연산한다.
코드를 보면 길이와 반지름을 알아서 구조 분해로 꺼내와서 각각에게 맞게 곱하고 있다.
Map과 List타입도 JSON의 데이터가 가진 key-value구조를 아주 잘 해석한다.
var json = {
'user': ['Lily', 13]
};
var {'user': [name, age]} = json;
위의 예시는 JSON 데이터의 구조를 우리가 이미 알고있다면 효과적이다.
하지만 데이터는 일반적으로 네트워크와 같은 외부 소스에서 가져온다.
따라서 먼저 데이터의 유효성을 검사하여 구조를 확인해야 한다.
만일 패턴이 없으면 유효성 검사는 아래 코드처럼 매우 길어진다.
if (json is Map<String, Object?> &&
json.length == 1 &&
json.containsKey('user')) {
var user = json['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패턴(if문을 통한)만 가지고도 위와 동일한 유효성 검사를 실행 할 수 있다.
패턴은 더 간략하고 선언적인(declarative) JSON 유효성 검사법을 제공한다.
if (json case {'user': [String name, int age]}) {
print('User $name is $age years old.');
}
이 case패턴은 아래 5가지 경우를 동시에 검증한다.
json
is a map, because it must first match the outer map pattern to proceedjson
is not null)json
contains a key user
)user
pairs with a list of two values)String
and int)
String
and int)