List
List<T>
는 .NET에서 가장 많이 사용되는 제네릭 컬렉션 클래스 중 하나로, 동적 배열의 기능을 제공합니다. 비제네릭 컬렉션인 ArrayList의 한계를 극복하고 타입 안전성과 성능을 향상시키기 위해 도입되었습니다. System.Collections.Generic
네임스페이스에 포함되어 있으며, 타입 매개변수를 사용하여 특정 타입의 데이터를 안전하게 저장할 수 있습니다.
List의 기본 사용법
List<T>
를 사용하면 타입을 명시적으로 지정하여 타입 안전성을 보장받을 수 있습니다. 다음은 List<int>
를 사용하는 기본 예제입니다:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
// 요소 출력
foreach (int number in numbers)
{
Console.WriteLine(number);
}
// 출력:
// 1
// 2
// 3
}
}
List<T>
의 특징
List<T>
는 다음과 같은 특징을 가지고 있습니다:
- 타입 안전성: 컴파일 시 타입을 명확하게 지정하여 런타임 오류를 줄이고, 안전한 코드를 작성할 수 있습니다.
- 유연한 크기 조정: 리스트의 크기가 자동으로 조정되므로 고정 크기의 배열보다 유연하게 사용할 수 있습니다.
- 박싱 및 언박싱 문제 해결: 값 타입을 저장할 때 박싱 및 언박싱이 발생하지 않아 성능을 향상시킵니다.
장점
- 사용의 편의성: 다양한 메서드와 속성을 제공하여 데이터 관리가 용이합니다.
- 성능: 인덱스를 통한 빠른 접근(O(1))과 동적 크기 조절로 효율적입니다.
- LINQ 지원: LINQ를 사용하여 데이터를 손쉽게 쿼리하고 조작할 수 있습니다.
단점
- 스레드 안전하지 않음: 기본적으로 스레드 안전성을 제공하지 않으므로, 다중 스레드 환경에서 주의가 필요합니다.
- 중간에서의 삽입/삭제 비용: 리스트의 중간에 요소를 삽입하거나 삭제할 때 요소의 이동으로 인한 성능 저하가 발생할 수 있습니다.
주요 메서드와 속성
생성자와 초기화
기본 생성자
빈 리스트를 생성합니다.
List<int> numbers = new List<int>();
초기 용량 지정 생성
리스트의 초기 용량을 설정하여 성능을 최적화 합니다.
List<int> numbers = new List<int>(capacity: 100);
- 초기 용량 설정을 통한 최적화를 참조하세요.
컬렉션 초기화 사용
리스트를 초기화하면서 동시에 값을 할당합니다.
List<int> numbers = new List<int> { 1, 2, 3 };
- 리스트의 크기 및 메모리가 한 번에 설정되므로 성능상 이점이 있습니다.
배열에서 리스트로 변환
배열을 List<T>
로 변환합니다.
int[] array = new int[] { 1, 2, 3, 4, 5 };
List<int> list = new List<int>(array); // 추가 메모리 할당 없이 배열을 리스트로 변환
List<T>
생성 시 배열을 인자로 전달하면 배열을 바로 리스트의 내부 배열로 사용해 추가 메모리 할당을 줄일 수 있습니다.
타깃 타입 추론(C# 9.0 이상)
new
키워드를 통해 타입을 추론하여 간결하게 초기화합니다.
List<int> numbers = new() { 1, 2, 3 };
요소 추가
Add
리스트의 끝에 요소를 추가합니다.
numbers.Add(4);
AddRange
리스트에 여러 요소를 한꺼번에 추가합니다.
numbers.AddRange(new List<int> { 5, 6, 7 });
- 내부적으로 요소 수에 맞게 버퍼를 한 번에 복사하므로, 요소를 하나씩 추가하는
Add
에 비해 일반적으로 성능이 더 좋습니다.
요소 접근 및 검색
Contains, IndexOf, Find, FindAll 은 선형검색 방식을 사용 하며 다음과 같은 특징을 가집니다.
- 항상 처음부터 순차적으로 검색하며, 시간 복잡도는 O(n)입니다.
- 요소가 없는 경우, 리스트의 끝까지 탐색하며 비교 작업을 수행하므로 성능이 떨어집니다.
- 성능이 중요한 경우에는 HashSet 등의 대안을 고려하는 것이 좋습니다.
인덱서를 통한 접근
특정 인덱스의 요소에 접근합니다.
int firstNumber = numbers[0];
Contains
리스트에 특정 요소가 포함되어 있는지 확인합니다.
bool hasNumber = numbers.Contains(5);
IndexOf
특정 요소의 인덱스를 반환합니다.
int index = numbers.IndexOf(5);
- 리스트에 없는 요소를 검색해도
-1
을 반환하여 검색 실패를 나타내며, 예외를 발생시키지 않습니다.
Find
조건에 맞는 첫 번째 요소를 반환합니다.
int foundNumber = numbers.Find(n => n > 5);
FindAll
조건에 맞는 모든 요소를 리스트로 반환합니다.
List<int> largeNumbers = numbers.FindAll(n => n > 5);
BinarySearch
정렬된 리스트에서 이진 탐색을 사용하여 요소의 인덱스를 반환합니다.
numbers.Sort(); // 정렬 필요
int index = numbers.BinarySearch(5);
- 중앙 요소부터 시작해 검색 범위를 반씩 줄이면서 탐색하므로, 정렬된 리스트에서만 사용할 수 있습니다.
- 시간 복잡도는 O(log n)으로, 리스트가 클수록
IndexOf
와의 검색속도 차이도 더 커집니다.
요소 제거
Remove
리스트에서 첫 번째로 일치하는 특정 요소를 제거합니다.
numbers.Remove(4);
Remove
메서드는 삭제하려는 요소가 리스트에 존재하여 성공적으로 삭제되면true
를 반환하고, 해당 요소가 리스트에 없으면 아무 동작도 하지 않고false
를 반환합니다.- 리스트에 해당 요소가 없더라도 예외를 던지지 않으므로, 삭제 작업이 안전하게 처리됩니다.
- 요소를 삭제하면 리스트의 나머지 요소들을 앞으로 이동해야 하므로, 추가적인 비용이 발생합니다. 따라서 삭제 위치가 리스트의 앞쪽일수록 더 많은 요소를 이동해야 하므로 비용이 높아집니다.
RemoveAt
지정된 인덱스의 요소를 제거합니다.
numbers.RemoveAt(0);
- 지정된 인덱스가 리스트 범위를 벗어난 경우
ArgumentOutOfRangeException
예외가 발생합니다. - 예를 들어, 리스트의 요소 수가 4개인데
RemoveAt(5)
를 호출하면 예외가 발생합니다.
RemoveAll
조건에 맞는 모든 요소를 제거합니다.
numbers.RemoveAll(n => n > 5);
Clear
리스트의 모든 요소를 제거합니다.
numbers.Clear();
리스트 정렬 및 변환
Sort
리스트의 요소를 오름차순으로 정렬합니다.
numbers.Sort();
- 내림차순으로 정렬 시,
Sort
메서드와Reverse
메서드를 함께 사용하거나,Sort
메서드에 람다 식을 사용해 비교 함수를 전달할 수 있습니다.
List<int> numbers = new List<int> { 3, 1, 4, 1, 5 };
numbers.Sort(); // 오름차순으로 정렬
numbers.Reverse(); // 정렬된 리스트를 반전하여 내림차순으로 변환
Console.WriteLine(string.Join(", ", numbers)); // 출력: 5, 4, 3, 1, 1
- 람다 식을 사용한 정렬이 일반적으로 더 효율적입니다.
List<int> numbers = new List<int> { 3, 1, 4, 1, 5 };
numbers.Sort((a, b) => b.CompareTo(a)); // 내림차순으로 정렬
Console.WriteLine(string.Join(", ", numbers)); // 출력: 5, 4, 3, 1, 1
Reverse
리스트의 요소를 역순으로 정렬합니다.
numbers.Reverse();
- 리스트의 절반만 순회하면서 처음과 끝을 서로 교환하는 방식으로 동작합니다.
- 시간 복잡도는 O(n)이며, 리스트의 크기에 비례하는 성능을 가집니다.
- 공간 복잡도는 O(1)이며, 리스트 내에서 직접 요소 위치를 바꾸기 때문에 추가 메모리를 사용하지 않습니다. 복사를 하지 않고 인덱스를 바꿔치기만 하므로 메모리 효율적입니다.
ConvertAll
리스트의 요소 타입을 변환하여 새로운 리스트를 생성합니다.
List<string> stringNumbers = numbers.ConvertAll(n => n.ToString());
기타 유용한 메서드
Insert
지정된 인덱스에 요소를 삽입합니다.
numbers.Insert(2, 99);
- 선형적인 요소 이동이 발생하며, 인덱스 뒤에 있는 모든 요소를 한 칸씩 뒤로 이동합니다.
- 삽입할 위치가 리스트의 앞쪽에 가까울수록 더 많은 요소를 이동해야 하므로, 최악의 경우 O(n)의 시간 복잡도를 가집니다.
- 리스트의 마지막 위치에 삽입하는 경우에는 이동할 요소가 없으므로, 평균 O(1)의 성능을 가지며, 단순히 요소를 추가하는
Add
와 비슷하게 동작합니다. - 빈번한 삽입이 필요하다면
LinkedList<T>
사용을 고려하세요
InsertRange
지정된 인덱스에 여러 요소를 삽입합니다.
numbers.InsertRange(2, new List<int> { 88, 77 });
Capacity
리스트의 용량을 가져오거나 설정합니다.
numbers.Capacity = 200;
Count
리스트에 포함된 요소의 개수를 반환합니다.
int count = numbers.Count;
TrimExcess
리스트의 용량을 요소의 개수에 맞춰 줄여 메모리를 최적화합니다.
numbers.TrimExcess();
AsReadOnly
리스트를 읽기 전용 컬렉션으로 반환합니다.
var readOnlyNumbers = numbers.AsReadOnly();
List<T>
의 활용
복잡한 데이터 구조 관리
List<T>
는 객체의 리스트를 관리하는 데도 유용합니다. 예를 들어, 학생 정보를 관리하는 클래스의 리스트를 사용할 수 있습니다:
class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Name = "Alice", Age = 20 },
new Student { Name = "Bob", Age = 22 },
new Student { Name = "Charlie", Age = 21 }
};
foreach (Student student in students)
{
Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
// 출력 결과:
// Name: Alice, Age: 20
// Name: Bob, Age: 22
// Name: Charlie, Age: 21
}
}
}
위 예제에서는 Student
객체를 List<Student>
로 관리하여 학생 정보를 손쉽게 처리할 수 있습니다.
읽기 전용 컬렉션
리스트를 다른 코드에서 수정하지 못하게 하기 위해 읽기 전용으로 변환할 수 있습니다. AsReadOnly()
메서드를 사용하면 리스트를 읽기 전용으로 만들어 안전성을 높일 수 있습니다.
List<int> numbers = new List<int> { 1, 2, 3 };
var readOnlyNumbers = numbers.AsReadOnly();
// readOnlyNumbers는 수정이 불가능
다만, 닷넷 버전이 허용된다면 불변컬렉션을 사용하는 것이 더 권장됩니다.
LINQ와의 통합
List<T>
는 LINQ 쿼리와 결합하여 데이터를 쉽게 필터링하고 검색할 수 있습니다. 다음은 LINQ를 사용하여 리스트에서 특정 조건을 만족하는 요소를 찾는 예제입니다.
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
Console.WriteLine("Even numbers:");
foreach (int number in evenNumbers)
{
Console.WriteLine(number);
// 출력 결과:
// 2
// 4
// 6
// 8
// 10
}
}
}
자세한 내용은 LINQ 를 참조하세요.
리스트의 중복제거, 통합, 분할
LINQ 를 참조하세요.
커스텀 정렬 및 비교
List<T>
에서 Sort()
메서드를 사용할 때 기본 정렬 외에 사용자 지정 정렬을 적용하고 싶다면 IComparable<T>
또는IComparer<T>
인터페이스를 구현하여 사용할 수 있습니다.
IComparable<T>
구현
class Student : IComparable<Student>
{
public string Name { get; set; }
public int Age { get; set; }
public int CompareTo(Student other)
{
return this.Age.CompareTo(other.Age);
}
}
IComparer<T>
구현
class NameComparer : IComparer<Student>
{
public int Compare(Student x, Student y)
{
return x.Name.CompareTo(y.Name);
}
}
students.Sort(new NameComparer());
병렬 처리
PLINQ 사용: 대용량 데이터를 병렬로 처리하여 성능을 향상시킬 수 있습니다.
var processedNumbers = numbers.AsParallel().Select(n => Process(n));
데이터 바인딩과 UI 연동
WPF나 WinForms에서 List<T>
를 데이터 소스로 사용하여 UI와 바인딩할 수 있습니다.
dataGridView.DataSource = students;
- 데이터 변경 시 UI에 자동으로 반영되도록 하려면
ObservableCollection<T>
를 사용하는 것이 좋습니다.
사용자 정의 확장 메서드 작성
리스트의 기능을 확장하려면 확장 메서드를 작성하여 자주 사용하는 기능을 재사용할 수 있습니다. 이를 통해 코드 재사용성이 높아지고 리스트 작업이 간편해집니다.
public static class ListExtensions
{
public static void RemoveAllGreaterThan(this List<int> list, int threshold)
{
list.RemoveAll(x => x > threshold); // 확장 메서드로 간편히 사용
}
}
// 사용
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
numbers.RemoveAllGreaterThan(3);
이런 확장 메서드는 코드 가독성과 유지보수성을 높이며, 다양한 리스트 연산을 쉽게 재사용할 수 있습니다.