Patterns (@Dart3.0)

Patterns에 대한 다음 설명은, Dart 공식 사이트의 내용(아래의 출처 참조)을 기반으로, 독자의 이해를 돕기 위한 추가적인 글을 포함하거나, 출처의 글을 수정하는 방식으로 작성하였습니다.

Patterns은 실제의 값에 매핑될 수 있는 값들의 ‘형태’를 의미합니다. 첫째는 match로, 변수가 값을 가지고 있을때, 값에 따라서 다르게 동작하게 합니다. 둘째는 destructure로, 복합적인 값들에서 개별적인 값들을 추출할수 있습니다.

Pattern Matching

값이 주어진 형태(pattern, 이후 패턴)에 부합 하는지 확인하는 기능입니다. 대표적인 예제는 switch 구문입니다. 예제는 1이라는 값이, number가 가진 상수의 pattern과 동일한 것인지 검사하는 작업이라고 볼 수 있습니다.

switch (number) {
  // Constant pattern matches if 1 == number.
  case 1:
    print('one');
}

List와 같은 collection type을 사용하는 경우는 List 라는 상위의 패턴과 List에 속한 항목이라는 하위의 패턴으로 구성할 수 있습니다. 이 경우 상위 패턴을 outer 패턴, 하위 패턴을 inner 패턴이라고 부릅니다. 아래의 예제는 outer 패턴이 List이며, inner 패턴이 상수 두개인 경우에 대한 pattern matching 예시를 보여주고 있습니다.

const a = 'a';
const b = 'b';
List obj = [a, 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');
}

Pattern Destructuring

Pattern은 복합적인 값을 구성하는 개별적인 값들을 추출하는 경우에도 활용한다. 이를 destructuring이라고 한다. 예를 들어 다음의 예시에서는 [1,2,3]인 리스트에서 각각의 값 1,2,3을 추출하여, 다시 각각 a,b,c에 할당하는 것을 보여준다.

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);

Pattern destructuring은 보다 복잡한 형태로도 구성할 수 있다. 아래의 예시에서는 다음의 조건이 모두 맞을때 c를 출력한다.

첫째로 list는 List 타입 이여야 한다.
둘째로 list의 첫번째 항목이 갖는 값은 문자 ‘a’ 혹은 ‘b’ 이여야 한다.
세째로 list는 두번째 항목(c)을 갖고 있어야 한다.

Pattern Use Cases

Dart 언어에서의 pattern 사용 방법은 제한이 없으며, 다음의 대표적인 경우들로 설명할 수 있다.

  • Local variable declarations and assignments
  • for and for-in loops
  • if-case and switch-case
  • Control flow in collection literals

Use Case: Variable Declaration

변수 선언시 패턴을 사용할 수 있습니다. 다음의 예제와 같은 복잡한 형태의 값들에서 a,b,c를 추출해 낼 수 있습니다. 명심해야할 부분으로, 패턴을 사용하여 변수를 선언하는 경우에는 반드시 var 혹은 final의 형태로 선언해야 합니다.

// Declares new variables a, b, and c.
var (a, [b, c]) = ('str', [1, 2]);

Use Case: Variable Assignment

변수 값을 할당하는 경우에 사용할 수 있습니다. 가장 유용한 사용법은 복수의 값을 상호 교환하는 swap의 경우입니다. 다음의 예제를 보면 (b, a) = (a, b)와 같은 방식으로 두 변수의 값을 한 줄로 변경하는 것을 볼 수 있습니다.

var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap.
print('$a $b'); // Prints "right left".

Use Case: Conditional Statements (switch & if)

패턴은 조건문에서 다양한 형태로 활용할 수 있습니다. 가장 기본적인 것은 switch 구문에서 조건에 부합하는 경우에 특정 작업을 수행하는 경우입니다.

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:
}

case 1:은 obj가 정수 1의 값을 가진 경우에 수행하는 동작을 정의합니다. case >= first && <= last:는 obj가 fisrt가 가진 값 보다 크거나 같고, last가 가진 값 보다 작거나 같은 경우에 수행하는 동작을 정의합니다. case (var a, var b):는 obj가 정수 두개인 Record인 경우에 수행하는 동작을 정의합니다.

지금까지와는 다른 형태의 switch 구문을 다음의 예제에서 볼 수 있습니다.

var isPrimary = switch (color) {
  Color.red || Color.yellow || Color.blue => true,
  _ => false
};

