패턴 매칭(Pattern Matching) 은 C# 7.0 에서 부터 도입된 기능으로 어떤 식이 특정 패턴(형태)와 일치하는지를 검사한다.
패턴 매칭을 이용하면 장황하고 거추장스러운 분기문을 간결하고 읽기 쉬운 코드로 대체할 수 있다.
패턴 매칭은 식을 입력받고 일치 여부를 반환한다.

식 이란, 코드에서 단일 결과값을 만들어 낼 수 있는 연산자와 연산자 조합을 말한다.
예를 들어 1 + 2 는 단일 결과값 3 을 만들어내는 식이다.
a = 123; // 123과 a = 123은 식
c = typeof(int); // c와 c = typeof(int)는 식
d = a + 456; // a + 456과 d = a + 456은 식
주어진 시기 특정 형식 int, string, … 과 일치하는지를 평가한다.
만약 주어진 식과 형식이 일치한다면, 선언 패턴 (Declaration Pattern)은 식을 해당 형식으로 변환한다.
is 연산자는 왼쪽에 있는 식이 오른쪽에 있는 패턴과 일치하는지를 테스트한다.
using System;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
object foo = 23;
// foo가 int인 경우 foo를 int 형식으로 변환하여 bar에 할당함.
if (foo is int bar)
{
Console.WriteLine(bar);
}
}
}
}
23
형식 패턴(Type Pattern)은 선언 패턴과 거의 같은 방식으로 동작하지만, 변수 생성 없이 형식 일치 여부만 테스트 한다. C# 9.0 에서 더 간략한 형식 패턴 매칭을 지원하기 위해 도입됐다.
using System;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
object foo = 23;
if (foo is int)
{
Console.WriteLine(foo);
}
}
}
}
23
상수 패턴(Constant Pattern) 은 식이 특정 상수와 일치하는지를 검사한다.
정수 리터럴과 문자열 리터럴뿐 아니라 null 과 enum 등 모든 상수와 매칭할 수 있다.
using System;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
var GetCountryCode = (string nation) => nation switch
{
"KR" => 82,
"US" => 1,
"UK" => 44,
_ => throw new ArgumentException("Not Supported Code")
};
Console.WriteLine(GetCountryCode("KR"));
Console.WriteLine(GetCountryCode("US"));
Console.WriteLine(GetCountryCode("UK"));
}
}
}
82
1
44
프로퍼티 패턴(Property Pattern) 매칭은 식의 속성이나 필드가 패턴과 일치하는지를 검사한다.
입력된 식이 int, double 같은 기본 데이터 형식이 아닌 경우 유용하게 사용할 수 있다.
using System;
namespace PatternMatching
{
class MainApp
{
class Car
{
public string Model { get; set; }
public DateTime ProducedAt { get; set; }
}
static string GetNickname(Car car)
{
var GenerageMessage = (Car car, string nickname) =>
$"{car.Model} produced in {car.ProducedAt.Year} is {nickname}";
if (car is Car { Model: "Mustang", ProducedAt.Year: 1967 })
return GenerageMessage(car, "Fasback");
else if (car is Car { Model: "Mustang", ProducedAt.Year: 1976 })
return GenerageMessage(car, "Cobra II");
else
return GenerageMessage(car, "Unknown");
}
static void Main(string[] args)
{
Console.WriteLine(GetNickname(new Car() { Model = "Mustang", ProducedAt = new DateTime(1967, 11, 23)}));
Console.WriteLine(GetNickname(new Car() { Model = "Mustang", ProducedAt = new DateTime(1976, 6, 7)}));
Console.WriteLine(GetNickname(new Car() { Model = "Mustang", ProducedAt = new DateTime(2099, 12, 25)}));
}
}
}
Mustang produced in 1967 is Fasback
Mustang produced in 1976 is Cobra II
Mustang produced in 2099 is Unknown
관계 패턴(Relational Pattern) 매칭은 관계 연산자를 이용하여 입력받은 식을 상수와 비교한다.
using System;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
static bool IsPassed(double score) => score switch
{
< 60 => false,
_ => true,
};
if (IsPassed(60.1))
{
Console.WriteLine("Passed");
}
}
}
}
Passed
패턴과 패턴을 패턴 논리 연산자 and, or, not 을 조합해 하나의 논리 패턴(Logical Pattern) 으로
만들 수 있다.
using System;
namespace PatternMatching
{
class MainApp
{
class OrderItem
{
public int Amount { get; set; }
public int Price { get; set; }
}
static double GetPrice(OrderItem orderItem) => orderItem switch
{
OrderItem { Amount: 0 } or OrderItem { Price: 0 } => 0.0,
OrderItem { Amount: >= 100 } and OrderItem { Price: >= 10000 } => orderItem.Amount * orderItem.Price * 0.8,
not OrderItem { Amount: < 100 } => orderItem.Amount * orderItem.Price * 0.9,
_ => orderItem.Amount * orderItem.Price
};
static void Main(string[] args)
{
Console.WriteLine(GetPrice(new OrderItem() { Amount = 0, Price = 10000 }));
Console.WriteLine(GetPrice(new OrderItem() { Amount = 100, Price = 10000 }));
Console.WriteLine(GetPrice(new OrderItem() { Amount = 100, Price = 9000 }));
Console.WriteLine(GetPrice(new OrderItem() { Amount = 1, Price = 1000 }));
}
}
}
0
800000
810000
1000
괄호 패턴(Parenthesized Pattern)은 소괄호 () 로 패턴을 감싼다.
보통 논리 패턴으로 여러 패턴을 조합한 뒤 이를 새로운 패턴으로 만드는 경우 사용한다.
using System;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
object age = 30;
if (age is (int and > 19))
{
Console.WriteLine("Major");
}
}
}
}
Major
위치 패턴(Positional Pattern) 은 식의 결과를 분해하고, 분해된 값들이 내장된 복수의 패턴과 일치하는지를 검사한다. 위치 패턴 안에 내장되는 패턴에는 어떠한 패턴이든 올 수 있다.
단, 분해된 값들과 내장된 패턴의 개수, 순서가 일치해야 한다는 점에는 주의해야 한다.
아래의 예제에서 itemPrice 는 string, int 요소로 이루어진 튜플이며,
이 튜플을 상수 패턴 "espresso"와 관계 패턴 < 5000 으로 이루어진 위치 패턴으로 매칭하고 있다.
using System;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
Tuple<string, int> itemPrice = new Tuple<string, int>("espresso", 3000);
if (itemPrice is ("espresso", < 5000))
{
Console.WriteLine("The coffee is affordable.");
}
}
}
}
The coffee is affordable.
var 패턴 (Var Pattern) 은 null을 포함한 모든 식의 패턴 매칭을 성공시키고,
그 식의 결과를 변수에 할당한다.
다음 예제와 같이 어떤 식의 결과를 임시 변수에 할당한 뒤 추가적인 연산을 수행하고자 할 때 유용하게 사용할 수 있다.
using System;
using static System.Formats.Asn1.AsnWriter;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
// 모든 과목이 60점이 넘고, 평균이 60점 이상인 경우에만 Pass
var IsPassed = (int[] scores) =>
scores.Sum() / scores.Length is var average &&
Array.TrueForAll(scores, (score) => score >= 60) &&
average >= 60;
int[] scores1 = { 90, 80, 60, 80, 70 };
Console.WriteLine($"[{string.Join(",", scores1)}] Pass : {IsPassed(scores1)}");
int[] scores2 = { 90, 80, 59, 80, 70 };
Console.WriteLine($"[{string.Join(",", scores2)}] Pass : {IsPassed(scores2)}");
}
}
}
[90,80,60,80,70] Pass : True
[90,80,59,80,70] Pass : False
무시 패턴(Discard Pattern) 도 var 패턴 처럼 모든 식과의 패턴 일치 검사를 성공시킨다.
그러나 var 패턴과는 다르게 is 식에서는 사용할 수 없고, switch 식에서만 사용할 수 있다.
모든 식을 매칭할 수 있기 때문에 switch 문의 default 케이스와 비슷한 용도로 사용하면 되며,
무시 패턴은 _ 기호를 이용한다.
using System;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
var GetCountryCode = (string nation) => nation switch
{
"KR" => 82,
"US" => 1,
"UK" => 44,
_ => throw new ArgumentException("Not Supported Code") // 무시 패턴 매칭
};
Console.WriteLine(GetCountryCode("KR"));
Console.WriteLine(GetCountryCode("US"));
Console.WriteLine(GetCountryCode("UK"));
}
}
}
82
1
44
목록 패턴(List Pattern) 은 배열이나 리스트가 패턴의 시퀀스와 일치하는지를 검사한다.
패턴의 시퀀스는 대괄호 [] 사이에 패턴의 목록을 입력해 만든다.
목록 패턴 파일이나 데이터베이스에서 레코드를 읽어 처리하는 것과 같은 다량의 데이터를 처리할 때 유용하다.
using System;
using System.Net.Mime;
namespace PatternMatching
{
class MainApp
{
static void Main(string[] args)
{
var GetStatistics = (List<object[]> records) =>
{
var statistics = new Dictionary<string, int>();
foreach (var record in records)
{
var (contentType, contentViews) = record switch
{
[_, "COMEDY", .., var views] => ("COMEDY", views),
[_, "SF", .., var views] => ("SF", views),
[_, "ACTION", .., var views] => ("ACTION", views),
[_, .., var amount] => ("ETC", amount),
_ => ("ETC", 0)
};
if (statistics.ContainsKey(contentType))
statistics[contentType] += (int)contentViews;
else
statistics.Add(contentType, (int)contentViews);
}
return statistics;
};
List<object[]> MovieRecords = new List<object[]>()
{
new object[] { 0, "COMEDY", "SPY", 2015, 10000},
new object[] { 1, "COMEDY", "Scary Movie", 20000},
new object[] { 2, "SF", "Avengers", 100000},
new object[] { 3, "COMEDY", "극한직업", 25000},
new object[] { 4, "SF", "Star Wars", 200000},
new object[] { 5, "ACTION", "Fast & Furious", 80000},
new object[] { 6, "DRAMA", "Notting Hill", 1000},
};
var statistics = GetStatistics(MovieRecords);
foreach (var s in statistics)
Console.WriteLine($"{s.Key}: {s.Value}");
}
}
}
COMEDY: 55000
SF: 300000
ACTION: 80000
ETC: 1000