본문 바로가기

진리는어디에/C#

[C#] 패턴 매칭(Pattern Matching)

패턴 매칭이란?

여기서 말하는 "패턴 매칭"은 정규 표현식을 이용한 문자열 매칭이 아니다. 패턴 매칭이란 "임의의 객체가 특정 패턴(모양, 타입, 값)을 만족하는지 조사 하는 것"이다. 예를 들어 "객체 r의 타입은 Rect 타입인가? r은 정사각형인가? r의 좌표는 10인가?" 등을 조사하는 것이다.

패턴 매칭
임의의 객체가 특정 패턴(모양, 타입, 값)을 만족하는지 조사 하는 것

패턴 매칭은 아래와 같은 종류가 있다.

  • type pattern matcing : C# 초기 부터 지원. C# 7.0에 기능 추가
  • var pattern matching : C# 7.0
  • const pattern matching : C# 7.0
  • switch expression : C# 8.0

Type Pattern Matching

  • is 연산자
if( 객체 is 타입 )
{
}
  • C# 7.0에서 추가된 새로운 표기법
if( 객체 is 타입 변수 )
{
}
  • 예제
using System;

class Shape
{
}

class Circle : Shape
{
    public double radious = 100;
}

class Program
{
    public static void Draw(Shape s)
    {
        if (s is Circle)        // if( '객체' is '타입' )
        {
            Circle c1 = (Circle)s;
            Console.WriteLine(c1.radious);
        }

        if (s is Circle c2)     // if( '객체' is '타입' '변수' ), C# 7.0 부터 추가
        {
            Console.WriteLine(c2.radious);
        }
    }
    static void Main(string[] args)
    {
        Draw(new Circle());
    }
}

위 예제는 Draw 함수의 인자로 넘어오는 Shape 타입의 인자가 Circle 타입인지 조사한다. 보통은 타입을 조사하는 것에서 끝나지 않고 객체에 있는 값에 접근하기 위해 타입 캐스팅을 사용하게 된다. 

C# 7.0 부터는 타입 캐스팅을 사용하지 않고 "객체 in 타입 변수" 문법을 이용해 타입 캐스팅 없이 바로 캐스팅 된 변수를 사용 할 수 있다. "변수"는 해당 코드 블록을 벗어나기 전까지 유효하다.

'var' Pattern Matching

    ...
    
    public static void Draw(Shape s)
    {
        if(s is var c1)
        {
        }
    }
    
    ...

명시적인 타입(예제에선 Circle)대신 var를 사용한다고 해서 var pattern matching이라고 한다. 앞의 예제와 같이 명시적인 타입이 지정되어 있다면 true, false를 판별할 수 있겠지만 var는 명확한 타입을 지정할 수 없으므로 항상 true만 나온다. 그렇다면 항상 true만 리턴하는 저 패턴매칭을 어디에 쓰이는 것일까? 뒤에 소개될 switch expression에서 같이 소개 하도록 하겠다. 지금은 이런 패턴 매칭이 있다는 것 정도만 알고 다음으로 넘어가자.

Const Pattern Matching

앞에서는 변수를 타입과 패턴 매칭을 했다. 아래 예제의 const pattern matching은 상수 값과 패턴 매칭을 조사하는 것이다.

class Program
{
    static void Main(string[] args)
    {
        int n = 10;

        if (n is 10)                // const pattern matching
        {
        }
    }
}

5라인에서 변수 n과 상수 10을 is 연산을 통해 비교하고 있다. 그런데 모습이 딱 상등 연산자(==)를 이용하는 것과 다를 것이 하나도 없다. 실제 IL로 컴파일된 코드를 보더라도 완전 동일한 코드다. 그렇다면 왜 is를 사용하는가? 그냥 일반적인 상등 연산자(==)사용하는 것이 더 편리하지 않는가?

만일 정말 그렇다면 is 연산자는 나오지 않았을 것이다. 이제 부터 is 와 == 연산자가 다르게 사용되는 경우를 살펴 보도록하자. 아래와 같이 object 객체를 만들어 10을 저장했다. 하지만 아래 코드는 obj와 10의 타입이 맞지 않다고 컴파일 에러가 발생한다.

object obj = 10;
if (obj == 10) // '==' 연산자는 'object' 및 'int' 형식의 피연산자에 적용할 수 없습니다.
{
    Console.WriteLine("True");
}

그렇다면 둘중에 하나를 타입 캐스팅을 통해 같은 타입으로 만들어 줘야 한다. 10을 object 타입으로 캐스팅 해보자.

if (obj == (object)10)   // 컴파일 성공
{
    Console.WriteLine("True");
}

결과 부터 먼저 말하자면 컴파일은 성공했지만 "True"가 출력 되지는 않는다. 이유는 10을 object 타입으로 캐스팅하게 되면 '박싱'을 통해 힙메모리 영역에 '새로운' 참조 변수가 생성되어 obj와 비교 된다.

참조 변수에 대한 상등연산(==)은 값이 같은 것을 비교하는 것이 아닌 같은 참조인지를 비교하는 것이므로, 위 예에서 obj와 캐스팅 된 참조 변수는 같지 않은 것으로 판단 된다.

True가 출력 되게 하기 위해서는 10을 object로 캐스팅해 박싱을 할 것이 아니라 object를 int형태로 다운 캐스팅해야 한다.

if ((int)obj == 10)
{
    Console.WriteLine("True");
}

참조 변수를 값 변수 타입으로 캐스팅하게 되면 언박싱을 통해 스택 영역에 값 변수가 생성 되고 10과 값 비교를 하게 되어 True가 출력 된다.

어떤 것을 업캐스팅, 다운캐스팅 하느냐에 따라 결과가 달라진다. 뭔가 헷깔리고 복잡하다. 이때 const pattern matching은 박싱과 언박싱을 다 고려하여 is 연산 하나로 대신할 수 있게 해준다.

if (obj is 10)
{
    Console.WriteLine("True");
}
Tip. 캐스팅에서 박싱과 언박싱
데이터는 value(값)형 데이터와 reference(참조)형 데이터로 나뉜다. 값형 데이터는 스택 메모리 영역에, 참조형 데이터는 힙 메모리 영역에 저장되어 사용된다.

박싱
값형 데이터를 참조형 데이터로 변환, 즉 스택 영역의 데이터를 힙 영역으로 옮긴다.
묵시적으로 형변환이 가능하며 데이터 손실은 없다.

int a = 3;
Object b = a ;  or Integer c = a; 

언박싱
참조형 데이터를 값형 데이터로 변환, 즉 힙영역의 데이터를 스택영역으로 옮긴다.
박싱 된 참조형 데이터를 다시 값 형 데이터로 되돌리기 위해 사용된다.

int d = (int)b;

출처: https://lssang.tistory.com/26 [Aiden의 개발노트]

Switch Expression

전통적인 switch 문은 주로 정수를 이용해 1일 때는 뭐하겠다, 2일 때는 뭐하겠다, 아무것도 해당하지 않으면 뭐하겠다, 이런식으로 switch문을 작성된다. 물론 C#은 문자열을 이용한 switch 문도 가능하다.

// 전통적인 switch case
int n = 1;
switch (n)
{
    case 1:
        break;
    case 2:
        break;
    default:
        break;
}

C# 7.0 부터는 switch ~ case 문에 패턴 매칭을 사용 할 수 있다. 

using System;

class Shape 
{
    public int id = 1;
}

class Circle : Shape
{
    public double radious = 100;
}

class Rectangle : Shape
{
    public double width = 100;
    public double height = 100;
}

class Program
{
    static void Main(string[] args)
    {
        Shape s = new Rectangle();
        switch (s)
        {
            case null:                                  // const pattern matching
                break;
            case Circle c:                              // type pattern matchig
                break;
            case Rectangle r when r.height == r.width:  // when 절 사용
                break;
            case Rectangle r:
                break;
            case var v when v.id == 2 :                 // var pattern matching
                break;
            default:
                break;
        }
    }
}
  • 26라인, const pattern matching :
    객체가 null 상수인지 아닌지 조사한다.
  • 28라인, type pattern matching :
    객체가 Circle 타입인지 조사하고, Circle 타입인 경우 변수 c에 캐스팅 된 값을 저장한다.
  • 30라인, type pattern matching 과 when 절 :
    특정 조건을 한정 짓는데 case문만으로 부족할 때 전문 용어로 'case guard'라고 하는 when 조건절을 추가 할 수 있다. case guard는 반드시 boolean을 리턴하는 표현식이어야 한다.
  • 32라인
    큰 의미는 없다. 다만 switch문은 위에서 부터 아래로 차례로 평가 된다. 만일 이 패턴 매칭 조사가 27라인 보다 이전에 평가 된다면 27라인은 영원히 평가되지 않는다. switch 문에서는 일반적인(비교 조건이 적은) 평가식이 아래로 가도록 구성되어야 한다는 것을 보여 주기 위해 추가 되었다.
  • 34라인, var pattern matching :
    앞에서 설명하다만 'var' 패턴 매칭이다. 어떤 타입이든 관계 없이 id가 1인지만 조사한다. 사실 위 케이스에서는 var가 Shape 타입으로 지정되기 때문에 가장 상위 클래스인 'Shape v when v.id...'라고 해도 동일하다. 편의를 위해 var를 사용 할 수도 있다는 것만 알면 된다.

위 예제를 보면 결국 우리가 앞에서 살펴 보았던 if 문을 이용한 패턴 매칭과 똑같다.

마치며

일단 패턴 매칭 관련한 내용은 이정도로 살펴 보도록하고 지면이 길어지는 관계로 C# 8.0에서 새로이 추가된 switch expression은 다음 포스트에 이어 계속 살펴 보도록 하겠다.

부록 1. 같이 읽으면 좋은 글

 

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!