1. 컬렉션 (Collcetion)
컬렉션은 여러 개의 데이터를 한 번에 관리할 수 있는 자료 구조로
컬렉션을 사용하면 여러 데이터를 효율적으로 저장하고 관리할 수 있습니다.
Dart에는 대표적을 List, Set, Map이 있습니다.
2. 리스트(List) - 순서를 가진 친구들 👥
리스트는 순서가 있는 데이터의 집합으로 배열이라고도 불립니다.
순서가 있기에 인덱스를 통해 요소에 접근할 수 있어요.
List<[타입]> [변수 이름] = [요소];
List<String> fruits = ['사과', '바나나', '딸기'];
print(fruits[0]); // 사과
변수를 선언할 때는 var로도 선언할 수 있는데요!
물론 리스트도 var로 선언할 수 있습니다.
그리고 final과 const로도 변수를 선언한다는 것 기억하시나요?
만약, 각 요소의 변동이 없다면 final과 const로도 선언 가능하답니다!
var numbers = [1, 2, 3, 4];
var fruits = ['사과', '바나나', '딸기'];
final numbers = [1, 2, 3, 4];
const fruits = ['사과', '바나나', '딸기'];
만약 리스트 안에 요소를 넣고 싶지 않다면 이렇게도 선언할 수 있습니다.
List<int> numbers = [];
var numbers = [];
var numbers = <String>[];
1) 특징
이런 리스트는 어떤 특징이 있을까요?
첫번째로, 각각의 요소는 모두 같은 타입이어야 해요.
만약, numbers 리스트를 int 타입으로 선언했는데 갑자기 문자열로 저장하면 오류가 나요.
List<String> fruits = ['사과', '바나나', 5'];
/**
Error: A value of type 'int' can't be assigned to a variable of type 'String'.
List<String> fruits = ['사과', '바나나', 5']
**/
사실 다른 타입의 요소들을 넣었을 때 오류가 발생하지 않는 경우도 있긴 합니다..!
var fruits = ['사과', '바나나', 8];
print(fruits.runtimeType);
print(fruits);
// List<Object>
// [사과, 바나나, 8]
var는 초기값을 기반으로 타입을 추론한다는 것 기억하시나요?
다른 타입의 요소를 넣은 후, 리스트의 타입을 보면 List<Object> 가 출력되는 것을 확인하실 수 있습니다.
여기서 Object는 int, double, String 등의 상위 타입인데요.
즉, int, double, String은 Object를 상속받은 것입니다.
때문에 다른 타입의 요소들도 넣을 수 있는 것이죠.
두 번째로 index를 통해 각 요소에 접근할 수 있어요.
리스트의 부제목 기억나시나요?
바로 순서를 가진 친구들인데요.
리스트는 순서를 가지고 있기에 각각의 요소에 index로 접근가능하답니다!
List<int> numbers = [10, 20, 30];
print(numbers[0]); // 10
print(numbers[1]); // 20
다만 주의할 점은 리스트의 첫 번째 요소는 0부터 시작해요.
첫 번째 요소라고 1부터 시작한다고 생각하시면 안 됩니다!
참고로 index에 음수를 넣으면 RangeError가 발생해요.
세 번째는 index를 통해 요소를 바꿀 수도 있습니다.
하지만 const와 final로 선언한 리스트는 바꾸지 못한다는 점!
List<int> numbers = [10, 20, 30];
numbers[1] = 99;
print(numbers); // [10, 99, 30]
마지막으로는 중복된 값을 허용해요!
List<int> numbers = [1, 2, 2, 3, 3, 3];
2) 관련 메서드
메서드 | 설명 |
add | 하나의 요소 추가 |
addAll | 여러 요소 추가 |
remove | 요소 삭제 |
removeAt | 특정 인덱스의 요소 삭제 |
clear | 모든 요소 삭제 |
length | 요소의 개수 |
isEmpty | 요소가 없으면 true, 하나라도 있으면 false |
isNotEmpty | 요소가 하나라도 있으면 true, 없으면 false |
indexOf | 특정 요소의 index |
contains | 값이 있는지 확인 |
var fruits = ['사과', '바나나', '딸기'];
fruits.add('배');
print(fruits);
fruits.addAll(['체리', '수박', '포도']);
print('addAll 결과 : $fruits');
fruits.remove('사과');
print('remove 결과 : $fruits');
fruits.removeAt(3);
print('removeAt 결과 : $fruits');
print('length : ${fruits.length}');
print('isEmpty : ${fruits.isEmpty}');
print('isNotEmpty : ${fruits.isNotEmpty}');
print('indexOf : ${fruits.indexOf('파인애플')}');
print('contains : ${fruits.contains('배')}');
fruits.clear();
print('fruits : $fruits');
/**
[사과, 바나나, 딸기, 배]
addAll 결과 : [사과, 바나나, 딸기, 배, 체리, 수박, 포도]
remove 결과 : [바나나, 딸기, 배, 체리, 수박, 포도]
removeAt 결과 : [바나나, 딸기, 배, 수박, 포도]
length : 5
isEmpty : false
isNotEmpty : true
indexOf : -1
contains : true
fruits : []
**/
3. Sets - 중복은 가라! 🚫
앞서 List는 중복을 허용한다는 것 기억나시나요?
Set는 List와 달리 중복을 허용하지 않는 데이터 구조입니다.
Set<타입> [변수 이름] = {요소};
Set<int> numbers = {1, 2, 3, 4};
var numbers = {1, 2, 3, 4};
만약 요소를 넣고 싶지 않다면 이렇게도 할 수 있어요.
Set<int> numbers = {};
var names = <String>{};
1) 특징
중복이 허용되지 않는 Set에는 어떤 특징이 있을까요?
첫 번째로 각각의 요소는 모두 같은 타입이어야 해요.
이건 List와 똑같은 특징이죠?
하나라도 다른 타입인 요소가 포함되면 오류가 나요.
Set<String> fruits = {'사과', '바나나', '딸기', 8};
// Error: A value of type 'int' can't be assigned to a variable of type 'String'
또한, List와 마찬가지로 타입이 다른 요소들을 넣었을 때 오류가 발생하지 않는 경우도 있습니다.
var fruits = {'사과', '바나나', '딸기', 8};
print(fruits.runtimeType); // LinkedSet<Object>
두 번째로 요소들의 순서가 없다는 점인데요.
순서가 없기 때문에 List처럼 [0], [1] 의 방식으로 요소에 접근할 수 없습니다.
그렇다면 Set에서는 어떻게 요소에 접근할까요?
바로 for문을 이용하는 것입니다.
Set<String> fruits = {'사과', '바나나', '딸기'};
// for-in
for (var fruit in fruits) {
print(fruit);
}
print('-------------');
// forEach()
fruits.forEach((fruit) => print(fruit));
/**
사과
바나나
딸기
-------------
사과
바나나
딸기
**/
세 번째는 중복된 값을 저장할 수 없다는 것입니다.
Set의 가장 큰 특징이에요.
같은 값이 여러 번 추가되면 첫 번째로 저장된 값만 남고 나머지는 무시됩니다.
Set<String> fruits = {'사과', '바나나', '딸기', '사과', '바나나'};
print(fruits); // {사과, 바나나, 딸기}
2) 관련 메서드
메서드 | 설명 |
add | 하나의 요소 추가 |
addAll | 여러 요소 추가 |
remove | 요소 삭제 |
clear | 모든 요소 삭제 |
length | 요소의 개수 |
isEmpty | 요소가 없으면 true, 하나라도 있으면 false |
isNotEmpty | 요소가 하나라도 있으면 true, 없으면 false |
contains | 값이 있는지 확인 |
var fruits = {'사과', '바나나', '딸기'};
fruits.add('배');
print(fruits);
fruits.addAll(['체리', '수박', '포도']);
print('addAll 결과 : $fruits');
fruits.remove('사과');
print('remove 결과 : $fruits');
print('length : ${fruits.length}');
print('isEmpty : ${fruits.isEmpty}');
print('isNotEmpty : ${fruits.isNotEmpty}');
print('contains : ${fruits.contains('배')}');
fruits.clear();
print('fruits : $fruits');
/**
{사과, 바나나, 딸기, 배}
addAll 결과 : {사과, 바나나, 딸기, 배, 체리, 수박, 포도}
remove 결과 : {바나나, 딸기, 배, 체리, 수박, 포도}
length : 6
isEmpty : false
isNotEmpty : true
contains : true
fruits : {}
**/
4. Map - We are One! 🤝
Map은 키(Key)와 값(Value)이 하나의 쌍으로 데이터를 저장하는 구조를 가졌어요.
Map<Key 타입, value 타입> 변수 이름 = {key: value};
Map<String, int> ages = {'Alice' : 25, 'Bob' : 30};
print(ages); // {Alice: 25, Bob: 30}
Map에 어떤 요소도 넣고 싶지 않다면 이렇게 작성해요!
Map<String, int> = people = {};
var people = {};
1) 특징
Map에는 어떤 특징이 있을까요?
첫 번째는 Key와 Value는 각각 타입이 같아야 해요.
Map<String, int> ages = {'Alice' : 25, 'Bob' : 'student'};
print(ages);
/**
Error: A value of type 'String' can't be assigned to a variable of type 'int'.
Map<String, int> ages = {'Alice' : 25, 'Bob' : 'student'};
**/
마찬가지로 타입이 달라도 오류가 발생하지 않는 경우도 있답니다.
var ages = {'Alice' : 25, 'Bob' : 'student'};
print(ages.runtimeType);
print(ages);
/**
IdentityMap<String, Object>
{Alice: 25, Bob: student}
**/
두 번째는 Key를 통해 Value를 저장하고 찾을 수 있어요.
List는 인덱스를 통해 요소를 찾지만, Map은 Key를 이용하는 것이죠!
var ages = {'Alice' : 25, 'Bob' : 30};
print(ages['Alice']); // 25
세 번째로 Map의 Key는 중복될 수 없지만, Value는 중복될 수 있어요.
만약, 같은 Key가 두 번 들어가면 마지막 값이 덮어쓰기 됩니다.
var ages = {'Alice' : 25, 'Bob' : '30', 'Alice' : 35};
print(ages['Alice']); // 35
네 번째로 빠르게 검색이 가능하다는 점인데요.
Map은 List보다 특정 값을 찾는 속도가 더 빠릅니다.
왜 그러냐면, List에서 값을 찾으려면 모든 요소를 순차적으로 검사하기 때문에 시간이 오래 걸려요.
반면, Map은 Hash Table 구조를 갖고 있기 때문에 Key를 이용해서 빠르게 찾을 수 있어요.
여기서 Hash Table은 특정한 값을 빠르게 찾기 위한 자료구조예요.
흔히 아는 사전이나 전화번호부와 비슷한 개념이에요.
비유를 하자면 전화번호부에서 이름으로 전화번호를 찾을 때, 첫 번째 페이지부터 하나하나 찾으면 시간이 오래 걸리지만,
Hash Table을 사용하면 이름 -> 특정 페이지로 바로 이동해서 한 번에 찾을 수 있어요.
Map에서는 Key를 Hash Funtion(해시 함수)을 이용해 숫자로 변환해요.
이 Hash Function은 어떤 입력값(Key)이 들어오더라도 항상 같은 숫자(Hash Value)를 반환하는 함수예요.
Key | Hash Function(해시 함수) 변환 -> 해시 값(저장 위치) |
'Alice' | 101번 위치 |
'Bob' | 205번 위치 |
즉, ages['Alice']를 검색할 때 Alice를 Hash Funtion에 넣어 해시 값을 구해요.
이 위치로 바로 이동하여 데이터를 반환(25)하는 거죠.
2) 관련 메서드
메서드 | 설명 |
[변수 이름][키 이름]] = [값] | 새로운 키와 값 추가 |
remove | 요소 삭제 |
clear | 모든 요소 삭제 |
length | 요소의 개수 |
isEmpty | 요소가 없으면 true, 하나라도 있으면 false |
isNotEmpty | 요소가 하나라도 있으면 true, 없으면 false |
[변수 이름][[키 이름]] | 검색 |
[변수 이름][[키 이름]] = [값] | 키의 값 수정 |
containsKey() | 특정 요소가 있으면 true, 없으면 false 반환 |
keys | 모든 키 반환 |
values | 모든 값 반환 |
Map<String, int> ages = {'Alice' : 25, 'Bob' : 30};
print(ages);
ages['Nana'] = 20;
print(ages);
ages.remove('Alice');
print('remove 결과 : $ages');
print('length : ${ages.length}');
print('isEmpty : ${ages.isEmpty}');
print('isNotEmpty : ${ages.isNotEmpty}');
print(ages['Bob']);
ages['Bob'] = 15;
print(ages);
print('containsKey : ${ages.containsKey('Anna')}');
print(ages.keys);
print(ages.values);
ages.clear();
print('ages : $ages');
/**
{Alice: 25, Bob: 30}
{Alice: 25, Bob: 30, Nana: 20}
remove 결과 : {Bob: 30, Nana: 20}
length : 2
isEmpty : false
isNotEmpty : true
30
{Bob: 15, Nana: 20}
containsKey : false
(Bob, Nana)
(15, 20)
ages : {}
**/
5. 참고자료
https://dart.dev/language/collections
Collections
Summary of the different types of collections in Dart.
dart.dev
'Dart' 카테고리의 다른 글
[Dart] 함수형 프로그래밍 (Functional Programming) (0) | 2025.03.12 |
---|---|
[Dart] 제네릭 (Generics) - 제네릭 함수 & 클래스 (0) | 2025.03.12 |
[Dart] 함수 (Functions) (0) | 2025.03.11 |
[Dart] 열거형 (Enumerated types) (0) | 2025.03.11 |