Dart에서 List는 기본적으로 배열과 유사한 자료구조로 사용된다. 배열과는 다르게 List는 고정된 크기가 아니라 동적으로 크기가 변할 수 있으며, Dart의 컬렉션 중 가장 많이 사용되는 자료구조이다. List는 0개 이상의 객체를 순차적으로 저장하며, 다양한 메서드를 통해 데이터를 조작할 수 있다.
List 생성 방법
List를 생성하는 방법은 여러 가지가 있으며, 일반적인 배열처럼 데이터를 나열하거나 빈 List를 생성할 수도 있다. 예를 들어, 다음과 같이 List를 생성할 수 있다.
// 데이터가 있는 List 생성
List<int> numbers = [1, 2, 3, 4, 5];
// 빈 List 생성
List<String> names = [];
또한, List의 요소는 Dart에서 제공하는 다양한 타입을 포함할 수 있으며, 정적 타입과 동적 타입 모두 사용 가능한다.
List와 배열의 차이점
배열과 List는 비슷해 보이지만 Dart에서는 고정된 크기의 배열 개념을 사용하지 않으며, List를 통해 배열의 기능을 확장한 구조를 사용한다. Dart의 List는 크기가 가변적이어서 배열처럼 고정된 크기를 선언하는 대신, 데이터의 추가와 삭제가 자유롭게 이루어진다.
예를 들어, Python의 List와 유사한 기능을 제공하는 Dart의 List는 다음과 같은 동작을 수행할 수 있다:
List<int> numbers = [1, 2, 3];
numbers.add(4); // List에 값 추가
numbers.remove(2); // 값 제거
List의 주요 메소드와 속성
- add()
List에 새로운 요소를 추가한다.
numbers.add(6);
- remove()
List에서 특정 값을 제거한다. 첫 번째로 발견된 값만 제거된다.
numbers.remove(1);
- length
List의 길이를 반환한다.
int size = numbers.length;
- insert()
특정 위치에 값을 삽입한다.
numbers.insert(2, 10); // 2번째 위치에 10을 삽입
- sublist()
특정 범위의 List를 추출하여 새로운 List를 만든다.
List<int> subList = numbers.sublist(1, 3); // 인덱스 1에서 3까지의 List를 반환
List의 Index 접근
List에서 배열처럼 인덱스를 통해 값을 가져오거나 수정할 수 있다. List의 인덱스는 0부터 시작하며, 음수를 사용하여 뒤에서부터 접근할 수도 있다.
int firstValue = numbers[0]; // 첫 번째 값
numbers[1] = 20; // 두 번째 값을 20으로 변경
인덱스 접근 시 주의할 점은 인덱스 범위를 넘어서 접근할 경우 에러가 발생한다는 것이다. 이에 대비해 length
를 이용해 List의 길이를 확인하는 습관이 필요하다.
if (index < numbers.length) {
print(numbers[index]);
}
2차원 List
Dart에서는 List 안에 List를 넣어 2차원 이상의 List를 만들 수 있다. 이를 통해 행렬처럼 데이터를 관리할 수 있다. 예를 들어, 2x3 행렬을 다음과 같이 표현할 수 있다:
List<List<int>> matrix = [
[1, 2, 3],
[4, 5, 6]
];
이 경우 특정 값에 접근하려면 다음과 같이 하면 된다.
int value = matrix[1][2]; // 2행 3열의 값
행렬 구조의 데이터를 List로 다루면 복잡한 데이터 구조를 쉽게 관리할 수 있다. 이를 활용한 예시는 mermaid로 시각화할 수 있다.
List의 반복문
Dart에서는 다양한 방식으로 List를 순회할 수 있다. 가장 기본적인 방식은 for
문을 사용하는 것이다.
for (int i = 0; i < numbers.length; i++) {
print(numbers[i]);
}
또는 Dart의 forEach()
메서드를 사용하여 더 간결하게 표현할 수도 있다.
numbers.forEach((number) {
print(number);
});
forEach()
는 주로 코드가 간결해지고 가독성이 높아지므로 많이 사용되는 방식이다.
List의 검색 및 정렬
List에서 특정 값을 검색하거나 정렬하는 기능은 매우 유용하다. Dart는 List를 검색하거나 정렬하는 여러 가지 메서드를 제공한다.
- contains() 특정 값이 List에 포함되어 있는지 확인한다.
bool hasValue = numbers.contains(4); // 값 4가 List에 포함되어 있는지 확인
- indexOf() 특정 값이 List에서 처음으로 나타나는 인덱스를 반환한다. 값이 없으면 -1을 반환한다.
int index = numbers.indexOf(3); // 값 3의 인덱스 반환
- sort() List를 오름차순으로 정렬한다. 만약 사용자 정의 정렬을 원한다면 비교 함수를 사용할 수 있다.
numbers.sort(); // 기본 오름차순 정렬
numbers.sort((a, b) => b.compareTo(a)); // 내림차순 정렬
- reversed
List의 요소들을 역순으로 반환한다. 주의할 점은
reversed
는 새로운 Iterable을 반환하며, List로 다시 변환해야 할 필요가 있다.
List<int> reversedNumbers = numbers.reversed.toList(); // 역순으로 변환
List의 성능 고려
List는 다양한 자료형과 다양한 크기를 지원하지만, 성능을 고려해야 할 때도 있다. Dart에서 제공하는 List는 동적 크기를 지원하므로, 크기가 커질수록 성능에 영향을 미칠 수 있다. 크기가 변동하지 않는 고정된 크기의 List가 필요하다면, Dart의 List.filled()
메서드를 사용할 수 있다. 이는 크기가 고정된 List를 생성하며 성능 최적화에 유리할 수 있다.
List<int> fixedSizeList = List.filled(5, 0); // 5개의 0으로 채워진 고정 크기 List
또한, 크기가 일정하지 않은 List를 자주 조작하는 경우에는 add()
와 같은 메서드가 성능에 영향을 미칠 수 있다. 대용량 데이터를 다룰 때는 List의 크기 변동을 최소화하고, 미리 할당된 크기로 List를 생성하는 것이 좋다.
List의 동적 크기 변경
List의 크기를 동적으로 변경하는 기능은 매우 강력한다. 이를 통해 List는 데이터를 유연하게 추가, 삭제, 삽입할 수 있다. Dart에서는 List의 크기를 자동으로 조정하여 데이터를 관리한다.
- add(): 단일 값을 List에 추가한다.
dart
numbers.add(6);
- addAll(): 여러 값을 한 번에 List에 추가한다.
dart
numbers.addAll([7, 8, 9]);
- remove(): 특정 값을 제거한다. 처음 발견된 값만 제거된다.
dart
numbers.remove(4);
- removeAt(): 특정 인덱스의 값을 제거한다.
dart
numbers.removeAt(1); // 두 번째 값을 제거
- clear(): List의 모든 요소를 제거한다.
dart
numbers.clear();
이와 같은 메서드를 통해 List의 크기와 요소를 동적으로 관리할 수 있다.
2차원 List와 수학적 응용
Dart의 List는 다차원 데이터를 처리할 때 수학적으로도 유용하게 응용될 수 있다. 예를 들어, 2차원 배열(행렬)은 다음과 같은 방식으로 표현할 수 있다.
List<List<double>> matrix = [
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]
];
위의 행렬을 수식으로 표현하면,
처럼 나타낼 수 있다. Dart의 List를 이용하면 행렬 연산도 가능한다. 예를 들어, 두 행렬을 더하거나 빼는 것은 각 요소별로 연산을 수행하면 된다.
List<List<int>> matrixA = [
[1, 2],
[3, 4]
];
List<List<int>> matrixB = [
[5, 6],
[7, 8]
];
List<List<int>> matrixC = List.generate(2, (i) => List.generate(2, (j) => matrixA[i][j] + matrixB[i][j]));
이 경우, 결과 행렬 \mathbf{C}는 다음과 같다.
이처럼 List를 이용해 행렬 연산을 쉽게 구현할 수 있으며, 다차원 데이터를 효율적으로 관리할 수 있다.
List의 깊은 복사와 얕은 복사
Dart에서 List의 복사는 참조를 공유할 수 있는 얕은 복사(Shallow Copy)와 값을 완전히 복사하는 깊은 복사(Deep Copy)로 나눌 수 있다. 기본적으로 List의 복사는 얕은 복사로 동작하여, 복사된 List가 원본 List의 참조를 공유한다.
얕은 복사
얕은 복사는 단순히 List의 참조를 복사하는 것으로, 한 List에서 요소를 변경하면 원본 List에도 영향을 미친다. Dart에서 List의 얕은 복사는 단순한 대입으로 이루어진다.
List<int> original = [1, 2, 3];
List<int> shallowCopy = original;
shallowCopy[0] = 10;
print(original); // [10, 2, 3]
print(shallowCopy); // [10, 2, 3]
위 코드에서 보듯이 shallowCopy
에서 값을 수정하면 original
에도 동일하게 반영된다. 이는 두 List가 같은 메모리 참조를 공유하기 때문이다.
깊은 복사
깊은 복사는 원본 List의 값을 복사한 새로운 List를 만드는 것을 의미한다. Dart에서 깊은 복사를 하려면 List
의 List.from()
을 사용하거나 반복문을 통해 수동으로 복사해야 한다.
List<int> original = [1, 2, 3];
List<int> deepCopy = List.from(original);
deepCopy[0] = 10;
print(original); // [1, 2, 3]
print(deepCopy); // [10, 2, 3]
이 예에서는 deepCopy
가 원본 original
List와는 독립적인 메모리를 가지므로, 한 List의 변동이 다른 List에 영향을 주지 않는다.
다차원 List의 경우, 각 차원을 수동으로 복사해야 깊은 복사가 이루어진다. 예를 들어, 2차원 List를 깊은 복사하려면 모든 내부 List까지 복사해야 한다.
List<List<int>> matrix = [
[1, 2],
[3, 4]
];
// 깊은 복사
List<List<int>> deepMatrixCopy = matrix.map((list) => List.from(list)).toList();
deepMatrixCopy[0][0] = 10;
print(matrix); // [[1, 2], [3, 4]]
print(deepMatrixCopy); // [[10, 2], [3, 4]]
List의 효율적 사용: 메모리 및 성능 최적화
Dart의 List는 다양한 크기의 데이터를 유연하게 저장할 수 있지만, 성능과 메모리 사용에 주의해야 한다. List의 성능 최적화는 주로 다음과 같은 경우에 필요하다:
- 큰 데이터 세트 처리: 크기가 큰 List를 자주 조작하는 경우 성능 저하가 발생할 수 있다. 특히,
add()
메서드처럼 List의 크기를 동적으로 증가시키는 작업이 반복되면 메모리 재할당이 빈번히 일어나 성능에 악영향을 줄 수 있다. 따라서, 대용량 데이터를 다룰 때는 초기 List의 크기를 예측하여 필요한 만큼 미리 할당해두는 것이 좋다.
List<int> largeList = List.filled(1000000, 0); // 1,000,000개의 요소를 미리 할당
-
불필요한 List 복사 피하기: List를 다루면서 불필요하게 복사되는 것을 방지하는 것도 성능 최적화의 중요한 요소이다. 복사가 필요한 경우를 제외하고는, 참조를 사용하여 List를 처리하는 것이 메모리 사용을 줄이는 데 도움이 된다.
-
List 검색 최적화: 큰 List에서 데이터를 검색할 때는 시간 복잡도 O(n)을 가지므로, List보다는
Set
과 같은 다른 자료구조를 사용하는 것이 성능을 크게 개선할 수 있다. 특히 데이터 중복을 허용하지 않거나, 빠른 검색이 필요한 경우Set
이 List보다 효율적이다.
수학적 List 응용: 벡터 연산
List는 수학적 벡터로도 자주 사용된다. 벡터 연산을 List를 통해 구현하면 다양한 응용이 가능한다. 예를 들어, 두 벡터 \mathbf{a}와 \mathbf{b}가 있을 때, 이들의 합과 내적 연산은 다음과 같이 List로 표현할 수 있다.
벡터 합
Dart에서의 벡터 합은 List를 사용하여 다음과 같이 구현할 수 있다.
List<int> vectorA = [1, 2, 3];
List<int> vectorB = [4, 5, 6];
List<int> vectorC = List.generate(3, (i) => vectorA[i] + vectorB[i]);
벡터 내적
Dart에서 벡터의 내적은 다음과 같이 구현된다.
int dotProduct = List.generate(3, (i) => vectorA[i] * vectorB[i]).reduce((a, b) => a + b);
이와 같이 List를 이용한 수학적 연산은 간단히 구현할 수 있으며, 벡터나 행렬 계산을 효과적으로 처리할 수 있다.