C#LAB
C# 3.9 · 3부. 중급 문법

컬렉션 초기화자와 패턴 매칭 기초

값의 모양에 따라 처리를 정합니다.

예상 학습 시간 18분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 중급
개념 설명

모양을 그대로 적습니다

이 단원은 두 가지를 봅니다. 값을 만들면서 채우는 문법값이 어떤 모양인지 확인하는 문법입니다. 따로 배울 것 같지만 둘은 짝을 이루고 있습니다.

// 만들 때
var p = new Product { Name = "사과" };
int[] a = [1, 2, 3];

// 찾을 때 — 거의 같은 모양입니다
if (p is { Name: "사과" }) { }
if (a is [1, 2, 3]) { }

담을 때 적은 모양을 그대로 적으면 찾아집니다. 우연이 아니라 그렇게 설계된 것이고, 이 대칭을 알고 나면 새 패턴을 따로 외우지 않아도 됩니다.

1.5에서 switch 식과 >= 90 같은 관계 패턴을, 2.3에서 객체 초기화자를 이미 보았습니다. 여기서는 그 위에 나머지를 올립니다.

최소 예제 · 편집 가능

만들고, 모양으로 나누기

아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.

Program.cs
// 대괄호 하나로 목록을 만들면서 채웁니다
List<Product> products = [
    new("사과", 3000, true),
    new("포도", 12000, true),
    new("자두", 2000, false)
];

foreach (var p in products)
    Console.WriteLine($"{p.Name} : {Label(p)}");

// 속성의 모양으로 나눕니다. 위에서부터 처음 맞는 하나가 골라집니다.
static string Label(Product p) => p switch {
    { Usable: false }  => "지금은 팔지 않습니다",
    { Price: > 10000 } => "비싼 상품입니다",
    _                 => "보통 상품입니다"
};

class Product(string name, int price, bool usable) {
    public string Name { get; } = name;
    public int Price { get; } = price;
    public bool Usable { get; } = usable;
}
출력
사과 : 보통 상품입니다 포도 : 비싼 상품입니다 자두 : 지금은 팔지 않습니다

코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.

new("사과", 3000, true) 에 형식 이름이 없습니다. 왼쪽에 List<Product> 라고 적혀 있으니 컴파일러가 압니다. 클래스는 2.3의 기본 생성자로 적었습니다.

상세 사용법

컬렉션을 만들면서 채우기

