타입 검사 연산자와 캐스트 식 — `is`, `as`, `typeof`, 캐스트
타입 검사 연산자와 캐스트 식 — is, as, typeof, 캐스트
어떤 객체를 받았을 때 "정말 이 타입이 맞을까?", "이 타입으로 바꿔 쓸 수 있을까?"라는 질문이 드는 순간이 있어요. 바로 이때 쓰는 게 타입을 검사하거나 변환하는 연산자인데, C#이 제공하는 is, as, typeof, 그리고 캐스트 식을 하나씩 살펴볼게요.
출처: Microsoft Learn
본문
이 연산자와 식들은 타입을 검사하거나 변환하는 일을 담당해요. is 연산자는 표현식의 런타임 타입이 주어진 타입과 호환되는지 확인하고, as 연산자는 런타임 타입이 호환될 때 표현식을 명시적으로 그 타입으로 변환해요. 캐스트 식은 목표 타입으로의 명시적 변환을 수행하고, typeof 연산자는 타입에 대한 System.Type 인스턴스를 얻어요.
[!NOTE] C# 언어 참조는 가장 최근에 릴리스된 C# 버전을 기준으로 해요. 최근 세 버전에서 처음 도입된 기능이나 현재 공개 미리 보기에 포함된 기능은 문서에 표시됩니다. 특정 기능이 C#에 처음 등장한 시기를 찾으려면 C# 언어 버전 기록 문서를 참고하세요.
is 연산자
is 연산자는 표현식 결과의 런타임 타입이 주어진 타입과 호환되는지 확인해요. 그리고 표현식 결과를 패턴과 비교하는 일도 담당하죠.
타입 검사용 is 연산자를 쓴 식은 다음과 같은 형태를 가져요.
E is T
여기서 E는 값을 반환하는 표현식이고, T는 타입 이름이나 타입 매개변수예요. E 자리에 익명 메서드나 람다 식은 올 수 없어요.
is 연산자는 표현식 결과가 null이 아니면서 다음 조건 중 하나라도 참일 때 true를 반환해요.
- 표현식 결과의 런타임 타입이
T로 동일 변환(identity conversion)되는 경우. - 표현식 결과의 런타임 타입이
T에서 파생됐거나, 인터페이스T를 구현했거나, 그 외의 암시적 참조 변환이 존재하는 경우. 이 조건이 상속 관계와 인터페이스 구현을 모두 커버해요. - 표현식 결과의 런타임 타입이 기본 타입
T를 가진 널 가능 값 타입이면서HasValue가true인 경우. - 표현식이
ref struct의 인스턴스가 아닐 때, 런타임 타입에서T로 가는 박싱 또는 언박싱 변환이 존재하는 경우.
반면에 is 연산자는 사용자 정의 변환이나 암시적 span 변환은 고려하지 않아요.
아래 예시는 표현식 결과의 런타임 타입이 주어진 타입에서 파생된 경우, 즉 타입 사이에 참조 변환이 존재할 때 is 연산자가 true를 반환한다는 걸 보여줘요.
public class Base { }
public class Derived : Base { }
public static class IsOperatorExample
{
public static void Main()
{
object b = new Base();
Console.WriteLine(b is Base); // output: True
Console.WriteLine(b is Derived); // output: False
object d = new Derived();
Console.WriteLine(d is Base); // output: True
Console.WriteLine(d is Derived); // output: True
}
}
b에는 Base 인스턴스가 들어 있으니 Base 검사는 참이지만 Derived 검사는 거짓이에요. 반대로 d는 Derived 인스턴스인데, Derived는 Base를 상속하므로 Base든 Derived든 둘 다 참이 나오죠.
다음 예시는 is 연산자가 박싱·언박싱 변환은 반영하지만 숫자 변환은 고려하지 않는다는 걸 보여줘요.
int i = 27;
Console.WriteLine(i is System.IFormattable); // output: True
object iBoxed = i;
Console.WriteLine(iBoxed is int); // output: True
Console.WriteLine(iBoxed is long); // output: False
int는 IFormattable 인터페이스를 구현하니까 첫 줄은 참이에요. iBoxed는 박싱된 int이므로 int 검사는 참이지만, 박싱된 int는 long으로 자동 변환되지 않으니 long 검사는 거짓이죠. 숫자 사이의 변환이 is에서 고려되지 않는 게 바로 이 지점이에요.
C# 변환에 대한 자세한 내용은 C# 언어 사양의 Conversions 장을 참고하세요.
패턴 매칭을 통한 타입 검사
is 연산자는 표현식 결과를 패턴과 비교하기도 해요. 아래 예시는 선언 패턴을 이용해 표현식의 런타임 타입을 확인하는 방법을 보여줘요.
int i = 23;
object iBoxed = i;
int? jNullable = 7;
if (iBoxed is int a && jNullable is int b)
{
Console.WriteLine(a + b); // output 30
}
iBoxed는 박싱된 int라서 int a로 풀어내고, jNullable은 널 가능 int?라서 int b로 변환해요. 두 검사가 모두 성공해야 조건이 성립하고, a + b가 30으로 출력되죠.
지원되는 패턴의 전체 목록은 패턴 문서에서 확인할 수 있어요.
as 연산자
as 연산자는 표현식의 결과를 주어진 참조 타입이나 널 가능 값 타입으로 명시적으로 변환할 때 사용해요. 변환이 불가능하면 as 연산자는 null을 반환하죠. 캐스트 식과 달리 as 연산자는 절대 예외를 던지지 않아요.
다음 형태의 식은
E as T
여기서 E는 값을 반환하는 표현식이고 T는 타입 이름이나 타입 매개변수인데, 이 표현식은 아래와 같은 결과를 내요.
E is T ? (T)(E) : (T)null
한 가지 차이는 E가 단 한 번만 평가된다는 점이에요.
as 연산자는 참조, 널 가능, 박싱, 언박싱 변환만 고려해요. 사용자 정의 변환에는 as 연산자를 쓸 수 없고, 그런 변환이 필요하다면 캐스트 식을 사용해야 해요.
as 연산자의 사용 예시를 볼게요.
IEnumerable<int> numbers = new List<int>(){10, 20, 30};
IList<int> indexable = numbers as IList<int>;
if (indexable != null)
{
Console.WriteLine(indexable[0] + indexable[indexable.Count - 1]); // output: 40
}
List<int>는 IList<int>를 구현하므로 변환이 성공하고, 첫 요소와 마지막 요소의 합인 40이 출력돼요.
[!NOTE] 위 예시에서 볼 수 있듯이
as식의 결과를null과 비교해서 변환이 성공했는지 확인해야 해요. 변환이 성공하는지 검사하고, 성공하면 그 결과를 새 변수에 할당하는 일을 동시에 하려면is연산자를 쓸 수 있어요.
캐스트 식
(T)E 형태의 캐스트 식은 표현식 E의 결과를 타입 T로 명시적으로 변환해요. E의 타입에서 T로 가는 명시적 변환이 없으면 컴파일러가 오류를 냅니다. 런타임에는 명시적 변환이 실패할 수도 있고, 캐스트 식이 예외를 던질 수도 있어요.
명시적 숫자 변환과 참조 변환 예시를 볼게요.
double x = 1234.7;
int a = (int)x;
Console.WriteLine(a); // output: 1234
int[] ints = [10, 20, 30];
IEnumerable<int> numbers = ints;
IList<int> list = (IList<int>)numbers;
Console.WriteLine(list.Count); // output: 3
Console.WriteLine(list[1]); // output: 20
double에서 int로 캐스트하면 소수부가 잘려서 1234가 되고, 배열을 IList<int>로 캐스트하면 인덱스와 Count로 접근할 수 있게 되는 걸 볼 수 있어요.
지원되는 명시적 변환에 대한 자세한 내용은 C# 언어 사양의 Explicit conversions 절을 참고하세요. 사용자 지정 명시적·암시적 타입 변환을 정의하는 방법은 사용자 정의 변환 연산자 문서에서 다뤄요.
()의 다른 용도
괄호는 메서드를 호출하거나 대리자를 호출할 때도 사용해요. 또 C#이 식에서 연산을 평가하는 순서를 조정할 때도 괄호를 쓰죠. 자세한 내용은 C# 연산자 문서를 참고하세요.
typeof 연산자
typeof 연산자는 타입에 대한 System.Type 인스턴스를 얻어요. typeof 연산자의 인자는 타입 이름이나 타입 매개변수여야 해요. 예시를 볼게요.
void PrintType<T>() => Console.WriteLine(typeof(T));
Console.WriteLine(typeof(List<string>));
PrintType<int>();
PrintType<System.Int32>();
PrintType<Dictionary<int, char>>();
// Output:
// System.Collections.Generic.List`1[System.String]
// System.Int32
// System.Int32
// System.Collections.Generic.Dictionary`2[System.Int32,System.Char]
typeof(List<string>)과 typeof(int)는 각각 List<string>과 int 타입의 Type 인스턴스를 돌려주고, typeof로 얻은 타입 이름이 그대로 출력되는 걸 볼 수 있어요.
그런데 인자에는 메타데이터 주석이 필요한 타입을 쓸 수 없어요. 예를 들면 이런 타입들이죠.
dynamicstring?(혹은 그 외 널 가능 참조 타입)
이런 타입들은 메타데이터에 직접 표현되지 않아요. 대신 기본 타입을 설명하는 특성(attribute)이 붙어 있죠. 두 경우 모두 기본 타입을 사용하면 돼요. dynamic 대신 object를, string? 대신 string을 쓰면 됩니다.
typeof 연산자는 바인딩되지 않은 제네릭 타입(unbound generic type)에도 사용할 수 있어요. 바인딩되지 않은 제네릭 타입의 이름에는 타입 매개변수 개수보다 하나 적은 수만큼의 쉼표가 들어가야 해요. 예시를 볼게요.
Console.WriteLine(typeof(Dictionary<,>));
// Output:
// System.Collections.Generic.Dictionary`2[TKey,TValue]
Dictionary<,>에서 쉼표 하나가 타입 매개변수가 두 개라는 뜻이죠. 그래서 Dictionary\2[TKey,TValue]`라는 이름이 나와요.
표현식은 typeof 연산자의 인자가 될 수 없어요. 표현식 결과의 런타임 타입에 대한 System.Type 인스턴스를 얻으려면 GetType() 메서드를 사용해야 해요.
typeof 연산자로 타입 검사하기
typeof 연산자를 쓰면 표현식 결과의 런타임 타입이 주어진 타입과 정확히 일치하는지 확인할 수 있어요. 아래 예시는 typeof 연산자를 쓴 타입 검사와 is 연산자를 쓴 타입 검사의 차이를 보여줘요.
public class Animal { }
public class Giraffe : Animal { }
public static class TypeOfExample
{
public static void Main()
{
object b = new Giraffe();
Console.WriteLine(b is Animal); // output: True
Console.WriteLine(b.GetType() == typeof(Animal)); // output: False
Console.WriteLine(b is Giraffe); // output: True
Console.WriteLine(b.GetType() == typeof(Giraffe)); // output: True
}
}
b에는 Giraffe 인스턴스가 들어 있어요. is Animal은 파생 관계를 따지므로 참이지만, GetType() == typeof(Animal)은 정확한 타입 일치를 요구하므로 거짓이에요. 즉 is는 "그 타입이거나 그 타입에서 파생됐는지"를, typeof 비교는 "정확히 그 타입인지"를 보는 차이죠.
연산자 오버로드 가능 여부
is, as, typeof 연산자는 오버로드할 수 없어요.
사용자 정의 타입은 () 연산자를 오버로드할 수 없지만, 캐스트 식이 수행하는 사용자 지정 타입 변환은 정의할 수 있어요. 자세한 내용은 사용자 정의 변환 연산자 문서를 참고하세요.
C# 언어 사양
자세한 내용은 C# 언어 사양의 다음 절을 참고하세요.