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의 주요 메소드와 속성

  1. add()
    List에 새로운 요소를 추가한다.
numbers.add(6);
  1. remove()
    List에서 특정 값을 제거한다. 첫 번째로 발견된 값만 제거된다.
numbers.remove(1);
  1. length
    List의 길이를 반환한다.
int size = numbers.length;
  1. insert()
    특정 위치에 값을 삽입한다.
numbers.insert(2, 10); // 2번째 위치에 10을 삽입
  1. 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로 시각화할 수 있다.

graph TD; A[1차원 List] --> B[2차원 List]; B --> C[3차원 List]; B --> D[데이터 접근];

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를 검색하거나 정렬하는 여러 가지 메서드를 제공한다.

  1. contains() 특정 값이 List에 포함되어 있는지 확인한다.
bool hasValue = numbers.contains(4); // 값 4가 List에 포함되어 있는지 확인
  1. indexOf() 특정 값이 List에서 처음으로 나타나는 인덱스를 반환한다. 값이 없으면 -1을 반환한다.
int index = numbers.indexOf(3); // 값 3의 인덱스 반환
  1. sort() List를 오름차순으로 정렬한다. 만약 사용자 정의 정렬을 원한다면 비교 함수를 사용할 수 있다.
numbers.sort(); // 기본 오름차순 정렬
numbers.sort((a, b) => b.compareTo(a)); // 내림차순 정렬
  1. 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의 크기를 자동으로 조정하여 데이터를 관리한다.

dart numbers.add(6);

dart numbers.addAll([7, 8, 9]);

dart numbers.remove(4);

dart numbers.removeAt(1); // 두 번째 값을 제거

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

위의 행렬을 수식으로 표현하면,

\mathbf{A} = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}

처럼 나타낼 수 있다. 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}는 다음과 같다.

\mathbf{C} = \begin{bmatrix} 6 & 8 \\ 10 & 12 \end{bmatrix}

이처럼 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에서 깊은 복사를 하려면 ListList.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<int> largeList = List.filled(1000000, 0); // 1,000,000개의 요소를 미리 할당

수학적 List 응용: 벡터 연산

List는 수학적 벡터로도 자주 사용된다. 벡터 연산을 List를 통해 구현하면 다양한 응용이 가능한다. 예를 들어, 두 벡터 \mathbf{a}\mathbf{b}가 있을 때, 이들의 합과 내적 연산은 다음과 같이 List로 표현할 수 있다.

\mathbf{a} = \begin{bmatrix} a_1 \\ a_2 \\ a_3 \end{bmatrix}, \quad \mathbf{b} = \begin{bmatrix} b_1 \\ b_2 \\ b_3 \end{bmatrix}

벡터 합

\mathbf{c} = \mathbf{a} + \mathbf{b} = \begin{bmatrix} a_1 + b_1 \\ a_2 + b_2 \\ a_3 + b_3 \end{bmatrix}

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

벡터 내적

\mathbf{a} \cdot \mathbf{b} = a_1 b_1 + a_2 b_2 + a_3 b_3

Dart에서 벡터의 내적은 다음과 같이 구현된다.

int dotProduct = List.generate(3, (i) => vectorA[i] * vectorB[i]).reduce((a, b) => a + b);

이와 같이 List를 이용한 수학적 연산은 간단히 구현할 수 있으며, 벡터나 행렬 계산을 효과적으로 처리할 수 있다.