EveryDay.DevUp

형변환 완전 정리 — 암시적·명시적·as·is 본문

C# 심화

형변환 완전 정리 — 암시적·명시적·as·is

EveryDay.DevUp 2026. 3. 31. 20:00

형변환 완전 정리 — 암시적·명시적·as·is

같은 "타입 바꾸기"인데 왜 네 가지 방법이 있는 걸까? 각각이 실패했을 때 벌어지는 일이 완전히 다르다. 이 차이를 모르면 런타임에 터지는 코드를 쓰게 된다.


문제 제기

Unity에서 충돌 처리 코드를 작성한다고 하자. 부딪힌 오브젝트가 Enemy인지 확인하고 데미지를 주고 싶다.

C#
// 어떤 방식을 써야 할까?
void OnCollisionEnter(Collision collision)
{
    // 방법 1: 명시적 캐스트
    Enemy enemy = (Enemy)collision.gameObject.GetComponent<MonoBehaviour>();

    // 방법 2: as 연산자
    Enemy enemy = collision.gameObject.GetComponent<MonoBehaviour>() as Enemy;

    // 방법 3: is 패턴 매칭
    if (collision.gameObject.GetComponent<MonoBehaviour>() is Enemy enemy)
    {
        enemy.TakeDamage(10);
    }
}

방법 1은 상대가 Enemy가 아니면 InvalidCastException으로 게임이 터진다. 방법 2는 null을 반환하는데, null 체크를 빼먹으면 NullReferenceException이 된다. 방법 3은 타입 검사와 변환을 동시에 처리하면서, 실패하면 자연스럽게 블록을 건너뛴다.

