C# & .NET

C# 9.0: switch의 기능, 이젠 이런 것까지?

카루-R 2021. 5. 14. 10:00
반응형

환영합니다, Rolling Ress의 카루입니다.

에...제 주력 언어는 원래 C++입니다. 그런데 요즘에 UWP 앱을 개발하면서 (거의 반 강제적으로) C#을 쓰게 되었는데, 재밌네요. 특히 비주얼 스튜디오에서 제공하는 C#의 강력한 인텔리센스 기능과 자동 완성 등이 너무 편해서 당분간은 C#을 애용하게 될 것 같습니다.

오늘은 제가 특히 편리하다고 생각하는 C#의 문법 중 switch에 대해서 알아보겠습니다. 사실 'switch ~ case' 자체는 다른 언어에서도 종종 나와서 익숙하신 분들이 많을 겁니다. 주어진 식에 따라 분기를 나누는 역할을 하죠.

 

int flag = 3; 

switch (flag) 
{
case 1: DoFunc1(); break;
case 2: DoFunc2(); break;
default: DoFunc3(); break; 
}

 

이 부분은 C / C++ / C# 공통입니다. Go에서도 switch가 있긴 한데.. 형식이 좀 다르므로 패스. 다만 C#의 switch와 C/C++의 가장 큰 차이점이라면 묵시적인 fallthrough가 안 된다는 거겠죠. C#에서 fallthrough 기능을 사용하려면 다음과 같이 goto를 사용해야 합니다.

 

int flag = 3; 

switch (flag) 
{ 
case 1: DoFunc1(); goto case 2; 
case 2: DoFunc2(); goto default; 
default: DoFunc3(); break; 
}

 

C++17에서는 [[fallthrough]]; 로 사용할 수 있었죠. C#에서는 goto + (케이스)로 사용할 수 있습니다. 사실상 goto가 유일하게 용서되는 경우라고 할까요. 일단 여기까지가 기본적인 switch ~ case 문의 사용법이었습니다.

C# 7.0, switch문의 패턴매칭

C# 7.0에서 switch ~ case문이 패턴 매칭 식을 흡수합니다. 원래 switch 문의 조건식에는 값 형식의 식들만 들어갈 수 있었지만, 이제는 그런 제한이 사라졌어요. 클래스 인스턴스도 들어갈 수 있습니다. 그리고 case 문에는 패턴 매칭 식을 넣을 수가 있어요. 예시를 봅시다.

 

using System; 

public class Program 
{ 
    public static void Main() 
    { 
        object data = 5; 
        // object data = "STRING"; 
        // object data = 3.141592; 
        // object data = DateTime.Now; 
        
        switch (data) 
        { 
        case 5: 
            Console.WriteLine($"data is int: {data}"); 
            break;
        case "STRING": 
            Console.WriteLine($"data is string: {data}"); 
            break; 
        case 3.141592: 
            Console.WriteLine($"data is double: {data}"); 
            break; 
        case DateTime: 
            Console.WriteLine($"data is DateTime: {data}"); 
            break; 
        default: 
            Console.WriteLine("No Match"); 
            break; 
        }
    }
}

 

object data 부분을 주석처리 해 두었는데, 하나씩 주석을 풀어보면서 출력값을 봅시다.

 

data is int: 5
data is string: STRING
data is double: 3.141592
data is DateTime: 05/05/2021 11:12:56

 

이렇게 나옵니다. case DateTime 에서 보실 수 있듯이, 이제는 case가 상수식만 받지 않고 타입도 받을 수 있어요. 따라서 다음과 같이 써도 동일하게 작동합니다.

 

case int: // ... 
case string: // ... 
case double: // ... 
case DateTime: // ...

 

이렇게 쓸 경우 data에 타입에 따라 알맞은 case문이 선택되어 실행되게 됩니다. 참고로, 각각에 해당하는 변수가 필요할 경우 여기에서 바로 변수명을 정의할 수 있어요.

 

case int i: // ... 
case string s: // ...
case double f: // ...
case DateTime dt: // ...

 

이렇게 하면 해당 case 문 내부에서 이 변수들을 사용할 수 있게 됩니다.

참고로 제네릭과 비슷하게, 이때도 when을 사용할 수 있습니다.

 

using System; 

