C#LAB
C# 4.6 · 4부. 고급 문법

패턴 매칭 심화

겹쳐 놓은 패턴과 record 분해입니다.

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

패턴 안에 패턴

3.9에서 값의 모양으로 나누는 법을 배웠습니다. 거기서 미뤄 둔 것이 둘 있습니다. 패턴을 겹쳐 놓는 것record 를 괄호로 분해하는 것입니다.

패턴은 서로 안에 들어갑니다. 속성 패턴 안에 목록 패턴을, 그 안에 다시 관계 패턴을 넣을 수 있습니다. 겹쳐 놓으면 여러 단계를 파고 들어가 확인하는 일이 한 줄이 됩니다.

// 겹쳐 놓지 않으면
if (o.Customer is not null
    && o.Customer.Address is not null
    && o.Customer.Address.City == "서울"
    && o.Lines.Length > 2) { }

// 겹쳐 놓으면
if (o is { Customer.Address.City: "서울", Lines.Length: > 2 }) { }

중간에 null 이 있어도 안전합니다. 패턴은 가는 길에 null 을 만나면 그냥 맞지 않는 것으로 처리합니다. 위쪽처럼 is not null 을 단계마다 적을 필요가 없습니다.

최소 예제 · 편집 가능

여러 단계를 한 줄로

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

Program.cs
static string Describe(Order o) => o switch {
    // 속성 패턴 안에 속성 패턴, 그 안에 값 패턴
    { Customer: { Address: { City: "서울" } }, Lines: [] } => "서울인데 품목이 없습니다",

    // 같은 것을 점으로 이어 적을 수 있습니다(C# 10)
    { Customer.Address.City: "서울", Lines.Length: > 2 } => "서울, 품목 셋 이상",

    // 목록 패턴 안에 속성 패턴, 그 안에 관계 패턴
    { Lines: [{ Price: > 10000 }, ..] } => "첫 품목이 비쌉니다",

    { Lines: [] } => "품목이 없습니다",
    _ => "그 밖"
};

var orders = new Order[] {
    new(new Customer("홍길동", new Address("서울", "1길")),
        [new Line("책", 2, 12000)], "card"),
    new(new Customer("김철수", new Address("부산", "2길")), [], "bank"),
    new(new Customer("이영희", new Address("서울", "3길")),
        [new Line("펜", 1, 1000), new Line("종이", 10, 500),
         new Line("책상", 1, 200000)], "card"),
};

foreach (var o in orders)
    Console.WriteLine($"{Describe(o),-20} {o.Customer.Name}");

record Order(Customer Customer, Line[] Lines, string Pay);
record Customer(string Name, Address Address);
record Address(string City, string Street);
record Line(string Name, int Count, int Price);
출력
첫 품목이 비쌉니다 홍길동 품목이 없습니다 김철수 서울, 품목 셋 이상 이영희

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

김철수는 두 번째가 아니라 네 번째에 걸렸습니다. 첫 번째는 서울까지 함께 봐야 하는데 부산이기 때문입니다. 패턴은 위에서부터 차례로 맞춰 봅니다.

상세 사용법

점으로 이어 적기

속성 패턴을 겹쳐 놓는 것은 C# 8.0부터 되었지만, 단계마다 중괄호가 늘어 읽기 어려웠습니다. C# 10부터 점으로 이어 적을 수 있습니다.

중괄호로 — C# 8.0
o is {
    Customer: {
        Address: {
            City: "서울"
        }
    }
}
점으로 — C# 10
o is {
    Customer.Address.City: "서울"
}

둘은 같은 것입니다. 한 단계만 파고 들어간다면 점으로 적고, 같은 단계에서 여러 가지를 볼 때는 중괄호가 낫습니다.

// 같은 Address 에서 둘을 보려면 중괄호가 짧습니다
o is { Customer.Address: { City: "서울", Street: not null } }

괄호로 분해하기

Deconstruct 가 있는 형식은 괄호로 적어 분해할 수 있습니다. 이것을 위치 패턴이라고 합니다. 4.5record 는 위치로 적으면 그것이 함께 만들어집니다.

record Point(int X, int Y);

static string Where(Point p) => p switch {
    (0, 0)     => "원점",
    (_, 0)     => "가로축",
    (0, _)     => "세로축",
    ( > 0, > 0) => "1사분면",
    _          => "그 밖"
};

위치 패턴도 겹쳐 놓을 수 있습니다. 안쪽 괄호가 다시 분해입니다.

record Segment(Point From, Point To);

static string Line(Segment s) => s switch {
    (var a, var b) when a == b => "길이가 없습니다",
    ((0, _), (0, _))       => "세로축 위에 있습니다",
    _                        => "그 밖"
};

var 패턴은 무엇이든 맞으면서 이름을 붙여 둡니다. 값끼리 비교해야 할 때처럼 패턴만으로 적을 수 없는 조건은 when 으로 넘깁니다.

