컬렉션: List, Set, Map
컬렉션: List, Set, Map
Dart는 list, set, map [컬렉션]을 내장으로 지원해요. 이 글에서는 세 컬렉션을 각각 어떻게 만들고 다루는지 살펴본 뒤, 컬렉션 리터럴 안에서 조건과 반복을 표현하는 다양한 *컬렉션 요소(collection element)*를 알아볼게요. 컬렉션이 담는 타입을 설정하는 방법은 [제네릭]에서 더 자세히 다룹니다.
List
거의 모든 프로그래밍 언어에서 가장 흔한 컬렉션은 배열이에요. 즉 순서가 있는 객체의 묶음이죠. Dart에서 배열은 List 객체이기 때문에 대부분 그냥 리스트라고 부릅니다.
Dart 리스트 리터럴은 대괄호([])로 감싼, 쉼표로 구분된 요소들의 목록으로 표시돼요. 각 요소는 보통 표현식입니다. 간단한 Dart 리스트를 볼게요.
var list = [1, 2, 3];
Dart 컬렉션 리터럴의 마지막 항목 뒤에는 쉼표를 붙일 수 있어요. 이 trailing comma는 컬렉션에는 영향을 주지 않지만, 복사·붙여넣기 오류를 막는 데 도움이 돼요.
var list = ['Car', 'Boat', 'Plane',];
리스트는 0부터 시작하는 인덱스를 사용해요. 0이 첫 요소의 인덱스이고 list.length - 1이 마지막 요소의 인덱스예요. .length 속성으로 길이를 얻고, 첨자 연산자([])로 요소에 접근할 수 있어요.
var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);
list[1] = 1;
assert(list[1] == 1);
컴파일 타임 상수인 리스트를 만들려면 리스트 리터럴 앞에 const를 붙여요.
var constantList = const [1, 2, 3];
// constantList[1] = 1; // This line will cause an error.
Set
Dart의 set은 고유 요소들의 순서 없는 컬렉션이에요. Dart의 set 지원은 set 리터럴과 Set 타입으로 제공됩니다.
set 리터럴로 만든 간단한 Dart set을 볼게요.
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
빈 set을 만들려면 타입 인자가 앞에 붙은 {}를 쓰거나, {}를 Set 타입 변수에 할당해요.
var names = <String>{};
// Set<String> names = {}; // This works, too.
// var names = {}; // Creates a map, not a set.
기존 set에 항목을 추가하려면 add()나 addAll() 메서드를 써요.
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
set 안의 항목 수는 .length로 얻어요.
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);
컴파일 타임 상수인 set을 만들려면 set 리터럴 앞에 const를 붙여요.
final constantSet = const {
'fluorine',
'chlorine',
'bromine',
'iodine',
'astatine',
};
// constantSet.add('helium'); // This line will cause an error.
Map
map에서 각 요소는 키-값 쌍이에요. 쌍 안의 각 키는 값과 연결되고, 키와 값 모두 어떤 타입의 객체든 될 수 있어요. 같은 값이 여러 다른 키와 연결될 수는 있어도 각 키는 한 번만 나타날 수 있어요. Dart의 map 지원은 map 리터럴과 Map 타입으로 제공됩니다.
map 리터럴로 만든 간단한 Dart map 두 개를 볼게요.
var gifts = {
// Key: Value
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings',
};
var nobleGases = {2: 'helium', 10: 'neon', 18: 'argon'};
Map 생성자로 같은 객체를 만들 수도 있어요.
var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';
var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';
기존 map에 새 키-값 쌍을 추가하려면 첨자 할당 연산자([]=)를 써요.
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // Add a key-value pair
map에서 값을 가져올 때는 첨자 연산자([])를 사용해요.
var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');
map에 없는 키를 찾으면 null이 반환돼요.
var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);
map의 키-값 쌍 수는 .length로 얻어요.
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);
컴파일 타임 상수인 map을 만들려면 map 리터럴 앞에 const를 붙여요.
final constantMap = const {2: 'helium', 10: 'neon', 18: 'argon'};
// constantMap[2] = 'Helium'; // This line will cause an error.
컬렉션 요소
컬렉션 리터럴은 일련의 요소를 담아요. 런타임에 각 요소가 평가되어, 결과 컬렉션에 삽입되는 값을 0개 이상 만들어 냅니다. 이 요소는 크게 두 범주로 나뉘어요. 리프 요소와 제어 흐름 요소입니다.
- 리프 요소: 컬렉션 리터럴에 항목 하나를 삽입한다.
- 표현식 요소: 단일 표현식을 평가해 결과 값을 컬렉션에 삽입한다.
- map 엔트리 요소: 키와 값 표현식의 쌍을 평가해 결과 엔트리를 컬렉션에 삽입한다.
- 제어 흐름 요소: 조건부 또는 반복적으로 0개 이상의 값을 주변 컬렉션에 추가한다.
- null 인지 요소: 표현식을 평가하고 결과가
null이 아니면 값을 주변 컬렉션에 삽입한다. - 스프레드 요소: 주어진 시퀀스(컬렉션 표현식)를 순회해 결과 값을 모두 주변 컬렉션에 삽입한다.
- null 인지 스프레드 요소: 스프레드 요소와 비슷하지만 컬렉션이
null이어도 허용하며, 그럴 때는 아무것도 삽입하지 않는다. - if 요소: 조건 표현식에 따라 내부 요소를 조건부로 평가하고, 조건이 거짓이면 선택적으로 다른
else요소를 평가한다. - for 요소: 주어진 내부 요소를 반복·반복적으로 평가해 결과 값을 0개 이상 삽입한다.
- null 인지 요소: 표현식을 평가하고 결과가
표현식 요소
표현식 요소는 단일 표현식을 평가하고 결과 값을 컬렉션에 삽입해요. 이 표현식은 리터럴, 변수, 연산자, 함수 호출, 생성자 호출 같은 다양한 구문을 아우를 수 있어요.
컬렉션 안에서 표현식 요소의 문법은 다음과 같아요.
<expression>
Map 엔트리 요소
map 엔트리 요소는 키와 값 표현식의 쌍을 평가하고 결과 엔트리를 컬렉션에 삽입해요. 이 쌍의 키와 값 모두 표현식일 수 있어요.
컬렉션 안에서 map 엔트리 요소의 문법은 다음과 같아요.
<key_expression>: <value_expression>
Null 인지 요소
null 인지 요소는 표현식을 평가하고, 결과가 null이 아니면 값을 주변 컬렉션에 삽입해요.
표현식 요소에서 null 인지 요소의 문법은 다음과 같아요.
?<expression>
map 엔트리 요소에서는 이렇게 써요.
// key is a null-aware element
?<key_expression>: <value_expression>
// value is a null-aware element
<key_expression>: ?<value_expression>
// key and value are null-aware elements
?<key_expression>: ?<value_expression>
다음 예시에서 null 인지 요소 ?absentValue의 결과는 absentValue가 null이기 때문에 items라는 리스트에 추가되지 않아요.
int? absentValue = null;
int? presentValue = 3;
var items = [
1,
?absentValue,
?presentValue,
absentValue,
5,
]; // [1, 3, null, 5]
다음 예시는 map 엔트리 요소 안에서 null 인지 요소를 다양한 방법으로 쓰는 걸 보여줍니다.
String? presentKey = 'Apple';
String? absentKey = null;
int? presentValue = 3;
int? absentValue = null;
var itemsA = {presentKey: absentValue}; // {Apple: null}
var itemsB = {presentKey: ?absentValue}; // {}
var itemsC = {absentKey: presentValue}; // {null: 3}
var itemsD = {?absentKey: presentValue}; // {}
var itemsE = {absentKey: absentValue}; // {null: null}
var itemsF = {?absentKey: ?absentValue}; // {}
스프레드 요소
스프레드 요소는 주어진 시퀀스를 순회해 결과 값을 모두 주변 컬렉션에 삽입해요.
컬렉션 안에서 스프레드 요소의 문법은 다음과 같아요. 시퀀스 표현식은 Iterable을 구현하는 객체로 평가되는 어떤 표현식이든 될 수 있어요.
...<sequence_expression>
다음 예시에서 a라는 리스트의 요소가 items라는 리스트에 추가됩니다.
var a = [1, 2, null, 4];
var items = [0, ...a, 5]; // [0, 1, 2, null, 4, 5]
null로 평가될 수 있는 표현식을 스프레드하면서 그 null을 무시하고 싶다면(요소를 삽입하지 않으려면) null 인지 스프레드 요소를 써요.
Null 인지 스프레드 요소
null 인지 스프레드 요소는 스프레드 요소와 비슷하지만, 컬렉션이 null이어도 허용하고 그럴 때는 아무것도 삽입하지 않아요.
컬렉션 안에서 null 인지 스프레드 요소의 문법은 다음과 같아요.
...?<sequence_expression>
다음 예시에서 a라는 리스트는 null이라 무시되지만, b라는 리스트의 요소는 items에 추가됩니다. 컬렉션 자체가 null이 아니더라도 그 안에 null 요소가 있으면 결과에도 그 null 요소가 들어가는 걸 주의하세요.
List<int>? a = null;
var b = [1, null, 3];
var items = [0, ...?a, ...?b, 4]; // [0, 1, null, 3, 4]
null 안전성 때문에, null일 수 있는 값에 스프레드 연산(...)을 쓸 수는 없어요. 다음 예시는 extraOptions 파라미터가 nullable이고 그에 쓴 스프레드 연산자가 null 인지가 아니라서 컴파일 타임 에러가 나요.
List<String> buildCommandLine(
String executable,
List<String> options, [
List<String>? extraOptions,
]) {
return [
executable,
...options,
...extraOptions, // <-- Error
];
}
// Usage:
// buildCommandLine('dart', ['run', 'my_script.dart'], null);
// Result:
// Compile-time error
nullable 컬렉션을 스프레드하려면 null 인지 스프레드 요소를 써요. 다음 예시는 extraOptions에 null 인지 스프레드 연산자를 썼기 때문에 유효해요.
List<String> buildCommandLine(
String executable,
List<String> options, [
List<String>? extraOptions,
]) {
return [
executable,
...options,
...?extraOptions, // <-- OK now.
];
}
// Usage:
// buildCommandLine('dart', ['run', 'my_script.dart'], null);
// Result:
// [dart, run, my_script.dart]
If 요소
if 요소는 컬렉션에 조건부 요소를 추가하는 방법이에요. 조건 표현식이 참이면 첫 번째 결과를 포함하고, 거짓이면 else 뒤의 결과를 포함해요.
// If the operation resolves as true, include the first
// result, otherwise, include the second result.
if (<bool_expression>) <result> else <result>
if 요소는 참일 때만 결과를 포함하는 형태로도 쓸 수 있어요.
var includeItem = true;
var items = [0, if (includeItem) 1, 2, 3]; // [0, 1, 2, 3]
거짓인 조건은 결과를 포함하지 않아요.
var includeItem = true;
var items = [0, if (!includeItem) 1, 2, 3]; // [0, 2, 3]
if-case와 결합하면 패턴에 따라 요소를 골라낼 수도 있어요. 다음 예시는 data의 타입별로 다른 문자열을 만듭니다.
Object data = 123;
var typeInfo = [
if (data case int i) 'Data is an integer: $i',
if (data case String s) 'Data is a string: $s',
if (data case bool b) 'Data is a boolean: $b',
if (data case double d) 'Data is a double: $d',
]; // [Data is an integer: 123]
For 요소
for 요소는 주어진 내부 요소를 반복·평가해 결과 값을 0개 이상 삽입해요.
컬렉션 안에서 for 요소의 문법은 이렇게 두 형태가 있어요.
for (<expression> in <collection>) <result>
for (<initialization_clause>; <condition_clause>; <increment_clause>) <result>
다음 예시들은 컬렉션 안에서 for 요소를 쓰는 다양한 방법을 보여줍니다.
var numbers = [2, 3, 4];
var items = [1, for (var n in numbers) n * n, 7]; // [1, 4, 9, 16, 7]
var items = [1, for (var x = 5; x > 2; x--) x, 7]; // [1, 5, 4, 3, 7]
var items = [1, for (var x = 2; x < 4; x++) x, 7]; // [1, 2, 3, 7]
제어 흐름 요소 중첩하기
제어 흐름 요소는 서로 중첩할 수 있어요. 다른 언어의 리스트 컴프리헨션에 대한 강력한 대안이에요.
다음 예시에서는 numbers의 짝수만 items에 포함됩니다.
var numbers = [1, 2, 3, 4, 5, 6, 7];
var items = [
0,
for (var n in numbers)
if (n.isEven) n,
8,
]; // [0, 2, 4, 6, 8]
if나 for 요소 바로 안에서 컬렉션 리터럴에 스프레드를 쓰는 건 흔하고 관용적인 패턴이에요. 예를 들어 다음처럼요.
var items = [
if (condition) oneThing(),
if (condition) ...[multiple(), things()],
]; // [oneThing, [multiple_a, multiple_b], things]
모든 종류의 요소를 임의로 깊게 중첩할 수 있어요. 다음 예시에서는 if, for, 스프레드 요소가 컬렉션 안에서 서로 중첩되어 있어요.
var nestItems = true;
var ys = [1, 2, 3, 4];
var items = [
if (nestItems) ...[
for (var x = 0; x < 3; x++)
for (var y in ys)
if (x < y) x + y * 10,
],
]; // [10, 20, 30, 40, 21, 31, 41, 32, 42]
더 알아보기
- 제네릭 — 컬렉션이 담는 타입 구성하기
dart:core의 컬렉션 — List, Set, Map의 Lists/Sets/Maps 섹션- 타입 시스템 — 타입 추론이 리터럴 타입에 미치는 영향