public class Program 
{
    public static void Main() 
    { 
        object data = 5; 
        
        switch (data) 
        { 
        case int i when i > 10: // data가 int 타입이고 10보다 큰 경우 
            Console.WriteLine($"data is int: {data} > 10"); 
            break; 
        case int i when i <= 10: // data가 int 타입이고 10 이하인 경우 
            Console.WriteLine($"data is int: {data} <= 10"); 
            break; 
        } 
    } 
}
data is int: 5 <= 10

 

이렇게 자료형과 변수를 넣고 when을 통해 조건식을 추가로 걸어주면, 해당하는 조건식이 참인 경우에 case가 실행되게 됩니다. 다중 if문이 보기 싫을 때 switch로 깔끔하게 정리해버릴 수가 있죠.

자, 그리고 여기서 제가 굉장히 높이 평가하는 기능이 등장합니다.

 

object val1 = 5; 
object val2 = "6"; 

switch (val1) 
{ 
case var _ when val1.ToString() + val2 == "56": 
    Console.WriteLine("first case"); 
    break;
case var _ when val1 as int? == 6 && val2 as string == "6": 
    Console.WriteLine("second case"); 
    break; 
}
first case

 

case 변수는 var _로 생략해버리고, 아예 when 을 통해 조건식으로만 case를 실행할 수 있게 만드는 거죠. 참고로 var _ 가 C# 9.0 부터인가...? 혹시라도 오류가 생긴다면 var 뒤에 변수 이름을 명시적으로 선언해주세요. 아마 7.x 버전대에서는 변수 이름 생략이 안 될 겁니다. 아무튼, switch문의 기능이 확 늘어난 느낌이라 굉장히 마음이 편안해지네요. 물론 그로 인한 부작용은 우리가 공부해야 할 양이 늘었다는 것. 여기까지는 뭐...그래요. 그럴 수 있어요. 근데 조금만 더 나가다보면 머리가 아파집니다.

 

C# 8.0, switch expression

함수를 하나 정의해봅시다. 요일에 맞춰서 해당 요일을 string으로 반환해보죠.

 

using System; 

public class Program 
{ 
    public static void Main() 
    { 
        Console.WriteLine(GetDay()); 
    } 
    
    public static string GetDay() 
    { 
        switch (DateTime.Now.DayOfWeek) 
        {
        case DayOfWeek.Monday: 
            return "Monday"; 
        case DayOfWeek.Tuesday: 
            return "Tuesday"; 
        case DayOfWeek.Wednesday: 
            return "Wednesday"; 
        case DayOfWeek.Thursday: 
            return "Thursday"; 
        case DayOfWeek.Friday: 
            return "Friday"; 
        case DayOfWeek.Saturday: 
        case DayOfWeek.Sunday: 
            return "Weekend"; 
        } 
    } 
}

 

포스팅 작성일이 수요일이라 저는 Wednesday가 출력되었습니다. 그런데, 지금 보면 return이 계속 반복되고 있는 걸 볼 수 있죠. 이거 좀..보기 지저분하지 않습니까? 그리고 어차피 DateTime.Now.DayOfWeek에 따라서 case가 결정되는 걸 아는데, 조금 복잡하게 느껴지기도 하고요.

그래서 등장한 게 switch expression입니다. switch를 식처럼 사용할 수 있어요. 다만 사용법이 조금 독특합니다. GetDay() 메서드만 봅시다.

 

public static string GetDay() 
{ 
    return DateTime.Now.DayOfWeek switch 
    { 
        DayOfWeek.Monday => "Monday", 
        DayOfWeek.Tuesday => "Tuesday", 
        DayOfWeek.Wednesday => "Wednesday", 
        DayOfWeek.Thursday => "Thursday", 
        DayOfWeek.Friday => "Friday", 
        DayOfWeek.Saturday or DayOfWeek.Sunday =>"Weekend", 
    }; 
}

 

return은 그냥 return문이고, 중요한 건 DateTime.Now.DayOfWeek switch입니다. switch를 제어할 변수가 switch 앞에 오고, switch 뒤에는 소괄호 없이 바로 블럭이 옵니다. 그리고 case문이 사라지고 (변수) => (값) 형태로 바뀐 것을 볼 수 있죠. 맨 아래에는 Saturday와 Sunday를 or로 묶은 점 참고하시고요.