위치 패턴은 순서에 기댑니다. 나중에 record 의 매개 변수를 중간에 끼워 넣으면, 여기 적은 괄호가 조용히 다른 것을 가리킵니다. 매개 변수가 늘어날 만한 형식이라면 속성 패턴으로 이름을 적는 편이 안전합니다.

순서가 규칙입니다

패턴은 위에서부터 맞춰 보고 처음 맞은 것에서 멈춥니다. 넓은 것을 위에 두면 아래가 죽습니다. 이것은 경고가 아니라 오류입니다.

static string B(object o) => o switch {
    object => "무엇이든",
    string => "문자열",      // 여기에 닿을 수 없습니다
    _      => "그 밖"
};

// error CS8510: 패턴에 연결할 수 없습니다. 이미 switch 식의 이전 ARM에서
// 처리되었거나 일치시킬 수 없습니다.

좁은 것을 위에 두세요. 겹쳐 놓은 패턴에서는 특히 잘 보이지 않습니다. 최소 예제에서 { Lines: [] } 를 맨 위로 올리면 그 아래 서울 조건이 죽습니다.

빠뜨린 것은 알려 줍니다

switch 식은 모든 경우를 덮어야 합니다. 빠뜨리면 무엇이 빠졌는지까지 알려 줍니다.

static string A(int n) => n switch { > 0 => "양수", < 0 => "음수" };

// warning CS8509: switch 식에서 입력 형식의 가능한 값을 모두 처리하지는
// 않습니다(전체 아님). 예를 들어 '0' 패턴은 포함되지 않습니다.

경고를 무시하고 넘기면 실행할 때 예외입니다.

A(0);

// SwitchExpressionException: Non-exhaustive switch expression failed to
// match its input. Unmatched value was 0.

_ => 를 붙이면 경고는 사라집니다. 다만 그것으로 덮으면 나중에 경우가 늘어도 알려 주지 않습니다. 빠뜨리면 안 되는 자리에서는 _ 를 두지 않고 경고를 그대로 두는 편이 낫습니다.

열거형은 다 덮어도 남습니다

여기서 한 번 놀랍니다. 열거형의 항목을 모두 적어도 경고가 나옵니다.

enum Kind { A, B }

static string C(Kind k) => k switch { Kind.A => "가", Kind.B => "나" };

// warning CS8524: switch 식에서 명명되지 않은 열거형 값이 사용되는 입력
// 형식의 일부 값을 처리하지 않습니다. 예를 들어, 패턴 '(Kind)2'이(가)
// 포함되지 않았습니다.

열거형은 이름 붙인 값만 담기는 것이 아니기 때문입니다. (Kind)9 처럼 아무 정수나 넣을 수 있고, 데이터베이스나 외부에서 들어온 값이 그럴 수 있습니다.

C((Kind)9);

// SwitchExpressionException: Non-exhaustive switch expression failed to
// match its input. Unmatched value was 9.

그래서 열거형에는 _ 를 두는 편이 맞습니다. 모르는 값이 왔을 때 무엇을 할지 적어 두는 것이 실제로 필요한 일입니다. CS8509 와 달리 CS8524 는 덮을 방법이 없습니다.

static string C(Kind k) => k switch {
    Kind.A => "가",
    Kind.B => "나",
    _ => throw new ArgumentOutOfRangeException(nameof(k), k, null)
};

언제 겹치지 않는 편이 나은가

패턴이 늘 나은 것은 아닙니다. 깊이 겹쳐 놓으면 한 줄이 하는 일이 많아집니다. 그 줄이 안 맞았을 때 어느 단계에서 어긋났는지 알 수 없습니다.

이럴 때무엇을
모양으로 나누어 값을 정함패턴. 경우가 나란히 보입니다
왜 안 맞았는지 알려야 함if. 단계마다 다른 메시지를 낼 수 있습니다
조건이 둘뿐if. 1.5에서 본 그대로입니다
세 단계보다 깊이 파고듦중간을 변수에 담고 나누어 적습니다

입력 검사에는 특히 그렇습니다. 화면이 "주소를 확인하세요" 라고 말해야 하는데 패턴 하나로 묶어 두면 "맞지 않습니다" 밖에 말할 수 없습니다.

버전 배지

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

6.0
7.0
8.0
9.0
10.0
11.0
12.0
13.0
14.0
  • 7.0is 형식 패턴 · when
  • 8.0switch 식 · 속성 패턴 · 위치 패턴
  • 9.0관계 패턴 · and · or · not
  • 10.0확장 속성 패턴(점으로 이어 적기)
  • 11.0목록 패턴 · 슬라이스 패턴
C# 8.0+

