[닷넷코어] 임의타입에 대한 임의의 형변환

개요

문자열을 숫자로 바꾸거나 그 반대를 행하는 경우 형 변환이 필요합니다.
일반적으로 형변환은 클래스형에 대한 변환을 의미하지는 않습니다. 클래스의 경우 보통 캐스팅이라고 하여 객체지향 프로그래밍의 근간인 대체가능성에 기반한 변환을 의미하게 됩니다.

형 변환은 왜 하는거야? 처음부터 그 형으로 만들면 되잖아.
코드 속의 값은 그 형으로 만들면 되지만, 외부에서 들어오는 값은 다양하니까.
쿼리스트링 같은 것들인가?
응. 원래 문자열에서 숫자로 바꾸려면 형 변환이 일어나.
그렇네!

IConvertible

CLR공용타입들(char, bool, byte, int 등)은 별도의 변환 방법을 제공해야 합니다. 이를 위한 내장 인터페이스가 System.IConvertible 입니다.

이 인터페이스에서는 ToChar, ToByte 등 CLR공용타입 전체에 대한 변환 메소드 구현을 요구하고 있습니다.
사실 이것만 구현하면 c#의 형변환에 참여할 수 있는 타입의 클래스를 제작할 수 있습니다. 이 경우의 인스턴스는 캐스팅이 아니라 형 변환이라고 봐야겠죠.
(실제 위의 레퍼런스 링크에서는 Complex라는 샘플 클래스를 통해 IConvertible의 구상을 보여주고 있습니다)

CLR공용타입이 전부 이 인터페이스를 구상하고 있으므로 이론적으로 모든 CLR공용타입은 다른 타입으로 변환시킬 수 있습니다.

IConvertible에 구현할 메소드가 너무 많아..
개발의 본질은 묵묵한 노가다라고 하는 사람도 있어 ^^
하핫, 전혀 괜찮아!

Convert

이러한 IConvertible에 기반한 형 간의 변환에 대한 자세한 지식을 은닉하고 유틸리티로 제공해주는 게 System.Convert입니다.

이를 이용하면 다음과 같이 편리하게 형을 변환할 수 있습니다.

var str = "1234"; //문자열을

var dbl = Convert.ToDouble(str); //더블로!

ToXXX 시리즈에 CLR공용타입 전체가 정의되어있고 각 메소드는 다시 인자로 모든 CLR공용타입을 받아들이는 노가다성 조합메소드를 제공합니다.
즉 ToInt32(bool), ToInt32(char), ToInt32(byte),.. 이런 식으로 각 메소드마다 받아들이는 인자도 CLR공용타입 전부가 준비되어 방대한 메소드를 갖고 있습니다.

하핫, 이것봐 MS도 노가다로 구현했어!
혼자한 건 아니지만 ^^;

ChangeType

하지만 근본적으로 IConvertible에 의존하므로 typeof를 통해 넘어온 값을 is IConvertible 로 검사해보면 손쉽게 IConvertible의 메소드로 변환하여 반환할 수 있습니다. 이런 처리를 하는 메소드가 ChangeType입니다.

var str = "1234"; //문자열을

var dbl = Convert.ChangeType(str, typeof(double)); //더블로!

하지만 위의 결과에서 dbl 의 타입을 실제로 체크해보면 double이 아니라 object가 됩니다.
Convert.ChangeType 메소드 입장에서 인자로 들어온 값은

  1. value is IConvertible로 확인하면
  2. 결국 CLR공용타입 중 하나이거나 최종적으로 ToType을 호출할 수 있으므로
  3. 하나하나 비교하면서 일치하는 타입으로

변환하면 됩니다. 하지만 메소드의 반환타입은 하나 뿐이므로 object로 형변환하여 반환하게 됩니다. 가볍게 Convert의 ChangeType을 구현하면 다음과 같을 것입니다(.net core의 실제 코드도 대동소이합니다)

class Convert{
  static object ChangeType(object value){

    //IConvertible 이 아니면 정리
    if(!(value is IConvertible)) throw;

    //들어온 값의 타입
    var type = value.GetType();

    //미리 형변환해 둔 IConvertible
    var v = (IConvertible)value;
    
    //국제화기준
    var c = CultureInfo.CurrentCulture;

    //CLR공용타입과 하나씩 비교
    if(type == typeof(int)) return (object)v.ToInt32(c.NumberFormat);
    else if(type == typeof(long)) return (object)v.ToInt64(c.NumberFormat);
    //....CLR공용타입 전체검사 및 매칭되는 메소드 호출결과
  }
}

ChangeType 변환 함수는 하나의 함수가 모든 인자에 대응할 수 있으므로 각 형에 맞춰 ToXXX 를 부르는 것보다 편리합니다.
내부에서 object로 넘어온 인자를 평가해주기 때문입니다.
하지만 반대로 반환되는 형이 언제나 object라서 호스트측에서 다시 변환해야 합니다. 이를 코드로 비교해보죠.

//ToXXX를 사용
var str = "1234";