이 문법은 color의 값에 따라서, true 혹은 false 값을 isPrimary 변수에 저장하는 경우입니다. switch 구문 안에 case 구문이 없습니다. 다만 쉼표(,)로 분리된 구문들이 있습니다. 그리고 Dart에서 간단한 함수를 한줄로 만드는 경우에 사용하는 문법인 =>가 있는 것을 볼 수 있습니다. 따라서 switch 구문 안의 두 줄은 각각 true 혹은 flase를 리턴하는 기능을 한다고 보면 됩니다. 그렇다면 => 앞의 내용이 관건인데, 논리 연산자 ||는 OR를 의미한다는 내용을 토대로, 첫번째 줄의 의미는 color가 갖는 값이 Color.red 이거나 혹은 Color.yellow 이거나 혹은 Color.blue 인 경우에 true를 isPrimary에 저장하도록 한다는 것을 유추할 수 있습니다. 만약 세가지 값에 color가 가진 값이 부합하지 않으면 두번째 줄이 실행되는데, 기호 ‘_’는 와일드 카드라고 부르며, ‘모든 조건’으로 해석합니다. 따라서, 세가지 조건에 부합하지 않는 모든 경우에 대해서는 false를 isPrimary에 저장하게 됩니다.

이렇게 패턴은 color의 값이 논리 연산자의 조건 혹은 와일드 카드에 부합하는지 안하는지에 따른 차별적인 동작 정의에 사용할 수 있습니다. 해당 내용을 일반적인 switch 구문에 사용한 예제는 다음과 같습니다.

switch (shape) {
  case Square(size: var s) || Circle(size: var s) when s > 0:
    print('Non-empty symmetric shape');
}

예제에서 보듯이 switch 구문안의 case에 논리 연산자인 ||를 적용하였습니다. 추가로 한가지 더 포함한 것은 case 구문의 마지막에 있는 when s > 0 입니다. 이는 || 논리 연산을 수행하기 전에 s 값이 0보다 큰지 확인해서, 0보다 큰 경우에만 논리 연산 ||를 수행하라는 의미입니다.

추가로 알아볼 if-case 구문은 형태가 특이할 수 있습니다. 다음의 경우를 보면, if 조건문에서 json의 값을 확인하는데, case [‘user’, var name]가 json에 이어서 나타난 것을 볼 수 있습니다. 이를 if-case 구문이라고 하는데, if-A-case-B의 형태로 사용합니다. 의미는 A가 B의 패턴이라면 true, 그렇지 않다면 false의 의미로 동작합니다.

if (json case ['user', var name]) {
  print('Got user message for user $name.');
}

Use Case: Loop Statement (for, for-in)

패턴이라고 보기 어려울 수 있지만, 반복문에 대해서도 패턴의 개념을 적용할 수 있습니다. 다음의 예제에서는 for-in 루프에서 패턴을 사용하여 hist.entries가 반환하는 항목을 MapEntry 객체 형태로 destructureing 합니다.

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가 있는지 확인한 다음, 명명된 필드 하위 패턴의 키와 값으로 반복됩니다. 각 반복에서 MapEntry의 key getter 및 value getter를 호출하고 결과를 각각 로컬 변수 key 및 count에 바인딩합니다.

getter 호출 결과를 동일한 이름의 변수에 바인딩하는 것이 일반적인 사용 사례이므로, 이 경우의 패턴은 변수 하위 패턴에서 getter 이름을 추론할 수도 있습니다. 이를 통해 key: key와 같은 중복된 것에서 :key:로 변수 패턴을 다음의 예제와 같이 단순화할 수 있습니다. 만약 두번째 항목을 (위의 코드에서) value:value로 명명했다면, 다음의 코드에서 :value로 단순화 하는 것도 같은 맥락에서 가능합니다.

for (var MapEntry(:key, value: count) in hist.entries) {
  print('$key occurred $count times');
}

Pattern Types (MORE TO READ)

Dart 언어 공식 홈페이지에서는 다양한 종류의 패턴에 대한 설명을 상세하게 제공하고 있습니다.

이번 글에서 패턴에 대한 개괄적인 내용을 다뤘다면, 이를 활용하는 경우에는 case-by-case로 살펴봐야할 사항들이 발생할 수 있습니다. 이 경우, 공식 사이트에서 사례별 활용 방법을 살펴보면 시행착오에 따른 시간을 줄 일수 있을 겁니다.

[출처] https://dart.dev/language/patterns

[출처] https://dart.dev/language/pattern-types

[출처] https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md#300—2023-05-10

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다