겹쳐 놓을 수 있게 된 것은 8.0의 속성 패턴과 위치 패턴부터이고, 그 뒤로는 같은 것을 짧게 적게 하는 변화가 이어졌습니다. 9.0이 &&·|| 를 패턴 안으로 들여왔고, 10.0이 중괄호 계단을 점으로 폈고, 11.0이 컬렉션을 모양으로 볼 수 있게 했습니다. 문법이 늘어난 만큼 한 줄에 담기는 것도 늘었으니, 어디까지 담을지는 읽는 사람을 보고 정하십시오.

실습 문제

직접 해보기

1. 겹쳐 놓은 패턴으로 줄이기 난이도 하

아래 코드를 패턴 하나로 고쳐보세요. CustomerAddressnull 일 수 있습니다.

static bool IsSeoulBig(Order o) {
    if (o.Customer is null) return false;
    if (o.Customer.Address is null) return false;
    if (o.Customer.Address.City != "서울") return false;
    if (o.Lines is null || o.Lines.Length < 3) return false;

    return true;
}
패턴은 가는 길에 null 을 만나면 맞지 않는 것으로 처리합니다. 앞의 세 줄은 적지 않아도 됩니다. 길이는 Lines.Length: >= 3 으로 적습니다.
static bool IsSeoulBig(Order o) => o is { Customer.Address.City: "서울", Lines.Length: >= 3 }; // null 검사 세 줄이 사라집니다. Customer 가 null 이면 그 자리에서 // 맞지 않는 것이 되고, Address 도 마찬가지입니다. // Lines 가 null 인 경우도 함께 걸립니다. Length 를 보려면 Lines 가 // 있어야 하는데, 없으면 맞지 않는 것으로 끝납니다. // 왜 안 맞았는지를 화면에 알려야 한다면 이렇게 묶지 마세요. // "주소가 없습니다" 와 "품목이 모자랍니다" 를 구분할 수 없습니다.
2. 죽은 가지 찾기 난이도 중

아래 switch빌드되지 않습니다. 무엇 때문인지 설명하고 고쳐보세요. 죽는 것이 몇 줄인지도 함께 적어보세요.

static string Ship(Order o) => o switch {
    { Lines: [..] }                       => "보통 배송",
    { Lines: [] }                         => "보낼 것이 없습니다",
    { Lines: [{ Price: > 100000 }, ..] }  => "고가 배송",
    { Customer.Address.City: "제주" }     => "도서 배송",
    _                                     => "그 밖"
};
첫 줄의 [..] 는 몇 개든 맞습니다. 0개도 포함합니다. 다만 Linesnull 이면 맞지 않습니다. 그래서 아래 넷 가운데 죽는 것과 살아남는 것이 갈립니다.
// [..] 는 "몇 개든" 이라 0개도 맞습니다. 첫 줄이 목록을 가진 주문을 // 모두 가져가, 아래 두 줄이 죽습니다. // { Lines: [] } error CS8510 // { Lines: [{ Price: > 100000 }, ..] } error CS8510 // 제주 줄과 _ 는 죽지 않습니다. [..] 도 Lines 가 null 이면 맞지 않아, // 그런 주문은 아래로 내려오기 때문입니다. // 좁은 것을 위로 올립니다. static string Ship(Order o) => o switch { { Lines: [] } => "보낼 것이 없습니다", { Customer.Address.City: "제주" } => "도서 배송", { Lines: [{ Price: > 100000 }, ..] } => "고가 배송", { Lines: [..] } => "보통 배송", _ => "그 밖" }; // 제주를 고가보다 위에 둔 것은 판단입니다. 제주의 비싼 물건을 // 어느 쪽으로 보낼지가 정해져야 순서가 정해집니다. 겹치는 경우가 // 있으면 순서가 곧 규칙이므로, 그 자리에 주석을 남기세요. // 마지막 _ 는 Lines 가 null 인 경우에 걸립니다. [..] 도 null 은 // 맞지 않기 때문입니다.
요약
  • 패턴은 서로 안에 들어갑니다. 속성 안에 목록을, 그 안에 관계 패턴을 넣습니다.
  • 가는 길에 null 을 만나면 맞지 않는 것입니다. 단계마다 검사할 필요가 없습니다.
  • C# 10부터 점으로 이어 적습니다. { Customer.Address.City: "서울" }
  • record괄호로 분해됩니다. 다만 순서에 기대므로 매개 변수가 늘어날 형식에는 이름을 적으세요.
  • 순서가 규칙입니다. 넓은 것을 위에 두면 아래가 죽고, 그것은 경고가 아니라 오류입니다.
  • 열거형은 항목을 다 적어도 경고가 남습니다. 이름 없는 값이 들어올 수 있기 때문입니다.
  • 깊이 겹치면 어느 단계에서 어긋났는지 알 수 없습니다. 까닭을 알려야 하는 자리에는 if 를 두세요.

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