환영합니다, 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로 훅 들어오니 정신이 더 없는 것 같아요. 더 알아가고 있다고 느낄 때 쯤이면 새로운 기능들이 무섭게 추가되니, 항상 공부하는 자세가 필요할 것 같습니다.
'C# & .NET' 카테고리의 다른 글
[C# 10.0] C#의 편리한 구문들 모음 (Syntax Sugar) (0) | 2022.02.23 |
---|---|
[C# UWP] UWP에서 설정값 저장하고 불러오기 (0) | 2022.02.23 |
[C# UWP] UWP에선 ConfiguraionManager 대신 이걸 사용하세요 (0) | 2022.02.23 |
[C# ML] C#으로 머신러닝 모델 구축하기 (ML.NET) (5) | 2022.02.20 |
[CS1545] GGHS Todo V1.5A, 오류와의 싸움 (0) | 2021.10.17 |