| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
| 26 | 27 | 28 | 29 | 30 |
- 프레임워크
- 패스트캠퍼스
- 샘플
- DotsTween
- 오공완
- 2D Camera
- job
- 가이드
- ui
- adfit
- C#
- Tween
- unity
- 패스트캠퍼스후기
- Unity Editor
- TextMeshPro
- RSA
- sha
- Framework
- 직장인공부
- 직장인자기계발
- 환급챌린지
- Dots
- AES
- 암호화
- Custom Package
- Job 시스템
- base64
- 최적화
- 게임개발
- Today
- Total
EveryDay.DevUp
형변환 완전 정리 — 암시적·명시적·as·is 본문
형변환 완전 정리 — 암시적·명시적·as·is
같은 "타입 바꾸기"인데 왜 네 가지 방법이 있는 걸까? 각각이 실패했을 때 벌어지는 일이 완전히 다르다. 이 차이를 모르면 런타임에 터지는 코드를 쓰게 된다.
문제 제기
Unity에서 충돌 처리 코드를 작성한다고 하자. 부딪힌 오브젝트가 Enemy인지 확인하고 데미지를 주고 싶다.
// 어떤 방식을 써야 할까?
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# 코드가 컴파일되면 변환되는 중간 언어) 명령어가 생성되는지까지 파고들어 보자.
개념 정의
네 가지 형변환 한눈에 보기
집을 이사하는 상황을 떠올려 보자. 암시적 변환은 작은 트럭의 짐을 큰 트럭으로 옮기는 것 — 무조건 들어가니까 확인할 필요가 없다. 명시적 캐스트는 큰 트럭의 짐을 작은 트럭에 억지로 밀어넣는 것 — 안 들어가면 짐이 쏟아진다(예외 발생). as는 "들어가면 넣고, 안 들어가면 말자"는 태도 — 실패해도 조용하다. is 패턴 매칭은 "일단 들어가는지 확인하고, 되면 바로 적재까지 한 번에" — 가장 실용적이다.
암시적 변환 — 컴파일러가 보장하는 안전한 변환
데이터 손실이 절대 발생하지 않는다고 컴파일러가 판단할 때, 별도 문법 없이 자동으로 변환이 일어난다.
// 숫자 타입: 작은 범위 → 큰 범위 (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: 모든 타입의 조상
// 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, 숫자는 비트 수를 뜻한다.
주의할 함정 — 정밀도 손실이 허용되는 암시적 변환:
int big = 123_456_789;
float f = big; // 암시적 변환 허용!
Console.WriteLine(f); // 123456792 — 끝자리가 달라졌다!
int→float 변환은 암시적으로 허용되지만, float의 유효숫자가 약 7자리뿐이라 큰 정수에서 정밀도 손실이 발생한다. C# 명세는 "크기(magnitude)는 보존된다"는 기준으로 이 변환을 허용한 것이다.
명시적 캐스트 — 개발자가 책임지는 강제 변환
(Type)expr— 명시적 캐스트 (Explicit Cast) 괄호 안에 타입을 적어 강제 변환한다. 값 타입에서는 데이터를 잘라내고, 참조 타입에서는 런타임에 타입 호환성을 검사한다.
예시:int i = (int)3.14;소수점 아래를 버리고 3이 된다
참조 타입의 명시적 캐스트 (다운캐스팅):
Animal animal = new Dog();
Dog dog = (Dog)animal; // 성공 — 실제 타입이 Dog이므로
dog.Speak(); // "Woof!"
Animal another = new Cat();
Dog wrong = (Dog)another; // InvalidCastException! 실제 타입은 Cat
// (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
Animal animal = new Cat();
Dog dog = animal as Dog; // null — Cat은 Dog가 아니므로
if (dog != null)
{
dog.Speak(); // 실행되지 않음
}
// 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의 제한 사항:
// 값 타입에는 사용 불가 — 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이 아님도 보장
Animal animal = new Dog();
// C# 7+ 패턴 매칭 — 권장 방식
if (animal is Dog dog)
{
dog.Speak(); // dog는 이 블록에서 Dog 타입으로 확정, null 아님 보장
}
// 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 패턴 매칭이 더 나은 이유가 있다:
- null 체크를 빼먹을 수 없다 —
if블록 안에서만 변수에 접근 가능 - 타입 검사가 1회 —
as+ null 체크와 동일한 효율 - 스코프가 명확 — 변수의 유효 범위가 블록으로 제한됨
내부 동작
castclass vs isinst — CLR이 형변환을 처리하는 두 가지 방식
두 명령어의 동작을 정리하면:
castclass |
isinst |
|
|---|---|---|
| 사용하는 C# 문법 | (Dog)animal |
as, is 패턴 매칭 |
| 성공 시 | 변환된 참조 반환 | 변환된 참조 반환 |
| 실패 시 | InvalidCastException |
null 반환 |
| null 입력 시 | null 반환 (예외 없음) |
null 반환 |
| 실패 비용 | 매우 큼 (스택 트레이스 캡처) | 거의 없음 (수 나노초) |
is + 캐스트 vs is 패턴 매칭 — 타입 검사 횟수의 차이
C# 7 이전의 is + 캐스트 패턴에는 숨겨진 비효율이 있다.
// ❌ C# 7 이전 패턴: 타입 검사 2회
if (animal is Dog) // isinst → 1번째 타입 검사
{
Dog dog = (Dog)animal; // castclass → 2번째 타입 검사
dog.Speak();
}
// 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# 7+ 패턴 매칭: 타입 검사 1회
if (animal is Dog dog) // isinst → 1번만 검사 + 변수 할당
{
dog.Speak();
}
// 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 범위 초과)
// unchecked (기본값): 조용히 잘림
int big = 300;
byte b1 = (byte)big; // 44 (300 % 256 = 44)
// checked: 예외 발생
checked
{
byte b2 = (byte)big; // OverflowException!
}
// 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은 원래 타입과 정확히 일치해야 한다.
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으로 변환
// 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: 명시적 캐스트로 위험한 다운캐스팅
void OnTriggerEnter(Collider other)
{
// 상대가 Enemy가 아니면 게임이 터진다
Enemy enemy = (Enemy)other.GetComponent<MonoBehaviour>();
enemy.TakeDamage(damage);
}
// 명시적 캐스트의 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(...); }
void OnTriggerEnter(Collider other)
{
if (other.TryGetComponent<Enemy>(out var enemy))
{
enemy.TakeDamage(damage);
}
}
TryGetComponent는 내부적으로 한 번의 검색만 수행한다. 형변환 관점에서 보면, 제네릭 타입 매개변수 <Enemy>가 컴파일 타임에 타입을 확정하므로 런타임 캐스팅 자체가 불필요해지는 것이다.
Before / After — 다형적 아이템 처리
❌ Before: if-else 체인 + 각각 캐스팅
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 패턴 매칭
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;
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# 패턴 매칭이 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!
}
// ✅ Unity 오브젝트에는 Unity의 == 연산자를 사용
if (go != null) // Unity 오버로딩 → 올바름
{
go.SetActive(false);
}
// is 패턴 매칭을 쓰려면 Unity null 검사를 추가
if (go is GameObject g && g != null) // CLR 검사 + Unity 검사
{
g.SetActive(false);
}
왜 이런 일이 일어나는가? is와 as는 CLR의 isinst 명령어를 사용하는데, 이 명령어는 C# 객체의 참조가 유효한지만 검사한다. Unity가 오버로딩한 == 연산자(C++ 네이티브 객체의 생존 여부를 확인하는)는 isinst와는 완전히 별개의 메커니즘이다.
핵심 규칙: Unity 오브젝트(MonoBehaviour, GameObject, ScriptableObject 등)에 대해서는 is not null 대신 != null을 사용하라.
함정 2: boxing 후 다른 타입으로 언박싱
// ❌ int로 박싱한 것을 long으로 직접 꺼낼 수 없다
object boxed = 42; // int boxing
long value = (long)boxed; // InvalidCastException!
// ✅ 원래 타입으로 먼저 언박싱
object boxed = 42;
long value = (long)(int)boxed; // int로 언박싱 → long으로 변환
이 실수는 특히 Unity의 PlayerPrefs나 JSON 파싱에서 자주 발생한다. 숫자를 object로 받았을 때, 원본이 어떤 타입이었는지 항상 확인해야 한다.
함정 3: as 연산자 후 null 체크 누락
// ❌ null 체크 없이 바로 사용
Dog dog = animal as Dog;
dog.Speak(); // animal이 Dog가 아니면 NullReferenceException!
// ✅ 애초에 is 패턴 매칭을 사용
if (animal is Dog dog)
{
dog.Speak(); // null일 수 없음이 보장됨
}
as를 쓸 때 null 체크를 빼먹는 실수는 매우 흔하다. is 패턴 매칭은 이 실수를 구조적으로 방지한다.
함정 4: 정밀도 손실이 숨겨진 암시적 변환
// ❌ 컴파일러가 허용하지만 정밀도가 손실되는 변환
long bigNumber = 9_007_199_254_740_993L;
double d = bigNumber; // 암시적 변환 허용!
Console.WriteLine(d == bigNumber); // false! — double이 이 값을 정확히 표현 못함
long→double 변환은 암시적으로 허용되지만, double의 유효숫자(약 15자리)를 초과하는 큰 정수에서 값이 달라질 수 있다. 정확한 정수 연산이 필요하면 decimal을 사용하라.
C# 버전별 변화
C# 1~6: is + 캐스트의 시대
// C# 1~6: 타입 검사와 캐스팅이 분리
if (animal is Dog) // isinst — 1번째 검사
{
Dog dog = (Dog)animal; // castclass — 2번째 검사
dog.Speak();
}
IL_0002: isinst Dog // 1번째 타입 검사
// ... (분기 처리) ...
IL_0010: castclass Dog // 2번째 타입 검사 — 중복!
C# 7: 패턴 매칭의 등장 — 게임 체인저
// 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_0002: isinst Dog // 1번만 검사 + 결과를 변수에 저장
IL_0007: stloc.0 // dog에 저장 — castclass 불필요
C# 8: switch 표현식 + 속성 패턴
// 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# 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# 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 패턴 매칭을 기본으로 쓰고, 실패가 버그인 상황에서만 명시적 캐스트를 써라.
'C# 심화' 카테고리의 다른 글
| string vs StringBuilder vs Span<char> — 문자열 처리 도구 선택 (0) | 2026.04.01 |
|---|---|
| string은 왜 불변인가 — intern pool의 실체 / == 이 값 비교인 이유 / 변경할 때마다 새 객체가 생기는 원리 (0) | 2026.03.31 |
| readonly vs const — 무엇이 다른가 (0) | 2026.03.30 |
| var — 타입 추론의 원리와 한계 (0) | 2026.03.30 |
| null 처리 연산자 — ??, ?., ??= (0) | 2026.03.30 |