int i32 = Convert.ToInt32(str); 
long i64 = Convert.ToInt64(str); 
double dbl = Convert.ToDouble(str); 

결국 반환 형이 딱 떨어져 나오기 때문에 반환 값에 대한 형 변환이 없는 대신 원하는 메소드를 매번 지정해야 합니다.

//ChangeType를 사용
var str = "1234";

int i32 = (int)Convert.ChangeType(str, typeof(Int32)); 
long i64 = (long)Convert.ChangeType(str, typeof(Int64)); 
double dbl = (double)Convert.ChangeType(str, typeof(Double)); 

ChangeType이라는 메소드명은 하나로 유지되지만 인자로 타입을 보내야 할 뿐만 아니라 반환 값도 다시 형변환 해야 하는 상황입니다. 동일 메소드를 사용하기 위해 치루는 댓가가 너무 큰 거죠.

왜 하나의 메소드로 처리하는게 장점인거야? 그냥 상황에 맞춰서 ToXXX형으로 쓰면 되잖아.
맞아. 하지만 함수로 감싸거나 하면 메소드가 동일해야 코드를 일반화할 수 있거든
어떤 함수 내부에서 ToString을 호출할지 ToInt32를 호출할지 모른다는거야?
제네릭이나 직접 인자로 그 힌트는 줄 수 있지만 결국 코드는 메소드 이름이 다르니까 if로 분리할 수 밖에 없어.
어려워..
^^;;

제네릭을 사용한 해법

둘의 불편함을 덜고 변환하고 싶은 인자로 간단히 전달하면서 원하는 형을 받기 위해 제네릭의 도움을 받아보죠. 간단한 와꾸를 짜보면..

class Util{
  public static T To<T>(object value){
  }
}

정도로 생각해볼 수 있습니다. 이걸 사용하는 호스트 코드는

int i32 = Util.To<int>("1234");
long i64 = Util.To<long>("1234");
double dbl = Util.To<double>("1234");

정도로 정리할 수 있습니다. 훨씬 간결하면서도 반환 형이 확정적으로 들어오게 되었습니다.
제법 깔끔해졌으니 내부를 채워보죠.

제네릭을 통해 넘어오는 T의 경우 자바에서는 슈퍼타입토큰의 요령을 사용하지 않으면 타입을 얻을 수 없습니다. 하지만 c#은 바로 T에 대한 참조를 얻을 수 있으므로 코드는 매우 간단히 해결됩니다.

class Util{
  public static T To<T>(object value){
    return (T)Convert.ChangeType(v, typeof(T));
  }
}

이제 n타입에 대응하여 m타입을 반환할 수 있는 제네릭 기반의 변환 함수를 사용할 수 있게 되었습니다.

아까의 상황을 코드로 표현해줘.
음, 예를 들어 두 개의 숫자를 받아들여 합을 구하는 함수를 생각해보기로 해.
걍 +로 덧셈하면 되잖아
아니 원하는 형으로 변환해서 더하는 기능을 제공하려 해. 그럼 원래 값이 3.3F와 10인 경우 int로 더하면 13이 되고 float로 더하면
13.3F 가 될거야.
아 그거라면 그냥 더하기로는 안되겠군.
먼저 시그니처를 제네릭으로 짜보면 다음과 같을 거야.
class Util{
  public static T Plus<T>(object a, object b){
      //a와 b를 T로 변환한 뒤 더한 결과를 T형으로 반환한다.
  }
}
호오..제네릭..
만약 ToXXX메소드를 쓴다면 다음과 같이 작성될거야.
class Util{
  public static T Plus<T>(object a, object b){

      //T의 형을 얻는다
      var type = typeof(T); 

      //각 T에 따라 if를 생성한다.
      if(T == typeof(Int32)) return Convert.ToInt32(a) + Convert.ToInt32(b);
      if(T == typeof(Int64)) return Convert.ToInt64(a) + Convert.ToInt64(b);
      //...CLR공용타입 전부 짠다..
  }
}
어, 정말 ToXXX를 쓰면 타입만큼 if로 나눠줄 수 밖에 없구나. 그럼 이걸 어떻게 개선해?
이제 Util.To<>가 있으니 이걸 응용하면 간단해져.
class Util{
  public static T Plus<T>(object a, object b){
      return (T)((dynamic)Util.To<T>(a) + (dynamic)Util.To<T>(b));
  }
}
헐! 진짜?
if가 개입했던 이유는 코드 상에서 메소드의 이름이 다르기 때문이었으니까 ^^;
dynamic이 개입해서 기분이 꿉꿉해
T만으로는 +연산자에 어떻게 대응할지 컴파일러는 알 수 없으니까..^^;
그나저나 이름이 같다는 건 굉장한 거구나.

결론

개발 시 가장 기초 상황인 형 변환에 대해 다뤄봤습니다.
IConvertible를 근간에 두는 CLR공용타입 간 변환에 대한 기본적인 정책을 이해하고 Convert를 응용하여 보다 간편하게 형 변환 결과를 얻을 수 있었습니다.