as와 is연산자 그리고 패턴매칭
as와 is연산자
오늘 강의를 듣다가 as 연산자에 대해서 처음 접했다. C#의 연산자인데, 아무래도 학교에서 따로 배우지도 않았다보니 여태껏 그 존재도 모르고 명시적 캐스팅만 사용해왔다.
살짝 알아보니, 꽤 유용할 듯 싶어 조사한 내용들을 바탕으로 여기에 정리하고자 한다.
명시적 캐스팅 (Explicit Casting)
학교에서 배웠던 기초적인 타입 형변환 방법이다.
이는 런타임에 타입의 호환성을 검사하고, 시도한다. 캐스팅에 실패하면 InvalidCastException
예외가 발생한다.
장점
- 명확한 타입 확인 : 캐스팅이 성공하면 명시적으로 타입이 일치함을 알 수 있다.
- 값 타입과 참조 타입 모두 사용 가능 : 모든 타입에 범용적으로 사용할 수 있다.
단점
- 예외 처리 필요 : 캐스팅에 실패하면,
InvalidCastException
예외가 발생한다. 이는 예외 처리 구문을 필요로 한다. - 성능 저하 : 예외 처리가 포함된 코드는 성능에 부정적인 영향을 줄 수 있다.
as 연산자
as
연산자도 타입 형변환 방법에 해당한다. 캐스팅에 실패하면 null
을 반환한다.
일반적으로 캐스팅이 실패할 가능성으로 인해 명시적 캐스팅에는 주의가 필요하고, 비교적 안전한 캐스팅 방법이기에 Unity 게임 개발에서는 as
연산자를 사용하는 것을 대부분 권장한다.
장점
- 안전성 : 캐스팅에 실패해도 예외가 발생하지 않고, 대신
null
을 반환하기 때문에 프로그램의 안전성을 유지할 수 있다. - 간결성 : 명시적인 예외처리를 필요로 하지 않기 때문에, 코드가 더 간결해진다.
단점
- 한정된 사용 범위 : 값 타입에는 사용할 수 없고, 참조 타입에만 사용할 수 있다.
- 오류 발견의 어려움 : 캐스팅 실패시
null
을 반환하기 때문에, 실수로 인한 오류를 발견하기 어려울 수 있다.
두 방법의 정리
그래서 두 방법의 장단점을 표로 비교해보자면 다음과 같다.
캐스팅 방식 | 장점 | 단점 |
명시적 캐스팅 (Explicit Casting) | -캐스팅을 성공하면 타입이 확실해짐 -값 타입과 참조 타입 모두에 사용 가능 | -캐스팅 실패시 InvalidCastException 예외 발생 - 예외 처리 구문을 필요로 하며, 성능 저하를 일으킬 수 있음. |
as 연산자 | -캐스팅 실패시 예외를 발생시키지 않고 null 을 반환하여 안전 -코드가 간결하고 예외처리가 필요없어 직관적 | -값 타입에는 사용할 수 없음 -캐스팅 실패시 null 을 반환해 오류 발견이 어려울 수 있음 |
is 연산자
is
연산자는 객체의 타입을 확인하는데 사용한다. 객체가 해당 타입에 부합하면 true를, 그렇지 않으면 false를 반환한다. 이를 기반으로 로직의 조건부 분기와 같은 곳에 활용될 수 있겠다.
위에서 as
연산자의 단점으로 캐스팅 실패시 오류 발견이 어려울 수 있다고 했는데, is
연산자가 이를 보완해줄 수 있다.
is
연산자는 이 다음에 설명한 **패턴 매칭**에서 유용하게 사용된다.
패턴 매칭
패턴 매칭은 데이터의 형태나 값에 따라 코드의 실행 경로를 결정하는 기법을 말한다.
패턴 매칭에는 몇가지 종류가 있는데, 그 종류는 다음과 같다.
- 타입 패턴 매칭
- 스위치 패턴 매칭
- 프로퍼티 패턴 매칭
- 튜플 패턴 매칭
타입 패턴 매칭 (Type Pattern Matching)
C# 7.0부터 사용할 수 있는 기능으로,
특정 객체가 지정된 타입인지 검사하고 해당 타입으로 바로 캐스트할 수 있도록 하는 기능.
기본적인 사용법은 다음과 같다.
1
2
3
4
if(expression is Type variableName)
{
// 여기서 variableName 사용 가능
}
여기서 expression
은 검사하려는 객체, Type
은 검사하려는 타입, variableName
은 expression
이 Type
으로 확인 될 경우 그 값을 가지게 될 변수이다.
1
2
3
4
5
6
object obj = "Type Pattern Matching"
if(obj is string str)
{
Debug.Log(str);
}
예를 들자면 이런 방식으로 바로 조건을 검사하면서 지역 변수 선언이 가능하기 때문에, 작성해야 할 코드가 훨씬 줄어 간결하고 높은 가독성의 코드가 완성된다.
스위치 패턴 매칭 (Switch Pattern Matching)
스위치 패턴 매칭은 C# 7.0에서 처음 도입되었고, C# 8.0에서 더 확장되었다.
switch
문 내에서 복잡한 조건 로직을 간결하게 해준다.
기본적인 사용법은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
switch (expression)
{
case TypePattern varName when condition:
// 조건이 일치할 때 실행할 코드
break;
case AnotherTypePattern varName when anotherCondition:
// 또 다른 조건이 일치할 때 실행할 코드
break;
...
default:
// 어떤 패턴에도 일치하지 않을 때 실행할 코드
break;
}
C# 8.0에서 switch
문법을 더 간결하게 표현할 수 있는 방법을 도입했는데, 이는 위 방법을 좀 더 간단하게 해준다.
위의 경우를 switch문
이라고 한다면, 다음의 방식은 switch식
이라고 부른다.
1
2
3
4
5
6
7
var result = expression switch
{
TypePattern varName when condition => result1,
AnotherTypePattern varName when anotherCondition => result2,
...
_ => defaultResult
};
default 키워드는 _
로 대체하여 사용한다.
이해를 돕기 위해 이를 사용하는 한가지 상황을 예로 들어보자.
먼저, 다음과 같은 클래스들로 다양한 적들을 정의한다면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class Enemy : MonoBehaviour
{
public abstract void Attack();
}
class Zombie : Enemy
{
public override void Attack() => Debug.Log("Zombie attacks with hands!");
}
class Vampire : Enemy
{
public override void Attack() => Debug.Log("Vampire bites!");
}
class Ghost : Enemy
{
public override void Attack() => Debug.Log("Ghost haunts!");
}
이러한 적들을 처리하는 메소드는 다음과 같이 구현할 수 있겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void HandleEnemyAttack(Enemy enemy)
{
switch (enemy)
{
case Zombie zombie:
zombie.Attack();
// 추가적인 Zombie 전용 로직
Debug.Log("Run away from Zombie!");
break;
case Vampire vampire when DateTime.Now.Hour < 6 || DateTime.Now.Hour > 18:
// 밤에만 공격하는 Vampire
vampire.Attack();
Debug.Log("Find garlic!");
break;
case Ghost ghost:
ghost.Attack();
// 추가적인 Ghost 전용 로직
Debug.Log("Use ghost detector!");
break;
default:
Debug.Log("Unknown enemy. Stay alert!");
break;
}
}
switch
문 내에서 패턴 매칭으로 적의 타입을 확인하고, 각 타입에 맞는 공격 메소드를 호출한다.
when
키워드를 활용해 추가적인 조건을 검사할 수 있다.
switch식
의 경우에 대해서도 예제를 써 보자면,
1
2
3
4
5
6
7
8
9
string HandleItem(Item item) => item switch
{
{ Type: ItemType.Weapon } => "Found a weapon!",
{ Type: ItemType.Potion, Value: var value } when value > 50 => "Found a strong potion!",
{ Type: ItemType.Potion } => "Found a potion.",
{ Type: ItemType.Coin, Value: var value } when value >= 100 => "Found a gold coin!",
{ Type: ItemType.Coin } => "Found a coin.",
_ => "Unknown item."
};
이렇게 아이템의 타입과 수치에 기반한 분기를 간략하게 나타낼 수 있겠다.
프로퍼티 패턴 매칭 (Property Pattern Matching)
C# 8.0에서 처음 도입된 프로퍼티 패턴 매칭은 객체의 프로퍼티나 필드를 기준으로 복잡한 로직을 간결하고, 직관적으로 작성할 수 있게 해준다.
기본적인 사용법은 다음과 같다.
1
instance is { property1: value1, property2: value2, ...}
instance
라는 인스턴스에 대해 property1
이라는 프로퍼티는 value1
인지, property2
는 value2
인지 검사하는 구문이다.
1
if(employee is { Position: "Junior", YearsOfExperience: >= 2 }) { ... }
위와 같이 조건문에 활용할 수 있겠다.
또한 이는 switch
문과도 함께 사용될 수 있는데, 다음과 같은 방식으로 사용될 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Address
{
public string Country { get; set; }
public string City { get; set; }
}
public float CalculateDiscount(Address address)
{
return address switch
{
{ Country: "USA", City: "New York" } => 0.10f, // 뉴욕에 사는 고객에게 10% 할인
{ Country: "USA", City: "Los Angeles" } => 0.05f, // 로스앤젤레스에 사는 고객에게 5% 할인
_ => 0 // 그 외 지역에 사는 고객에게는 할인 X
};
}
튜플 패턴 매칭 (Tuple Pattern Matching)
C# 7.0에서 도입된 튜플과 C# 8.0에서 도입된 패턴 매칭 기능을 결합한 것.
튜플의 각 요소에 대해 패턴 매칭을 수행해 로직 분기를 코드로 구현할 수 있다.
기본적인 사용법은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
(var variable1, var variable2, ...) = tuple;
switch(tuple)
{
case (pattern1, pattern2, ...):
// 이 조건에 실행할 코드
break;
case (anotherPattern1, anotherPattern2, ...):
// 이 조건에 실행할 코드
break;
}
프로퍼티 패턴 매칭과 크게 다르지 않은 것 같다.
간단한 예제를 작성해보자면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public enum Location { Town, Forest, Dungeon }
(int health, int mana, Location location) playerStatus = (health: 25, mana: 10, location: Location.Dungeon);
public void HandleGameEvent((int health, int mana, Location location) playerStatus)
{
switch (playerStatus)
{
case (> 50, > 30, Location.Forest):
Debug.Log("플레이어가 숲에서 강력한 몬스터를 만났습니다.");
break;
case (_, _, Location.Dungeon) when playerStatus.health < 20:
Debug.Log("플레이어의 체력이 매우 낮아 던전을 탈출합니다.");
break;
case (var health, var mana, _) when health + mana < 50:
Debug.Log("플레이어가 적의 공격을 받아 체력과 마나가 심각하게 소진되었습니다.");
break;
case (_, _, Location.Town):
Debug.Log("플레이어가 마을에 도착해 휴식을 취합니다.");
break;
default:
Debug.Log("플레이어가 모험을 계속합니다.");
break;
}
}
여기서 _
은 와일드카드가 된다.