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

이런 확장 메서드는 코드 가독성과 유지보수성을 높이며, 다양한 리스트 연산을 쉽게 재사용할 수 있습니다.