자, 그런데 사실 Saturday와 Sunday는 어차피 '나머지' 개념이니까 default로 처리해도 되겠죠? 그래서 _ 을 입력하면 정의하지 않은 모든 값들을 처리할 때 사용할 수 있습니다.

 

public static string GetDay() => DateTime.Now.DayOfWeek switch 
{ 
    DayOfWeek.Monday => "Monday", 
    DayOfWeek.Tuesday => "Tuesday", 
    DayOfWeek.Wednesday => "Wednesday", 
    DayOfWeek.Thursday => "Thursday",
    DayOfWeek.Friday => "Friday",
    _ =>"Weekend", 
};

 

조금 더 간결하게 하면 이렇게 됩니다. 당황하지 마세요. 그냥 람다 식으로 쓴 것 뿐입니다. switch 자체가 하나의 식으로 인정되기 때문에 그냥 저렇게 =>를 통해서 간단하게 값을 반환할 수 있어요.

 

using System; 

public class Program 
{ 
    public static void Main() 
    { 
        Console.WriteLine(Greet(13)); 
    } 
    
    public static string Greet(int hour) => hour switch 
    { 
        <= 12 => "Good morning", 
        >= 13 and <= 18 => "Good evening", 
        > 18 and <= 24 => "Good night" 
    }; 
}
Good evening

 

이번에는 시간에 따라서 인사를 하는 프로그램을 만들어봤습니다. 현재 시각을 13시로 넘겨줬어요. 그랬더니 Good evening이 출력되었습니다.

 

<= 12 => "Good morning",
>= 13 and <= 18 => "Good evening",
> 18 and <= 24 => "Good night"

 

 

이게 <= => 이런 이상한 기호들이 나와서 '뭐지?' 싶으신 분들도 계실 겁니다. 저게 구분은 '=>'이 기준이니, 아래처럼 보면 이해하기 편하실 겁니다.

 

(<= 12) => "Good morning",
(>= 13 and <= 18) => "Good evening",
(> 18 and <= 24) => "Good night"

 

네, 그러니까 이 코드는 사실상 아래와 같은 느낌으로 해석할 수 있는 거죠.

 

(hour <= 12) => "Good morning",
(hour >= 13 and hour <= 18) => "Good evening",
(hour > 18 and hour <= 24) => "Good night"

 

물론 이 코드가 실제로 컴파일이 되는 건 아닙니다. hour가 이렇게 생략되어 있다는 것만 말씀드리고 싶네요. MS에서는 이걸 관계 패턴 매칭(Relational Pattern Matching)이라고 부르는 것 같습니다.

 

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

 

참고로, 여기서 더 어렵게 나갈 수도 있습니다. 아마 record 타입이 C# 9에서 새로 추가된 걸로 아는데, Tuple과 겉보기에 크게 다를 건 없습니다. record에 대해서는 따로 설명해드리기로 하고, 일단은 MS 예시를 보시죠.

 

public record Order(int Items, decimal Cost);

 

record를 하나 정의합니다. 이걸 클래스로 나타내면 아래와 같은 모습이겠죠.

 

public class Order 
{
    public Order(int items, decimal cost) 
    {
        (Items, Cost) = (items, cost) 
    }
    
    public int Items { get; set; } 
    public decimal Cost { get; set; } 
}

 

물론 record에서는 기본으로 set 이 아니라 init을 이용하지만, 이런 느낌이라고 보시면 됩니다. 아무튼, 여기에 Order 타입을 받는 아래와 같은 메서드가 있습니다.

 

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

 

첫째와 둘째 줄은 각각의 파라미터가 해당 조건을 만족할 때 실행되고, 셋째 줄은 Items에 관계 없이 Cost만 해당 식을 만족하면 실행됩니다. null일 경우엔 예외가 생기고, null이 아닌 다른 모든 값은 마지막 var o 부분이 실행되는 셈이죠. _ (default)랑 같다고 봐도 됩니다.

자, 이렇게 switch문의 확장된 기능에 대해서 알아보았습니다. C# 7.1까지만 해도 뭐 그러려니 했었는데 8.0에서 스위치 식으로 훅, 9.0에서 record로 훅 들어오니 정신이 더 없는 것 같아요. 더 알아가고 있다고 느낄 때 쯤이면 새로운 기능들이 무섭게 추가되니, 항상 공부하는 자세가 필요할 것 같습니다.

 

반응형