패턴 일치 개요

‘패턴 일치’는 식에 특정 특징이 있는지 확인하기 위해 테스트하는 기법입니다. C# 패턴 일치는 식을 테스트하고 식이 일치하는 경우 작업을 수행하기 위한 보다 간결한 구문을 제공합니다. "is expression"은 패턴 일치를 지원하여 식을 테스트하고 해당 식의 결과에 새 변수를 조건부로 선언합니다. “switch 식”을 사용하면 식의 처음 일치 패턴을 기준으로 작업을 수행할 수 있습니다. 이 두 식은 다양한 ‘패턴’ 어휘를 지원합니다.

이 문서에서는 패턴 일치를 사용할 수 있는 시나리오를 살펴봅니다. 이러한 기법은 코드의 가독성과 정확성을 향상하는 데 사용할 수 있습니다. 적용할 수 있는 모든 패턴에 대해 알아보려면 언어 참조에서 패턴에 관한 문서를 살펴보세요.

null 검사

패턴 일치에서 가장 널리 사용되는 시나리오는 값이 null이 아닌지 확인하는 것입니다. 다음 예제를 사용하여 null이 아닌지를 테스트할 때 null 허용 값 형식을 테스트하고 기본 형식으로 변환할 수 있습니다.

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

위 코드는 변수의 형식을 테스트하고 여기에 새 값을 할당하는 ‘선언 패턴’입니다. 이 언어의 규칙 덕분에 다른 언어보다 이 기법을 더 안전하게 사용할 수 있습니다. 변수 number는 액세스만 가능하며 if 절의 실제 부분에 할당됩니다. else 절에서나 if 블록 뒤에서 등 다른 곳에서 액세스하려고 하면 컴파일러가 오류를 발생시킵니다. 이 패턴은 == 연산자를 사용하지 않으므로 형식이 == 연산자를 오버로드하는 경우에도 작동합니다. 따라서 이것은 not 패턴을 추가하여 null 참조를 확인하는 이상적인 방법이 됩니다.

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

앞에 나온 예제에서는 ‘상수 패턴’을 사용하여 변수를 null에 비교했습니다. not은 부정된 패턴이 일치하지 않는 경우 일치하는 ‘논리 패턴’입니다.

형식 테스트

패턴 일치를 위해 사용되는 또 다른 일반적인 방법은 변수가 지정된 형식과 일치하는지 테스트하는 것입니다. 예를 들어, 다음 코드는 변수가 null이 아닌 값인지 테스트하며 System.Collections.Generic.IList<T> 인터페이스를 구현합니다. 이 경우 해당 목록의 ICollection<T>.Count 속성을 사용하여 중간 인덱스 찾기를 수행합니다. 선언 패턴은 변수의 컴파일 시간 형식과 관계없이 null 값과 일치하지 않습니다. 아래 코드는 IList을 구현하지 않는 형식으로부터 보호하는 것 외에도 null로부터 보호합니다.

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

switch 식에 동일한 테스트를 적용하여 변수를 여러 형식에 대해 테스트할 수 있습니다. 이 정보는 특정 런타임 형식을 기반으로 더 나은 알고리즘을 만드는 데 사용할 수 있습니다.

불연속 값 비교

변수를 테스트하여 특정 값에 대한 일치 항목을 찾을 수도 있습니다. 다음 코드는 열거형에서 선언된 모든 가능한 값에 대해 값을 테스트하는 예를 보여 줍니다.

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

위의 예제에서는 열거형의 값을 기준으로 디스패치된 메서드를 보여 주었습니다. 마지막 _ 케이스는 모든 값과 일치하는 ‘무시 패턴’입니다. 이 케이스는 값이 정의된 enum 값 중 어느 것과도 일치하지 않는 경우의 오류 조건을 처리합니다. 해당 스위치 암을 생략하면 컴파일러는 패턴 식이 가능한 모든 입력 값을 처리하지 않는다고 경고합니다. 런타임에는 검사 대상 개체가 switch 암(arm) 중 어느 것과도 일치하지 않는 경우 switch 식이 예외를 throw합니다. 일련의 열거형 값 대신 숫자형 상수를 사용할 수 있습니다. 명령을 나타내는 상수 문자열에 대해서도 이와 비슷한 기법을 사용할 수 있습니다.

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

위의 예제에서는 동일한 알고리즘을 보여 주지만 열거형 대신 문자열 값을 사용합니다. 이 시나리오는 애플리케이션이 일반적인 데이터 형식이 아닌 텍스트 명령에 응답하는 경우에 사용할 수 있습니다. C# 11부터 다음 샘플과 같이 Span<char> 또는 ReadOnlySpan<char>을(를) 사용하여 상수 문자열 값을 테스트할 수도 있습니다.

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

위의 모든 예제에서 ‘무시 패턴’으로 모든 입력이 처리되었는지 확인할 수 있습니다. 컴파일러는 모든 가능한 입력값이 처리되었는지 확인함으로써 이 작업을 도와줍니다.

관계형 패턴

‘관계형 패턴’을 사용하여 값이 상수와 비교되는 방식을 테스트할 수 있습니다. 예를 들어, 다음 코드는 화씨 온도를 기준으로 물의 상태를 반환합니다.

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