네 가지 형변환은 "실패를 어떻게 처리하느냐"가 핵심이다. 어떤 상황에서 어떤 방식을 써야 하는지, 내부에서 무슨 IL(Intermediate Language, C# 코드가 컴파일되면 변환되는 중간 언어) 명령어가 생성되는지까지 파고들어 보자.


개념 정의

네 가지 형변환 한눈에 보기

C# 형변환 4가지 — 실패 시 동작 비교

집을 이사하는 상황을 떠올려 보자. 암시적 변환은 작은 트럭의 짐을 큰 트럭으로 옮기는 것 — 무조건 들어가니까 확인할 필요가 없다. 명시적 캐스트는 큰 트럭의 짐을 작은 트럭에 억지로 밀어넣는 것 — 안 들어가면 짐이 쏟아진다(예외 발생). as는 "들어가면 넣고, 안 들어가면 말자"는 태도 — 실패해도 조용하다. is 패턴 매칭은 "일단 들어가는지 확인하고, 되면 바로 적재까지 한 번에" — 가장 실용적이다.

암시적 변환 — 컴파일러가 보장하는 안전한 변환

데이터 손실이 절대 발생하지 않는다고 컴파일러가 판단할 때, 별도 문법 없이 자동으로 변환이 일어난다.

C#
// 숫자 타입: 작은 범위 → 큰 범위 (Widening)
int score = 95;
long bigScore = score;       // int → long: 32비트 → 64비트, 손실 없음
double preciseScore = score; // int → double: 정수 → 부동소수점, 손실 없음

// 참조 타입: 파생 클래스 → 기본 클래스 (Upcasting)
Dog myDog = new Dog();
Animal animal = myDog;       // Dog → Animal: 항상 안전
object obj = myDog;          // Dog → object: 모든 타입의 조상
IL
// int → long 암시적 변환
IL_0001: ldc.i4.s  95        // int 95를 스택에 푸시
IL_0003: stloc.0             // score에 저장
IL_0004: ldloc.0             // score 로드
IL_0005: conv.i8             // int(32비트) → long(64비트) 변환
IL_0006: stloc.1             // bigScore에 저장

// int → double 암시적 변환
IL_0007: ldloc.0             // score 로드
IL_0008: conv.r8             // int → double(64비트 부동소수점) 변환
IL_0009: stloc.2             // preciseScore에 저장

conv.i8은 32비트 정수를 64비트로 부호 확장(sign extension)하는 명령어다. 값이 보존됨을 CLR(Common Language Runtime, C# 코드를 실행하는 런타임 엔진)이 보장한다. 참조 타입의 업캐스팅은 IL 수준에서 아무 변환 명령어도 생성하지 않는다 — 참조값이 그대로 전달될 뿐이다.

conv.i8 / conv.r8 — 숫자 타입 변환 IL 명령어 conv.i8은 스택 최상위 값을 64비트 정수(long)로, conv.r8은 64비트 부동소수점(double)로 변환한다. 앞의 conv는 convert, 뒤의 i/r은 integer/real, 숫자는 비트 수를 뜻한다.

주의할 함정 — 정밀도 손실이 허용되는 암시적 변환:

C#
int big = 123_456_789;
float f = big;           // 암시적 변환 허용!
Console.WriteLine(f);    // 123456792 — 끝자리가 달라졌다!

intfloat 변환은 암시적으로 허용되지만, float의 유효숫자가 약 7자리뿐이라 큰 정수에서 정밀도 손실이 발생한다. C# 명세는 "크기(magnitude)는 보존된다"는 기준으로 이 변환을 허용한 것이다.

명시적 캐스트 — 개발자가 책임지는 강제 변환

(Type)expr — 명시적 캐스트 (Explicit Cast) 괄호 안에 타입을 적어 강제 변환한다. 값 타입에서는 데이터를 잘라내고, 참조 타입에서는 런타임에 타입 호환성을 검사한다.
예시: int i = (int)3.14; 소수점 아래를 버리고 3이 된다

참조 타입의 명시적 캐스트 (다운캐스팅):

C#
Animal animal = new Dog();
Dog dog = (Dog)animal;    // 성공 — 실제 타입이 Dog이므로
dog.Speak();              // "Woof!"

Animal another = new Cat();
Dog wrong = (Dog)another; // InvalidCastException! 실제 타입은 Cat
IL
// (Dog)animal — 명시적 캐스트의 IL
IL_0001: ldarg.0               // animal을 스택에 로드
IL_0002: castclass Dog         // Dog로 캐스트 — 실패 시 예외!
IL_0007: stloc.0               // dog 변수에 저장
IL_0008: ldloc.0               // dog 로드
IL_0009: callvirt instance void Animal::Speak()  // 가상 메서드 호출

castclass는 CLR의 핵심 캐스팅 명령어다. 객체의 런타임 타입이 지정된 타입과 호환되면 참조를 그대로 반환하고, 호환되지 않으면 InvalidCastException을 던진다. 한 가지 특이한 점은, null이 들어오면 예외 없이 null을 그대로 반환한다.

castclass — IL 캐스팅 명령어 스택 최상위의 객체 참조를 지정된 타입으로 변환한다. 호환되면 참조를 그대로 반환하고, 호환되지 않으면 InvalidCastException을 발생시킨다.

as 연산자 — 예외 없는 안전한 변환 시도

as — 안전한 형변환 연산자 변환이 가능하면 변환된 참조를 반환하고, 불가능하면 예외 없이 null을 반환한다. 참조 타입과 nullable 값 타입에서만 사용 가능하다.
예시: Dog d = animal as Dog; animal이 Dog가 아니면 d는 null
C#
Animal animal = new Cat();
Dog dog = animal as Dog;       // null — Cat은 Dog가 아니므로
if (dog != null)
{
    dog.Speak();               // 실행되지 않음
}
IL
// animal as Dog — as 연산자의 IL
IL_0001: ldarg.0               // animal을 스택에 로드
IL_0002: isinst Dog            // Dog 호환 검사 — 실패 시 null 반환
IL_0007: stloc.0               // dog 변수에 저장 (Dog 참조 or null)
IL_0008: ldloc.0               // dog 로드
IL_0009: ldnull                // null 로드
IL_000a: cgt.un                // dog > null? (null이 아닌지 비교)
IL_000c: stloc.1               // 비교 결과 저장
IL_000d: ldloc.1
IL_000e: brfalse.s IL_0017     // null이면 점프 (건너뜀)
IL_0010: ldloc.0               // dog 로드
IL_0011: callvirt instance void Animal::Speak()  // 호출
isinst — IL 타입 검사 명령어 스택 최상위의 객체가 지정된 타입과 호환되면 변환된 참조를 반환하고, 호환되지 않으면 null을 반환한다. castclass와 달리 절대 예외를 던지지 않는다.

핵심 차이가 여기에 있다. castclass는 실패 시 예외, isinst는 실패 시 null. 이 한 줄의 차이가 코드의 안전성을 결정한다.

as의 제한 사항:

C#
// 값 타입에는 사용 불가 — null을 담을 수 없으므로
// int result = obj as int;  // 컴파일 에러!

// nullable 값 타입은 가능
int? result = obj as int?;    // OK — null 반환이 가능하므로

is 패턴 매칭 — 검사와 변환을 한 번에

is — 타입 검사 연산자 + 패턴 매칭 (C# 7+) C# 7 이전: 타입 호환 여부만 bool로 반환한다. C# 7 이후: 타입 검사 + 변환 + 변수 할당을 한 문장에서 처리한다.
예시: if (animal is Dog dog) { dog.Bark(); } 검사·변환·할당이 한 번에 — null이 아님도 보장
C#
Animal animal = new Dog();

// C# 7+ 패턴 매칭 — 권장 방식
if (animal is Dog dog)
{
    dog.Speak();     // dog는 이 블록에서 Dog 타입으로 확정, null 아님 보장
}
IL
// animal is Dog dog — is 패턴 매칭의 IL
IL_0001: ldarg.0               // animal을 스택에 로드
IL_0002: isinst Dog            // Dog 호환 검사 + 변환 (한 번에!)
IL_0007: stloc.0               // dog 변수에 저장
IL_0008: ldloc.0
IL_0009: ldnull
IL_000a: cgt.un                // null이 아닌지 비교
IL_000c: stloc.1
IL_000d: ldloc.1
IL_000e: brfalse.s IL_0019     // null이면 점프
IL_0011: ldloc.0
IL_0012: callvirt instance void Animal::Speak()

as 연산자의 IL과 거의 동일하다. 둘 다 isinst 한 번으로 처리된다. 하지만 is 패턴 매칭이 더 나은 이유가 있다:

  1. null 체크를 빼먹을 수 없다if 블록 안에서만 변수에 접근 가능
  2. 타입 검사가 1회as + null 체크와 동일한 효율
  3. 스코프가 명확 — 변수의 유효 범위가 블록으로 제한됨

내부 동작

castclass vs isinst — CLR이 형변환을 처리하는 두 가지 방식

castclass vs isinst — 실패 경로 비교

두 명령어의 동작을 정리하면:

  castclass isinst
사용하는 C# 문법 (Dog)animal as, is 패턴 매칭
성공 시 변환된 참조 반환 변환된 참조 반환
실패 시 InvalidCastException null 반환
null 입력 시 null 반환 (예외 없음) null 반환
실패 비용 매우 큼 (스택 트레이스 캡처) 거의 없음 (수 나노초)

is + 캐스트 vs is 패턴 매칭 — 타입 검사 횟수의 차이

C# 7 이전의 is + 캐스트 패턴에는 숨겨진 비효율이 있다.

C#
// ❌ C# 7 이전 패턴: 타입 검사 2회
if (animal is Dog)               // isinst → 1번째 타입 검사
{
    Dog dog = (Dog)animal;       // castclass → 2번째 타입 검사
    dog.Speak();
}
IL
// is + 캐스트의 IL — 타입 검사가 2회 발생
IL_0001: ldarg.0
IL_0002: isinst Dog            // ← 1번째: isinst로 타입 검사
IL_0007: ldnull
IL_0008: cgt.un
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: brfalse.s IL_001e     // false면 점프
IL_000f: ldarg.0               // animal을 다시 로드
IL_0010: castclass Dog         // ← 2번째: castclass로 또 검사!
IL_0015: stloc.1
IL_0016: ldloc.1
IL_0017: callvirt instance void Animal::Speak()
C#
// ✅ C# 7+ 패턴 매칭: 타입 검사 1회
if (animal is Dog dog)           // isinst → 1번만 검사 + 변수 할당
{
    dog.Speak();
}
IL
// is 패턴 매칭의 IL — 타입 검사가 1회
IL_0001: ldarg.0
IL_0002: isinst Dog            // ← 단 1번: 검사 + 변환 동시
IL_0007: stloc.0               // dog 변수에 저장
IL_0008: ldloc.0
IL_0009: ldnull
IL_000a: cgt.un
IL_000c: stloc.1
IL_000d: ldloc.1
IL_000e: brfalse.s IL_0019
IL_0011: ldloc.0               // 저장된 dog를 바로 사용
IL_0012: callvirt instance void Animal::Speak()

is + 캐스트 방식은 코드 크기 31바이트, is 패턴 매칭은 26바이트. isinst의 결과를 변수에 저장해두기 때문에 castclass를 다시 호출할 필요가 없다.

숫자 타입 명시적 변환의 내부 — checked vs unchecked

checked / unchecked — 오버플로 검사 컨텍스트 checked 블록 안에서는 숫자 변환 시 오버플로가 발생하면 OverflowException을 던진다. unchecked(기본값)에서는 조용히 상위 비트를 잘라낸다.
예시: checked { byte b = (byte)300; } → OverflowException (300은 byte 범위 초과)
C#
// unchecked (기본값): 조용히 잘림
int big = 300;
byte b1 = (byte)big;           // 44 (300 % 256 = 44)

// checked: 예외 발생
checked
{
    byte b2 = (byte)big;       // OverflowException!
}
IL
// unchecked 변환
IL_0003: ldloc.0               // big (300) 로드
IL_0004: conv.u1               // 하위 8비트만 취함 → 44

// checked 변환
IL_0003: ldloc.0               // big (300) 로드
IL_0004: conv.ovf.u1           // 범위 초과 시 OverflowException!

conv.u1은 단순히 하위 8비트를 잘라내고, conv.ovf.u1은 값이 0~255 범위를 벗어나면 예외를 던진다. .ovf 접미사가 오버플로 검사를 수행하는 것이다.

boxing된 값 타입의 캐스팅 — 정확한 타입으로만 언박싱

boxing / unboxing boxing은 값 타입을 object나 인터페이스로 변환할 때 힙에 복사본을 만드는 과정이다. unboxing은 그 반대로, 힙의 박스에서 값을 꺼내는 과정이다. unboxing은 원래 타입과 정확히 일치해야 한다.
C#
int val = 42;
object boxed = val;            // boxing: 힙에 int 42 복사

int unboxed = (int)boxed;     // ✅ OK — 원래 타입 int로 언박싱
// long wrong = (long)boxed;  // ❌ InvalidCastException! int를 long으로 직접 못 꺼냄
long correct = (long)(int)boxed; // ✅ OK — 먼저 int로 언박싱 → long으로 변환
IL
// boxing + unboxing
IL_0001: ldc.i4.s  42
IL_0003: stloc.0               // val = 42
IL_0004: ldloc.0
IL_0005: box [System.Runtime]System.Int32       // ← 힙에 복사
IL_000a: stloc.1               // boxed에 저장
IL_000b: ldloc.1
IL_000c: unbox.any [System.Runtime]System.Int32 // ← 정확한 타입으로 꺼냄

// int → long 올바른 변환
IL_000b: ldloc.1
IL_000c: unbox.any [System.Runtime]System.Int32 // 먼저 int로 꺼냄
IL_0011: conv.i8                                // 그 다음 long으로 변환

unbox.any는 박스 안의 타입이 정확히 일치하지 않으면 InvalidCastException을 던진다. int로 박싱한 것을 long으로 직접 꺼낼 수 없다 — 항상 원래 타입으로 먼저 언박싱한 뒤 변환해야 한다.


실전 적용

선택 기준 — 언제 어떤 형변환을 쓰는가

상황 추천 방식 이유
변환이 반드시 성공해야 할 때 (Type) 명시적 캐스트 실패 = 버그, 예외로 즉시 발견
변환 실패가 정상 흐름일 때 is Type var 패턴 매칭 검사+변환 1회, null 안전, 스코프 명확
숫자 타입 변환 (Type) 명시적 캐스트 as는 값 타입에 사용 불가
레거시 코드 유지보수 as + null 체크 C# 7 이전 코드와의 호환

Before / After — Unity 충돌 처리

❌ Before: 명시적 캐스트로 위험한 다운캐스팅

C#
void OnTriggerEnter(Collider other)
{
    // 상대가 Enemy가 아니면 게임이 터진다
    Enemy enemy = (Enemy)other.GetComponent<MonoBehaviour>();
    enemy.TakeDamage(damage);
}
IL
// 명시적 캐스트의 IL
IL_0008: castclass Enemy   // ← 실패 시 InvalidCastException!
IL_000d: stloc.0
IL_000e: ldloc.0
IL_000f: ldc.r4 10
IL_0014: callvirt instance void Enemy::TakeDamage(float32)

✅ After: TryGetComponent로 안전한 처리

TryGetComponent<T> — Unity 안전한 컴포넌트 검색 (Unity 2019.2+) 컴포넌트가 있으면 out 매개변수에 할당하고 true를 반환한다. 없으면 false를 반환한다. GetComponent + null 체크보다 효율적이며, 에디터에서 불필요한 null 경고를 생성하지 않는다.
예시: if (TryGetComponent<Rigidbody>(out var rb)) { rb.AddForce(...); }
C#
void OnTriggerEnter(Collider other)
{
    if (other.TryGetComponent<Enemy>(out var enemy))
    {
        enemy.TakeDamage(damage);
    }
}

TryGetComponent는 내부적으로 한 번의 검색만 수행한다. 형변환 관점에서 보면, 제네릭 타입 매개변수 <Enemy>가 컴파일 타임에 타입을 확정하므로 런타임 캐스팅 자체가 불필요해지는 것이다.

Before / After — 다형적 아이템 처리

❌ Before: if-else 체인 + 각각 캐스팅

C#
void UseItem(Item item)
{
    if (item is Potion)
    {
        Potion p = (Potion)item;       // isinst + castclass = 2회 검사
        player.Heal(p.HealAmount);
    }
    else if (item is Weapon)
    {
        Weapon w = (Weapon)item;       // 또 2회 검사
        player.Equip(w);
    }
}

✅ After: switch 패턴 매칭

C#
void UseItem(Item item)
{
    switch (item)
    {
        case Potion p:                 // isinst 1회로 검사+변환
            player.Heal(p.HealAmount);
            break;
        case Weapon w when w.Level <= player.Level:  // 조건부 매칭
            player.Equip(w);
            break;
        case Weapon w:
            UIManager.Show("레벨이 부족합니다");
            break;
    }
}

switch 패턴 매칭은 각 case마다 isinst 1회로 처리하며, when 절로 추가 조건도 붙일 수 있다.

사용자 정의 변환 연산자 — implicit vs explicit

자신만의 타입에서 형변환 규칙을 정의할 수 있다.

implicit operator / explicit operator — 사용자 정의 변환 연산자 타입 간 변환 규칙을 직접 정의한다. implicit은 자동 변환(데이터 손실 없음), explicit은 강제 변환(손실 가능)에 사용한다.
예시: public static implicit operator double(Meter m) => m.Value;
C#
public readonly struct Meter
{
    public double Value { get; }
    public Meter(double value) => Value = value;

    // Meter → double: 정보 손실 없음 → implicit
    public static implicit operator double(Meter m) => m.Value;

    // double → Meter: 의미적 변환 → explicit
    public static explicit operator Meter(double d) => new Meter(d);
}

Meter distance = (Meter)3.14;  // explicit: op_Explicit 호출
double raw = distance;          // implicit: op_Implicit 호출

IL 수준에서 implicit/explicit 연산자는 op_Implicit/op_Explicit이라는 static 메서드로 컴파일된다. as 연산자는 이 사용자 정의 변환을 무시한다as는 CLR의 런타임 타입 정보만 검사할 뿐이다.


함정과 주의사항

함정 1: Unity의 fake null — is/as가 속는 경우

이것은 Unity 개발자가 반드시 알아야 할 핵심 함정이다.

Unity의 UnityEngine.Object== 연산자를 오버로딩한다. Destroy()로 파괴된 오브젝트는 C# 참조는 살아있지만, Unity 내부의 C++ 객체는 이미 해제된 상태다. Unity는 이런 객체를 == null로 비교하면 true를 반환하도록 설계했다.

C#
// ❌ C# 패턴 매칭이 Unity fake null을 감지하지 못하는 상황
GameObject go = new GameObject("Test");
Destroy(go);
// 다음 프레임 이후...

Debug.Log(go == null);        // true  — Unity의 == 오버로딩
Debug.Log(go is null);        // false! — C#의 순수 참조 검사
Debug.Log(go is not null);    // true!  — 파괴된 객체를 "살아있다"고 판단

// is 패턴 매칭도 속는다
if (go is GameObject g)       // true! — CLR 수준에서 참조가 유효하므로
{
    g.SetActive(false);       // MissingReferenceException!
}
C#
// ✅ Unity 오브젝트에는 Unity의 == 연산자를 사용
if (go != null)               // Unity 오버로딩 → 올바름
{
    go.SetActive(false);
}

// is 패턴 매칭을 쓰려면 Unity null 검사를 추가
if (go is GameObject g && g != null)  // CLR 검사 + Unity 검사
{
    g.SetActive(false);
}

왜 이런 일이 일어나는가? isas는 CLR의 isinst 명령어를 사용하는데, 이 명령어는 C# 객체의 참조가 유효한지만 검사한다. Unity가 오버로딩한 == 연산자(C++ 네이티브 객체의 생존 여부를 확인하는)는 isinst와는 완전히 별개의 메커니즘이다.

핵심 규칙: Unity 오브젝트(MonoBehaviour, GameObject, ScriptableObject 등)에 대해서는 is not null 대신 != null을 사용하라.

함정 2: boxing 후 다른 타입으로 언박싱

C#
// ❌ int로 박싱한 것을 long으로 직접 꺼낼 수 없다
object boxed = 42;              // int boxing
long value = (long)boxed;      // InvalidCastException!
C#
// ✅ 원래 타입으로 먼저 언박싱
object boxed = 42;
long value = (long)(int)boxed; // int로 언박싱 → long으로 변환

이 실수는 특히 Unity의 PlayerPrefs나 JSON 파싱에서 자주 발생한다. 숫자를 object로 받았을 때, 원본이 어떤 타입이었는지 항상 확인해야 한다.

함정 3: as 연산자 후 null 체크 누락

C#
// ❌ null 체크 없이 바로 사용
Dog dog = animal as Dog;
dog.Speak();                   // animal이 Dog가 아니면 NullReferenceException!
C#
// ✅ 애초에 is 패턴 매칭을 사용
if (animal is Dog dog)
{
    dog.Speak();               // null일 수 없음이 보장됨
}

as를 쓸 때 null 체크를 빼먹는 실수는 매우 흔하다. is 패턴 매칭은 이 실수를 구조적으로 방지한다.

함정 4: 정밀도 손실이 숨겨진 암시적 변환

C#
// ❌ 컴파일러가 허용하지만 정밀도가 손실되는 변환
long bigNumber = 9_007_199_254_740_993L;
double d = bigNumber;          // 암시적 변환 허용!
Console.WriteLine(d == bigNumber); // false! — double이 이 값을 정확히 표현 못함

longdouble 변환은 암시적으로 허용되지만, double의 유효숫자(약 15자리)를 초과하는 큰 정수에서 값이 달라질 수 있다. 정확한 정수 연산이 필요하면 decimal을 사용하라.


C# 버전별 변화

C# 1~6: is + 캐스트의 시대

C#
// C# 1~6: 타입 검사와 캐스팅이 분리
if (animal is Dog)                 // isinst — 1번째 검사
{
    Dog dog = (Dog)animal;         // castclass — 2번째 검사
    dog.Speak();
}
IL
IL_0002: isinst Dog               // 1번째 타입 검사
// ... (분기 처리) ...
IL_0010: castclass Dog            // 2번째 타입 검사 — 중복!

C# 7: 패턴 매칭의 등장 — 게임 체인저

C#
// C# 7: 타입 검사 + 변수 할당을 한 번에
if (animal is Dog dog)             // isinst 1번으로 끝
{
    dog.Speak();
}

// switch 문에서도 패턴 매칭
switch (shape)
{
    case Circle c:
        area = Math.PI * c.Radius * c.Radius;
        break;
    case Rectangle r when r.Width == r.Height:  // when 절로 추가 조건
        area = r.Width * r.Width;
        break;
}
IL
IL_0002: isinst Dog               // 1번만 검사 + 결과를 변수에 저장
IL_0007: stloc.0                  // dog에 저장 — castclass 불필요

C# 8: switch 표현식 + 속성 패턴

C#
// C# 8: switch가 값을 반환하는 표현식이 됨
string description = shape switch
{
    Circle { Radius: > 10 } => "큰 원",       // 속성 패턴
    Circle c                => $"원(r={c.Radius})",
    Rectangle r             => $"사각형({r.Width}x{r.Height})",
    _                       => "알 수 없는 도형"  // 디폴트
};

C# 9: 논리 패턴 + 관계 패턴

C#
// C# 9: not, and, or 패턴
if (obj is not null)              // != null보다 안전 (연산자 오버로딩 우회)
{
    // ...
}

// 관계 패턴
string GetGrade(int score) => score switch
{
    >= 90 and <= 100 => "A",
    >= 80 and < 90   => "B",
    >= 70 and < 80   => "C",
    _                => "F"
};

is not null!= null보다 안전한 이유가 있다. != 연산자는 오버로딩될 수 있지만, is not null 패턴은 순수 참조 null 검사로 컴파일되어 사용자 정의 연산자를 우회한다. 다만 앞서 본 것처럼 Unity 오브젝트에서는 이 특성이 오히려 함정이 된다.

C# 10~11: 확장 속성 + 리스트 패턴

C#
// C# 10: 중첩 속성 접근
if (person is { Address.City: "Seoul" })
{
    // person.Address.City == "Seoul"
}

// C# 11: 리스트 패턴
int[] numbers = { 1, 2, 3, 4, 5 };

if (numbers is [1, 2, ..])             // 1, 2로 시작하면
{
    Console.WriteLine("1, 2로 시작하는 배열");
}

if (numbers is [var first, .. var rest]) // 분해
{
    // first = 1, rest = [2, 3, 4, 5]
}

정리

형변환 선택 체크리스트:

  • [ ] 암시적 변환으로 충분한가? — 작은→큰 타입, 파생→기본 클래스면 캐스팅 문법이 불필요하다
  • [ ] 숫자 타입 변환인가?(Type) 명시적 캐스트를 쓰고, 오버플로가 걱정되면 checked 블록을 사용하라
  • [ ] 참조 타입이고 변환이 반드시 성공해야 하는가?(Type) 명시적 캐스트를 써서 실패를 즉시 감지하라
  • [ ] 참조 타입이고 변환이 실패할 수 있는가?if (obj is Type var) 패턴 매칭을 써라 (C# 7+)
  • [ ] Unity 오브젝트인가?is not null 대신 != null을 사용하라. TryGetComponent를 우선 고려하라
  • [ ] boxing된 값 타입인가? — 원래 타입으로 먼저 언박싱한 뒤 변환하라 ((long)(int)boxed)

IL 명령어 요약:

C# 문법 IL 명령어 실패 시
(Dog)animal (참조) castclass InvalidCastException
animal as Dog isinst null
animal is Dog dog isinst false + 변수 미할당
(int)doubleVal (숫자) conv.i4 조용히 잘림
checked { (byte)big } conv.ovf.u1 OverflowException
(int)boxed (언박싱) unbox.any InvalidCastException

현대 C#에서의 한 줄 원칙: 타입 변환이 필요하면 is 패턴 매칭을 기본으로 쓰고, 실패가 버그인 상황에서만 명시적 캐스트를 써라.