포스트

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연산자는 이 다음에 설명한 **패턴 매칭**에서 유용하게 사용된다.

패턴 매칭

패턴 매칭은 데이터의 형태나 값에 따라 코드의 실행 경로를 결정하는 기법을 말한다.

패턴 매칭에는 몇가지 종류가 있는데, 그 종류는 다음과 같다.

  1. 타입 패턴 매칭
  2. 스위치 패턴 매칭
  3. 프로퍼티 패턴 매칭
  4. 튜플 패턴 매칭

타입 패턴 매칭 (Type Pattern Matching)

C# 7.0부터 사용할 수 있는 기능으로,

특정 객체가 지정된 타입인지 검사하고 해당 타입으로 바로 캐스트할 수 있도록 하는 기능.

기본적인 사용법은 다음과 같다.

1
2
3
4
if(expression is Type variableName)
{
    // 여기서 variableName 사용 가능
}

여기서 expression은 검사하려는 객체, Type은 검사하려는 타입, variableNameexpressionType으로 확인 될 경우 그 값을 가지게 될 변수이다.

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인지, property2value2인지 검사하는 구문이다.

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;
    }
}

여기서 _은 와일드카드가 된다.

참조

Casting and type conversions (C# Programming Guide)

Pattern matching overview

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.