Add 를 여러 번 부르는 대신 중괄호 안에 값을 나열할 수 있습니다(C# 3.0). 3.2에서 본 컬렉션들이 모두 됩니다.

var list = new List<string> { "사과", "포도" };
var set  = new HashSet<int> { 1, 2, 2, 3 };   // 1, 2, 3

// 사전은 두 가지로 적을 수 있습니다
var d1 = new Dictionary<string, int> { { "사과", 3000 }, { "포도", 5000 } };
var d2 = new Dictionary<string, int> { ["사과"] = 3000, ["포도"] = 5000 };

두 사전은 같은 것을 만듭니다. 아래쪽(C# 6.0)이 꺼내 쓸 때의 모양과 같아서 읽기 좋습니다. 같은 키를 두 번 적었을 때 위쪽은 예외가 발생하고 아래쪽은 덮어씁니다.

2.3의 객체 초기화자와도 함께 사용합니다. 읽기 전용 컬렉션 속성에도 중괄호로 값을 추가할 수 있습니다.

var order = new Order {
    No = "A-100",
    Buyer = new Buyer { Name = "홍길동", City = "서울" },   // 중첩됩니다
    Items = { "사과", "포도" }                              // Add 를 부릅니다
};

Items = { … }새 목록을 담는 것이 아니라 이미 있는 목록에 추가하는 것입니다. 그래서 get 만 있는 속성에도 적을 수 있습니다.

대괄호로 적는 컬렉션 식

C# 12부터는 대괄호만으로 적습니다. 형식 이름을 반복하지 않아도 되고, 배열이든 목록이든 왼쪽에 적힌 것으로 만들어집니다.

int[] a = [1, 2, 3];
List<int> b = [4, 5];
Span<int> s = [7, 8];

// .. 를 붙이면 그 안의 값이 펼쳐집니다
int[] all = [..a, ..b, 6];      // 1 2 3 4 5 6

..스프레드라고 합니다. 목록 둘을 이어 붙이거나 앞뒤에 값을 하나 추가할 때 짧게 적힙니다. 빈 컬렉션은 [] 입니다.

is 로 확인하고 그 자리에서 담기

형식을 확인한 뒤 그 형식으로 사용하려면 확인과 변환을 두 번 적어야 했습니다. is 패턴(C# 7.0)은 그것을 한 번에 합니다.

예전에는
if (v is string) {
    var s = (string)v;
    Console.WriteLine(s.Length);
}
지금은
if (v is string s) {
    Console.WriteLine(s.Length);
}

맞으면 s 에 담기고 아니면 담기지 않습니다. 3.6에서 본 is not null 도 같은 문법이고, 컴파일러가 그 뒤로 null 이 아니라고 판단하는 것도 이 덕분입니다.

값의 모양으로 거르기

속성 패턴은 중괄호 안에 속성 이름과 조건을 적습니다. 여러 개를 적으면 모두 맞아야 합니다.

static string Fee(Order o) => o switch {
    { Price: > 100000 }                => "무료 배송",
    { City: "서울", Price: > 10000 } => "당일 배송",
    { Price: < 5000 }                  => "배송비 3000원",
    _                                 => "배송비 2500원"
};

// A-100  50000원 서울 : 당일 배송
// B-200   3000원 부산 : 배송비 3000원
// C-300 120000원 서울 : 무료 배송
// D-400   8000원 대구 : 배송비 2500원

속성 안의 속성도 { Buyer.City: "서울" } 처럼 점으로 이어 적을 수 있습니다. 값이 무엇인지가 아니라 어떤 조건에 드는지를 적는 것1.5switch 와 달라진 점입니다.

튜플 패턴은 값 여럿을 한 번에 봅니다. 괄호로 묶어 넘기고 괄호로 묶어 받습니다.

static string Quadrant(int x, int y) => (x, y) switch {
    (0, 0) => "원점",
    (_, 0) => "가로축",
    (0, _) => "세로축",
    _      => "그 밖"
};

목록 패턴(C# 11)은 길이와 자리를 봅니다. 컬렉션 식과 같은 대괄호입니다.

static string Shape(int[] a) => a switch {
    []                     => "비었습니다",
    [var only]             => $"하나뿐 {only}",
    [1, .., 9]             => "1로 시작해 9로 끝납니다",
    [var first, .. var rest] => $"첫째 {first}, 나머지 {rest.Length}개"
};

// []          => 비었습니다
// [7]         => 하나뿐 7
// [1,9]       => 1로 시작해 9로 끝납니다
// [1,5,9]     => 1로 시작해 9로 끝납니다
// [1,2,3,4]   => 첫째 1, 나머지 3개

..몇 개든 상관없다는 뜻이고, .. var rest 라고 적으면 그 부분을 받아 둡니다. 컬렉션 식의 스프레드와 같은 기호가 반대 방향으로 사용됩니다.

조건이 패턴으로 적기 어려우면 when 을 덧붙입니다. 3.7의 예외 필터와 같은 낱말이지만 여기서는 그 경우를 선택할지 정하는 조건입니다.

v switch {
    int n when n > 40 => $"큰 정수 {n}",
    int n            => $"정수 {n}",
    string s         => $"글자 {s.Length}자",
    null             => "값이 없습니다",
    _                => "그 밖"
}

만드는 문법과 찾는 문법

지금까지 본 것을 나란히 놓으면 이렇습니다.

만들 때찾을 때
new Product { Name = "사과" }p is { Name: "사과" }
int[] a = [1, 2, 3];a is [1, 2, 3]
[first, ..rest][var first, .. var rest]
(x, y)(x, y) switch { (0, 0) => … }

등호가 콜론으로 바뀌는 정도의 차이입니다. 새 패턴을 만나면 그것을 만드는 문법을 떠올려 보세요. 대개 같은 모양입니다.

다만 완전히 같지는 않습니다. 만들 때는 [..a, ..b] 처럼 .. 를 여러 번 적을 수 있지만, 찾을 때는 한 번만 적을 수 있습니다. 어디까지가 앞의 나머지이고 어디부터가 뒤의 나머지인지 정할 수 없기 때문입니다.

여기까지가 기초입니다

패턴은 서로 안에 들어갈 수 있습니다. 속성 패턴 안에 목록 패턴을, 그 안에 다시 관계 패턴을 넣는 식입니다. 깊이 겹친 패턴과 record 를 분해하는 패턴은 4부에서 다룹니다.

그리고 패턴이 늘 나은 것은 아닙니다. 조건이 둘뿐이면 if 가 읽기 쉽고, 경우가 여럿이거나 값의 모양으로 나눌 때 패턴이 짧아집니다. 1.5에서 본 모든 경우를 덮어야 한다는 규칙도 그대로입니다.

버전 배지

이 문법, 몇 버전부터 사용할 수 있나요

3.0
6.0
7.0
8.0
9.0
10.0
11.0
12.0
14.0
  • 3.0컬렉션 초기화자 · 객체 초기화자
  • 6.0인덱스 초기화자 — ["사과"] = 3000
  • 7.0is 패턴 — 확인하면서 담기
  • 8.0switch 식 · 속성 패턴 · 튜플 패턴
  • 9.0관계 패턴 · 논리 패턴(and · or · not)
  • 11.0목록 패턴 — [1, .., 9]
  • 12.0컬렉션 식 · 스프레드 — [..a, ..b]
C# 3.0+

패턴 매칭은 한 버전에 들어온 기능이 아니라 일곱 해에 걸쳐 조금씩 늘어난 것입니다. C# 7.0의 is 패턴으로 시작해 버전마다 볼 수 있는 모양이 추가되었고, C# 12의 컬렉션 식이 들어오면서 만드는 쪽과 찾는 쪽의 대칭이 완성되었습니다. 목록 패턴이 11.0, 컬렉션 식이 12.0으로 찾는 쪽이 한 해 먼저인 것도 그래서입니다.

실습 문제

직접 해보기

1. if 를 패턴으로 바꾸기 난이도 하

아래 코드를 switch 식과 속성 패턴으로 바꿔보세요. 판단 순서가 바뀌지 않도록 주의합니다.

static string Fee(Order o) {
    if (o.Price > 100000)
        return "무료 배송";

    if (o.City == "서울" && o.Price > 10000)
        return "당일 배송";

    if (o.Price < 5000)
        return "배송비 3000원";

    return "배송비 2500원";
}
if 하나가 항목 하나가 됩니다. 조건 둘을 && 로 묶은 것은 중괄호 안에 쉼표로 나열하면 됩니다. 마지막 return_ 입니다.
static string Fee(Order o) => o switch { { Price: > 100000 } => "무료 배송", { City: "서울", Price: > 10000 } => "당일 배송", { Price: < 5000 } => "배송비 3000원", _ => "배송비 2500원" }; // A-100 : 당일 배송 // B-200 : 배송비 3000원 // C-300 : 무료 배송 // D-400 : 배송비 2500원 // 순서가 중요합니다. 무료 배송을 아래로 내리면 120000원짜리 서울 주문이 // 당일 배송에 먼저 걸립니다.
2. 명령 해석하기 난이도 중

띄어쓰기로 나뉜 명령을 string[] 으로 받아 목록 패턴으로 해석해보세요. list·add 이름·remove 이름 셋을 알아듣고, 빈 입력·이름이 둘 이상인 add·모르는 명령도 각각 다르게 답합니다.

칸 수가 정해진 것은 ["add", var name] 처럼 그대로 적습니다. 칸 수를 모르는 것은 .. 로 받습니다. 좁은 것을 위에 두어야 합니다.
static string Run(string[] words) => words switch { [] => "명령을 입력하세요", ["list"] => "전체 목록을 보여 줍니다", ["add", var name] => $"추가합니다: {name}", ["remove", var name] => $"지웁니다: {name}", ["add", ..] => "추가할 이름을 하나만 적으세요", [var cmd, ..] => $"모르는 명령입니다: {cmd}" }; // [] => 명령을 입력하세요 // [list] => 전체 목록을 보여 줍니다 // [add 사과] => 추가합니다: 사과 // [remove 포도] => 지웁니다: 포도 // [add 사과 포도] => 추가할 이름을 하나만 적으세요 // [show 전체] => 모르는 명령입니다: show // ["add", var name] 을 ["add", ..] 아래로 내리면 칸이 둘인 add 도 // 위에 먼저 걸려 이름을 받지 못합니다.
요약
  • 컬렉션은 중괄호에 값을 나열해 만들면서 채웁니다. 사전은 ["사과"] = 3000 이 읽기 좋습니다.
  • C# 12부터는 대괄호로 적고, .. 로 다른 컬렉션을 펼쳐 넣습니다.
  • is 패턴은 확인과 담기를 한 번에 합니다.
  • 속성 패턴은 { Price: > 10000 }, 목록 패턴은 [1, .., 9]값의 모양을 적습니다.
  • 패턴으로 적기 어려운 조건은 when 을 덧붙입니다.
  • 만드는 문법과 찾는 문법이 짝을 이룹니다. 등호가 콜론으로 바뀌는 정도의 차이입니다.
  • 경우가 여럿이거나 모양으로 나눌 때 패턴이 짧아집니다. 조건이 둘뿐이면 if 가 낫습니다.

C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.