캐스팅과 형 변환은 식의 데이터 타입을 다른 데이터 타입으로 바꾸는 서로 다른 방법이에요.
C# 문서에서는 형 변환을 as 연산자나 is 연산자의 사용으로 분류해요. 캐스팅은 캐스트 연산자의 사용으로 정의해요.
C#에서는 숫자 값이나 클래스 계층 구조를 벗어나면, 문자열을 정수로 바꿔 주는 Int32.Parse()처럼 "변환 대상" 타입의 멤버를 호출하거나 object.ToString()처럼 "원래" 타입의 멤버를 호출해서 변환해야 하는 경우가 아주 많아요. Javascript를 비롯한 동적 언어에 익숙한 개발자라면 이 점을 알아 두는 게 좋아요.
암시적·명시적 캐스트 연산자(operator-overloading에서 다뤄요)를 정의해서 내 타입에 비교적 자유로운 캐스팅을 넣을 수 있다는 점도 기억해 둬요.
C#의 타입 시스템은 _C_나 Javascript보다 다소 엄격해서, 그만큼 캐스팅 연산에도 제약이 많아요. 암시적 캐스팅은 "변환 대상" 타입이 "원래" 타입 값의 크기와 부호를 그대로 보존할 수 있을 때 두 숫자 타입 사이에서 일어나요. 실수로 변환할 때 정밀도를 잃을 수 있는 경우가 문서에 예외로 적혀 있다는 점도 눈여겨봐요.
암시적 캐스트에는 별도의 문법이 필요하지 않아요. 예를 들어볼게요:
int myInt = 1729;
long myLong = myInt;
숫자(또는 문자열) 식을 bool로 암시적으로 변환할 수는 없어요. 이를 위해 BCL(Base Class Library)에서 Convert.ToBoolean()을 제공해요.
char 타입 식은 int로 암시적 캐스팅할 수 있어요. 반대 방향으로의 캐스팅은 반드시 명시적으로 해야 해요. int의 모든 값이 유효한 utf 16 문자인 것은 아니거든요.
숫자 타입을 암시적으로 캐스팅할 수 없을 때는 일반적으로 명시적 캐스트 연산자를 사용할 수 있어요.
캐스팅하려는 값을 "변환 대상" 타입이 표현할 수 없을 때, 즉 타입의 범위가 충분히 넓지 않거나 부호가 충돌할 때는 정수의 경우 오버플로 예외가 발생할 수 있고, 실수와 double의 경우 "변환 대상" 타입 변수가 Infinity 값을 가질 수 있어요.
int 타입 식은 char로 명시적 캐스팅할 수 있어요. 이때 유효하지 않은 char가 나올 수도 있어요.
int largeInt = Int32.MaxValue;
int largeNegInt = Int32.MinValue;
ushort shortUnsignedInt = ushort.MaxValue;
float largeFloat = float.MaxValue;
float smallFloat = 17.29f;
// implicit cast
int from_ushort = shortUnsignedInt; // 65535
float from_int = largeInt; // -21474836E+09
int from_char = 'a'; // 96
// explicit cast
uint from_largeInt = (uint)largeInt; // 2147483647
uint from_neg = (uint) largeNegInt; // 2147483648 or OverflowException is thrown (if checked)
int from_smallFloat = (int) smallFloat; // 17
int from_largeFloat = (int) largeFloat; // -2147483648 or OverflowException is thrown (if checked)
char from_intc = (char) 32; // ' '
char from_invalid_int = (char) 0xdcbf; // invalid char - no exception thrown
// no cast available
int fromString = Int32.Parse("42"); // 42
string toString = largeInt.ToString(); // "2147483647"
int fromString_bad = Int32.Parse("forty two"); // FormatException is thrown
checked 키워드에 대해서는 이 문서를 참고해요.
어떤 타입이든 암시적으로 기본 클래스나 인터페이스로 변환할 수 있어요.
IList<int> ll = new List<int>();
캐스트 연산자를 사용하면 기본 클래스에서 파생 클래스로 변환할 수 있어요.
IList<int> ll = new List<int>();
List<int> l = (List<int>)ll;
캐스팅하려는 식의 타입이 변환 대상 타입과 관련이 없어서 캐스팅에 실패하면 InvalidCastException 인스턴스가 발생해요.
이런 형 변환에는 is 키워드를 쓰는 방법이 더 흔해요:
interface IFoo { int Bar(); }
class Foo : IFoo { public int Bar() { return 42; }}
Object r = new Random();
IFoo ifoo = new Foo();
Object o = new Foo();
int result = 1729;
if (ifoo is Foo foo)
{
result = foo.Bar();
}
// result == 42
result = 1729;
if (o is Foo foo2)
{
result = foo2.Bar();
}
// result == 42
result = 1729;
if (r is Foo foo3)
{
result = foo3.Bar();
}
// result == 1729
as 키워드는 is와 비슷한 역할을 해요. 예를 들어 var foo = ifoo as Foo;처럼 써요. 이 예제에서 ifoo가 Foo 타입이 아니면 foo는 null이 되고, 그렇지 않으면 ifoo가 foo에 대입돼요.
타입은 자신만의 명시적·암시적 캐스트 연산자를 정의할 수 있어요.
BCL에 있는 명시적·암시적 캐스트의 예로는 BigInteger 구조체와 다른 숫자 타입 사이의 변환이 있어요.
typeof 사용하기객체의 정확한 타입을 알아내야 할 때는 is가 조금 너무 관대할 수 있어요. is는 객체를 어떤 클래스나 그 기본 클래스 중 아무 것으로나 변환해 주거든요. 이럴 때는 typeof와 Object.GetType()이 해답이에요.
object o = new List<int>();
o is ICollection<int> // true
o.GetType() == typeof(ICollection<int>) // false
o is List<int> // true
o.GetType() == typeof(List<int>) // true