본문 바로가기

C#

C# 컬렉션 초기화와 Index/Range

컬렉션 초기화 구문과 Index/Range 연산자는 컬렉션을 간결하게 만들고, 슬라이싱을 쉽게 해줍니다. 실전에서 바로 쓸 수 있는 문법과 주의점을 정리합니다.

1. 컬렉션 초기화 기본

컬렉션 초기화는 내부적으로 Add 호출을 나열하는 문법 설탕입니다. 가독성과 선언적 스타일에 유리합니다.

var numbers = new List<int> { 1, 2, 3, 4 };

var dict = new Dictionary<string, int>
{
    { "Kim", 30 },   // Add("Kim", 30)
    { "Lee", 28 }
};

// C# 6+ 인덱서 초기화: 존재하면 갱신, 없으면 추가
var ages = new Dictionary<string, int>
{
    ["Kim"] = 30,
    ["Lee"] = 28,
    ["Lee"] = 29 // Lee 키가 이미 있으면 값만 덮어씁니다
};

사용자 타입도 Add 메서드만 제공하면 컬렉션 초기화를 지원합니다.

public class Team
{
    private readonly List<string> _members = new();
    public void Add(string name) => _members.Add(name);
    public int Count => _members.Count;
}

var team = new Team { "Kim", "Lee", "Park" }; // Team.Add(...)가 호출됩니다

2. 중첩 초기화와 최신 문법

딕셔너리 안에 리스트처럼 중첩 구조도 간단히 초기화할 수 있습니다. C# 9+에서는 대상 지정 new로 더 짧게 쓸 수 있습니다.

var squads = new Dictionary<string, List<string>>
{
    ["Red"] = new() { "A", "B" },
    ["Blue"] = new() { "C", "D", "E" }
};

불변 컬렉션은 컬렉션 초기화가 보통 기대대로 동작하지 않습니다. 불변 컬렉션의 Add는 새 인스턴스를 반환하므로, 초기화 구문으로는 결과가 누적되지 않습니다. 팩터리나 Builder를 사용합니다.

using System.Collections.Immutable;

var good1 = ImmutableList.CreateRange(new[] { 1, 2, 3 });
var builder = ImmutableList.CreateBuilder<int> { 1, 2, 3 };
var good2 = builder.ToImmutable();

3. Index/Range 한눈에

C# 8+에서는 ^와 ..로 끝에서부터 인덱싱과 슬라이싱이 가능합니다. 끝 인덱스는 항상 제외(exclusive)입니다.

var arr = new[] { 10, 20, 30, 40, 50 };
int last = arr[^1];       // 50, 끝에서 1번째
int secondLast = arr[^2]; // 40

int[] mid = arr[1..4];    // 20,30,40 (시작 포함, 끝 제외)
int[] head = arr[..3];    // 10,20,30 (처음부터)
int[] tail = arr[3..];    // 40,50 (끝까지)
int[] trim = arr[1..^1];  // 양끝 잘라내기: 20,30,40

문자열에도 동일하게 적용됩니다.

string s = "abcdef";
char lastCh = s[^1];  // 'f'
string first3 = s[..3]; // "abc"
string middle = s[2..^2]; // "cd"

Span/ReadOnlySpan에서 Range는 복사 없이 보기를 제공합니다. 배열/문자열의 Range 슬라이스는 새 배열/새 문자열을 만들어 복사합니다.

var data = Enumerable.Range(1, 10).ToArray();
Span<int> span = data;     // 배열 -> Span (뷰)
Span<int> slice = span[3..^3]; // 4,5,6,7
slice[0] = 999;            // data[3]도 999로 바뀝니다 (복사 없음)

4. List에서의 지원 범위

Index/Range는 배열, 문자열, Span/ReadOnlySpan, Memory/ReadOnlyMemory에서 바로 지원합니다. List<T>는 .NET 8부터 Index/Range 인덱서를 지원합니다. 그 이전 버전에서는 대안을 사용합니다.

var list = new List<int> { 10, 20, 30, 40, 50 };

// .NET 8 미만 대안
int last = list[list.Count - 1];
var middle = list.GetRange(1, list.Count - 2); // 20,30,40

// 고급: Span으로 변환해 슬라이싱 (.NET 5+)
using System.Runtime.InteropServices;
Span<int> view = CollectionsMarshal.AsSpan(list);
var core = view[1..^1]; // 20,30,40 (복사 없는 뷰)
core[0] = 999; // list[1]이 999로 변경됨. 리스트 수정 시 Span은 무효화될 수 있으니 주의.

5. 실전 팁

- 대량 초기화 시 List.Capacity를 먼저 설정하거나 AddRange를 고려하면 할당을 줄일 수 있습니다.

- Dictionary에서 키가 중복될 수 있으면 인덱서 초기화([key] = value)를, 중복이 논리적 오류면 Add 형태를 사용해 예외로 잡습니다.

- 큰 배열을 자를 때 배열 Range는 복사 비용이 듭니다. 성능이 중요하면 Span 슬라이싱을 고려합니다.

- 라이브러리 API에 Index/Range를 노출하려면 this[Index]/this[Range] 인덱서를 제공하거나, Slice(…), GetRange(…) 등 대안을 문서화합니다.

6. 종합 예제

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

public class Person { public string Name { get; set; } = ""; public int Age { get; set; } }

public static class Demo
{
    public static void Run()
    {
        // 1) 컬렉션/객체 초기화
        var people = new List<Person>
        {
            new() { Name = "Kim", Age = 30 },
            new() { Name = "Lee", Age = 28 },
            new() { Name = "Park", Age = 35 }
        };

        var byName = new Dictionary<string, Person>
        {
            ["Kim"] = people[0],
            ["Lee"] = people[1],
            ["Park"] = people[2]
        };

        // 2) Index/Range로 조회
        var ages = people.Select(p => p.Age).ToArray();
        int oldest = ages[^1];                 // 마지막 요소
        int[] midAges = ages[1..^1];           // 양끝 제외

        string id = "USR-2026-APR";
        string suffix = id[^3..];              // "APR"

        // 3) 성능형 슬라이스: List -> Span
        var scores = new List<int> { 10, 20, 30, 40, 50, 60 };
        Span<int> span = CollectionsMarshal.AsSpan(scores);
        var lastThree = span[^3..];            // 40,50,60 (뷰)
        lastThree[0] = 999;                    // scores[3] == 999

        Console.WriteLine(oldest);
        Console.WriteLine(string.Join(",", midAges));
        Console.WriteLine(suffix);
        Console.WriteLine(string.Join(",", scores));
    }
}

정리하면, 컬렉션 초기화로 선언을 간결하게, Index/Range로 접근과 슬라이싱을 직관적으로 만들 수 있습니다. 대상 런타임(.NET 8 이전/이후)과 복사/뷰 특성을 고려하여 적절히 선택하면 좋습니다.