능히 해낼 수 있다

230911 Dart: Records 본문

개발🌐/Flutter | Dart

230911 Dart: Records

roni_eo 2023. 9. 11. 10:23
반응형
더보기

✍️✍️✍️ 위 글은 작성자의 지식습득에 따라 추후 퇴고 될 수 있음을 알려드립니다(피드백 환영).


플러터는 프레임워크(크로스플랫폼)이고 이를 사용하기 위해선 다트라는 프로그래밍 언어로 앱 내부 로직과 UI를 작성해야한다.
다트는 JS의 Spread Operator(스프레드 연산자), Optional Chaining(옵셔널체이닝)등의 편리한 문법을 모두 가져옴과 동시에 정적타입 시스템으로 이뤄진 언어이다. 또한 JAVA와 유사하게 객체 지향 코드를 작성할 수 있으면서 간결하게 만들 수 있다는 특징을 가지고있다. 비동기 또한 Future 객체를 사용해 진행이 가능하다.
또한 단일 코드베이스로 Android, iOS 네이티브 그리고 웹을 위한 JS코드로 내보내는 것이 가능하다.

2023년 5월에 GoogleI/O에 추가된 타입 자료형인 Record라는 타입(자료형)이 있는데, 나의 dart지식은 약 10개월 전 상태에 머물러 있기 때문에 이것에 대해 dart 문서를 참고해 기록하고자 한다.


 0. Reacords?

레코드는 익명불변집계 유형으로 Python이나 Swift에서는 Tuple이라고 불리는 여러 개체를 하나의 개체로 묶는 개념이다. 다른 컬렉션 유형과 달리 레코드는 고정 크기, 이질적이며 유형이 지정된다.

익명(anoymous)
레코드에 속한 벨류는 키를 꼭 가질 필요가 없음. 값을 순서대로 호출하는 것(record.$1, record.$2)이 가능하지만,
0이 아닌 1부터 시작하는것에 유의해야 한다. 가령 키 값을 부여하게 된다면 $1나 $2등으로 호출이 불가하다
불변(immutable)
레코드를 한 번 생성 후, 내부에서 값을 추가, 교체 또는 삭제할 수 없다.
const나 final로 선언하지 않은경우, 다른 값을 대입하는 방식으로 변경이 가능하나, 그 외는 불가하다
집계(aggregate)
컬렉션으로 분류되는 List, Set 또는 Map처럼 레코드 도한 여러개의 값을 묶어서 사용이 가능하다.

고정 크기(fixed-sized)
레코드는 불변으로 고정 사이즈이고, List, Set, Map의 타입은 가변적이므로 고정 사이즈가 아니다.
이질적, 이종의(heterogeneous)
레코드에 속한 벨류는 다양한 타입을 가질 수 있음. 타 컬렉션 타입을 
dynamic으로 지정하지 않는 이상 모든 벨류가 하나의 타입에 속한다.
타입이 지정됨(typed)
레코드 자체에 대한 타입은 없으나, String이나 int같이 레코드의 구조자체가 하나의 타입이 된다.

 

레코드는 실제 값이므로 변수에 저장하고, 중첩하고, 함수와 주고받을 수 있으며, 목록, 맵, 집합과 같은 데이터 구조에 저장할 수 있다.

1. Record 문법

레코드 표현식은 괄호로 묶인 이름 또는 위치 필드의 쉼표로 구분된 목록으로, 변수에 대입하면 이렇다:

// 'record'라는 이름의 record 생성하고 초기화
// record 내부에는 문자열, 정수, 불린 값 및 다시 문자열이 포함됨
var record = ('first', a: 2, b: true, 'last');
// 'first': 문자열 'first'가 첫 번째 요소
// a: 2: 'a'와 정수 값 2로 이루어진 두 번째 요소
// b: true: 'b'와 불린 값 true로 세 번째 요소
// 'last': 문자열 'last'가 마지막 요소

레코드는 괄호로 묶인 쉼표로 구분된 유형의 레코드 타입 어노테이션을 사용하여 반환 타입과 매개변수 타입을 정의할 수 있다. 

// 함수는 (int, int) 형식을 입력으로 받고, 또 다른 (int, int) 형식의 레코드를 반환
(int, int) swap((int, int) record) {
  // 입력으로 받은 'record'에서 'a'와 'b'라는 변수로 값을 추출
  var (a, b) = record;
   
  // 변수 'a'와 'b'의 값을 교환하여 새로운 record 생성
  // 반환되는 record은 (b, a) 순서로 'a'와 'b'의 값을 가짐
  return (b, a);
}

또한 레코드는 변수선언과 초기화를 따로 수행한다. 초기화 할 때, 변수 선언 시 타입에 명시한 구조에 맞는 값을 사용해야한다.

// 변수 선언에서의 레코드 타입 주석:
(String, int) record;

// 레코드 표현식으로 초기화:
record = ('A string', 123);

위와 같이 꺼내서 사요이 가능하고, 키가 제공된 값인 경우, $1, $2등의 번호로 호출이 불가하고, 해당 값이 있는 위치는 건너뛰고 번호가 매겨지게 된다.

2. Record 필드

레코드 필드는 내장된 게터를 통해 액세스할 수 있다. 레코드는 변경할 수 없으므로 필드에는 세터가 없다.
명명된 필드는 같은 이름의 게터를 노출하고 위치 필드는 명명된 필드를 건너뛰고 $<포지션>이라는 이름의 게터를 노출한다:

// 'record' 변수에 레코드를 생성 및 초기화
var record = ('first', a: 2, b: true, 'last');

// 레코드 필드에 접근하여 값을 출력

// '$1'은 레코드의 첫 번째 요소인 'first'를 나타냄
print(record.$1); // 'first' 출력

// 'a'는 레코드의 두 번째 요소인 2를 나타냄
print(record.a); // 2 출력

// 'b'는 레코드의 세 번째 요소인 true를 나타냄
print(record.b); // true 출력

// '$2'는 레코드의 네 번째 요소인 'last'를 나타냄
print(record.$2); // 'last' 출력

구조분해 할당을 사용해 좀 더 간단히 표현 가능하다

// 'record' 변수에 레코드를 생성 및 초기화
var record = ('first', a: 2, b: true, 'last');

// 레코드의 필드를 추출하여 변수에 할당
var (first, a: alpha, b: beta, last) = record;

// 'first' 변수는 레코드의 첫 번째 요소 'first'와 동일
print(first); // 'first'를 출력

// 'alpha' 변수는 레코드의 'a' 필드, 즉 두 번째 요소 2와 동일
print(alpha); // 2를 출력

// 'beta' 변수는 레코드의 'b' 필드, 세 번째 요소 true와 동일
print(beta); // true를 출력

// 'last' 변수는 레코드의 네 번째 요소 'last'와 동일
print(last); // 'last'를 출력

3. Record 타입

개별 레코드 타입에 대한 타입 선언은 없지만 레코드는 필드 타입에 따라 구조적으로 타입이 지정된다.
레코드의 모양(필드 집합, 필드 유형 및 이름(있는 경우)이 레코드의 타입을 고유하게 결정한다.
또한 레코드의 각 필드에는 고유한 유형이 있어 필드 유형은 동일한 레코드 내에서 다를 수 있다.
타입시스템은 레코드에서 액세스하는 모든 위치에서 각 필드의 타입을 인식한다:

// 레코드 'pair'를 생성 및 초기화
(num, Object) pair = (42, 'a'); //정수 42와 문자열 'a'가 순서대로 포함

// 레코드 필드를 사용하여 변수에 할당

// 레코드의 첫 번째 요소. 
// 정적 유형은 `num`이며, 실행 시 유형은 `int`(num이 int를 포함하기 때문)
var first = pair.$1; 

// 레코드의 두 번째 요소. 
// 정적 유형은 `Object`이며, 실행 시 유형은 `String`(Object가 모든 객체유형을 포함하기 때문)
var second = pair.$2;

 

4. Record 동등

두 레코드의 모양(필드 집합)이 같고 해당 필드의 값이 같으면 두 레코드는 동일하다.
명명된 필드 순서는 레코드 모양의 일부가 아니므로 명명된 필드의 순서는 동일성에 영향을 미치지 않는다.
예시:

// 'point' 레코드를 생성 및 초기화
(int x, int y, int z) point = (1, 2, 3); // 3개의 필드 포함

// 'color' 레코드를 생성 및 초기화
(int r, int g, int b) color = (1, 2, 3); // 3개의 필드 포함

// 'point'와 'color' 레코드 간의 동등성 비교를 수행: 각각 (1, 2, 3)의 값을 가지므로 동등
print(point == color); // 'true'를 출력

Dart에서는 레코드 간의 동등성 비교가 필드 값의 동일성을 기반으로 이루어진다.

// 각 레코드는 중괄호 {}를 사용하여 명명된 매개변수를 가지며, 각 매개변수는 정수 값으로 초기화

// 'point' 레코드를 생성 및 초기화
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);

// 'color' 레코드를 생성 및 초기화
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);

// 'point'와 'color' 레코드 간의 동등성 비교를 수행:
print(point == color); // 'false'를 출력. Lint: Equals on unrelated types.

위 코드 같은경우는 false를 반환한다. 이유는 레코드의 필드 이름과 타입은 같지만 매개변수명과 값의 일치는 동등성에 영향을 미치지 않아 false가 되는 것이다. 또한 Dart에서는 레코드의 동등성은 레코드 자체의 타입이 동일해야 하는데 이 경우 두 레코드의 타입이 서로 달라 false가 되는 것이다.

5. 다중 반환(Multiple returns)

레코드는 함수에서 여러 개의 값을 반환하는 것이 가능하다. 가령 아래 코드처럼 함수 반환값은 한개의 레코드 이지만, 여러개의 값일 수 있는 것이다. 또한 구조분해할당을 사용해 적은 변수 갯수로 똑같은 기능을 수행할 수 있다.

// 레코드 패턴을 사용하여 여러 값을 반환
// => Map<String, dynamic> 형식의 json 맵을 인수로 받아서, 
//    json 맵에서 'name'과 'age' 키를 사용하여 레코드를 생성하고 반환
(String, int) userInfo(Map<String, dynamic> json) {
  // 'json' 맵에서 'name'과 'age' 키를 사용하여 레코드를 생성
  return (json['name'] as String, json['age'] as int);
}

// JSON 데이터를 나타내는 맵을 생성
final json = <String, dynamic>{
  'name': 'Dash',
  'age': 10,
  'color': 'blue',
};

// 레코드 패턴을 사용하여 userInfo 함수에서 반환된 레코드의 값을 추출
// => 'name'과 'age' 변수가 각각 해당 레코드의 첫 번째 및 두 번째 요소로 설정됨
var (name, age) = userInfo(json);

/* 다음에 해당:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

 

 

참고: https://dart.dev/language/records#record-fields

반응형