위 코드는 두 관계형 패턴이 모두 일치하는지 확인하는 결합 and논리 패턴도 보여 줍니다. 분리 or 패턴을 사용하여 둘 중 어느 패턴이 일치하는지 확인할 수도 있습니다. 두 관계형 패턴은 괄호로 묶여 있습니다. 어떤 패턴도 명확성을 위해 괄호로 묶을 수 있습니다. 마지막 두 개의 switch 암(arm)은 녹는점 케이스와 끓는점 케이스를 처리합니다. 이 두 암(arm)이 없으면 모든 가능한 입력이 처리되지 않았다는 경고가 발생합니다.

앞의 코드는 또한 컴파일러가 패턴 일치 식에 대해 제공하는 또 다른 중요한 기능을 보여 줍니다. 모든 입력 값을 처리하지 않으면 컴파일러가 경고합니다. 또한 스위치 암에 대한 패턴이 이전 패턴에서 적용되는 경우에도 컴파일러에서 경고를 발생합니다. 이를 통해 스위치 식을 자유롭게 리팩터링하고 순서를 조정할 수 있습니다. 동일한 식을 작성하는 또 다른 방법은 다음과 같습니다.

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

이전 샘플의 주요 단원과 다른 리팩터링 또는 다시 정렬은 컴파일러가 코드가 가능한 모든 입력을 처리하는지 유효성을 검사한다는 것입니다.

여러 입력

지금까지 다룬 모든 패턴은 하나의 입력을 검사. 개체의 여러 속성을 검사하는 패턴을 작성할 수도 있습니다. 다음 Order 레코드를 살펴보겠습니다.

public record Order(int Items, decimal Cost);

위의 위치 지정 레코드 형식은 명시적 위치에 두 개의 멤버를 선언합니다. 먼저 Items가 표시되고 그다음에 주문의 Cost가 표시됩니다. 자세한 내용은 레코드를 참조하세요.

다음 코드는 품목의 개수와 주문의 금액을 검사하여 할인 가격을 계산합니다.

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

처음 두 개의 암(arm)은 Order의 두 속성을 검사합니다. 세 번째 암(arm)은 비용만 검사합니다. 그다음 암(arm)은 null을 검사하고 마지막 암(arm)은 다른 모든 값의 일치 여부를 확인합니다. Order 형식이 적합한 Deconstruct 메서드를 정의하는 경우, 패턴에서 속성 이름을 생략하고 분해를 사용하여 속성을 검사할 수 있습니다.

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

위의 코드는 속성이 식에 대해 분해되는 ‘위치 지정 패턴’을 보여 줍니다.

목록 패턴

목록 패턴을 사용하여 목록 또는 배열의 요소를 확인할 수 있습니다. 목록 패턴은 시퀀스의 모든 요소에 패턴을 적용하는 방법을 제공합니다. 또한 무시 항목 패턴(_)을 적용하여 모든 요소와 일치시키거나 슬라이스 패턴을 적용하여 0개 이상의 요소와 일치시킬 수 있습니다.

목록 패턴은 데이터가 일반 구조를 따르지 않는 경우 유용한 도구입니다. 패턴 일치를 사용하여 개체 집합으로 변환하는 대신 데이터의 셰이프와 값을 테스트할 수 있습니다.

은행 거래가 포함된 텍스트 파일에서 발췌한 다음을 고려합니다.

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

CSV 형식이지만 일부 행에는 다른 행보다 더 많은 열이 있습니다. 처리의 경우 더 나쁜 경우 형식의 WITHDRAWAL 한 열에 사용자가 생성한 텍스트가 포함되며 텍스트에 쉼표가 포함될 수 있습니다. 값 프로세스 데이터를 이 형식으로 캡처하기 위한 dis카드 패턴, 상수 패턴 및 var 패턴을 포함하는 목록 패턴입니다.

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

앞의 예제에서는 각 요소가 행의 한 필드인 문자열 배열을 가져옵니다. 두 번째 필드의 switch 식 키로, 트랜잭션의 종류와 나머지 열 수를 결정합니다. 각 행은 데이터가 올바른 형식인지 확인합니다. 취소 패턴(_)은 트랜잭션 날짜와 함께 첫 번째 필드를 건너뜁니다. 두 번째 필드는 트랜잭션 형식과 일치합니다. 나머지 요소 일치 항목은 크기가 있는 필드로 건너뜁니다. 마지막 일치 항목은 var 패턴을 사용하여 양의 문자열 표현을 캡처합니다. 식은 잔액을 추가하거나 빼는 양을 계산합니다.

목록 패턴을 사용하면 데이터 요소 시퀀스의 모양과 일치시킬 수 있습니다. 요소의 위치와 일치하도록 무시 항목조각 패턴을 사용합니다. 다른 패턴을 사용하여 개별 요소에 대한 특성을 일치시킬 수 있습니다.

이 문서에서는 C#의 패턴 일치를 사용하여 작성할 수 있는 여러 종류의 코드를 살펴보았습니다. 다음 문서에서는 시나리오에서 패턴을 사용하는 더 많은 예제와 사용할 수 있는 패턴의 전체 어휘를 보여 줍니다.

참고 항목