본문 바로가기

.NET/C# Reflection

[C# Reflection] 런타임에 객체 생성하기

코드상에서는 단순히 MyClass obj = new MyClass()처럼 클래스의 객체를 생성할 수 있다.

런타임에서 어떤 클래스의 객체를 생성하고 싶을 땐 어떻게 해야 할까?

 

방법 1. Activator.CreateInstance()

인스턴스화 하고자 하는 클래스의 생성자 정보를 정확히 알고 있을 때 쓸 수 있는 방법이다. 제일 간단하다.

class MyClass
{
    public MyClass() {}
    public MyClass(int i) {}
}

static void Main(string[] args)
{
    var obj = Activator.CreateInstance(typeof(MyClass), 1);
}

Activator.CreateInstance(Type, params object[]) 메서드는 지정된 타입의 생성자에서 인수 목록이 가장 일치하는 생성자를 호출하여 객체를 생성해준다.

typeof 대신 아래와 같이 GetType()으로 타입을 가져다가 넣어도 된다.

static void Main(string[] args)
{
    var obj1 = new MyClass();
    
    var obj2 = Activator.CreateInstance(obj1.GetType());
}

위 코드는 MyClass의 기본 생성자를 호출하여 객체를 생성해줄 것이다.

 

방법 2. Type.GetConstructors()로 직접 찾아서 호출

생성자 정보를 나타내는 ConstructorInfo 개체들을 가져다가 직접 매개변수 리스트를 비교하여 원하는 생성자를 호출할 수 있다. 물론 이 방법은 Activator.CreateInstance()에 비해 신경 써야 할 부분이 많지만 private 생성자 등 코드로 직접 호출할 수 없는 생성자도 호출할 수 있는 등의 장점이 있다.

static object createInstance( Type t, params object[] args )
{
    var cons = t.GetConstructors( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance );

    // 원래 default형식 매개변수나 params, ref, out 등 이곳에서 고려할 사항이 많으나 여기서는 타입 비교만 한다.
    
    var ctorIndex = -1; // 호출 가능한 생성자의 인덱스
    for ( var i = 0; i < cons.Length; i++ )
    {
        var paramInfos = cons[i].GetParameters();

        // cons[i]의 매개변수 개수와 args에 지정된 값의 개수가 다른 경우 이 생성자는 호출할 수 있는 생성자가 아님.
        if ( paramInfos.Length != args.Length ) continue;

        bool invalid = false;
        for ( var j = 0; j < paramInfos.Length && !invalid; j++ )
        {
            // args[j]가 null인 경우 타입을 비교할 수 없으므로 매개변수의 타입이 값 형식인지 아닌지로 판단한다.
            if ( args[j] == null )
            {
                // 매개변수 타입이 값형식이라면 null을 대입할 수 없기에 호출 불가함.
                if ( paramInfos[j].ParameterType.IsValueType )
                {
                    invalid = true;
                }
                // 그렇지 않다면 타입과 상관 없이 null을 대입할 수 있기 때문에 OK.
            }
            // 두 타입이 서로 동일하다면 OK.
            else if ( args[j].GetType() == paramInfos[j].ParameterType )
            {

            }
            else
            {
                // TypeDescriptor.GetConverter()로 args[j]의 형식에 대한 타입 변환기를 가져온다.
                var converter = TypeDescriptor.GetConverter( args[j].GetType() );

                // 이 변환기로 args[j]의 값이 paramInfos[j]의 매개변수 형식으로 변환될 수 있는지 확인한다.
                if ( !converter.CanConvertTo( paramInfos[j].ParameterType ) )
                {
                    // 변환할 수 없으므로 continue
                    invalid = true;
                }
            }
        }

        if ( !invalid ) 
        {
        	if ( ctorIndex != -1 )
            {
            	// 이미 호출 가능한 생성자가 앞에서 1개 나왔음. = 생성자 모호함
                throw new AmbiguousMatchException( "지정된 인수로 호출할 수 있는 생성자가 2개 이상 존재합니다." );
            }
            else
            {
            	ctorIndex = i;
            }
        }
    }

    // 찾은 인덱스 번호의 생성자를 호출
    if ( ctorIndex != -1 )
    {
        // 호출 & 반환
        return cons[ctorIndex].Invoke( args );
    }
    else
    {
        throw new MissingMethodException( "지정된 인수와 일치하는 생성자를 찾을 수 없습니다." );
    }
}

이 코드는 타입 검사에 실패하는 예가 꽤 많을 것이다. (예를 들어 IList 형식과 string 형식을 모두 Collection으로 취급하여 TypeConverter가 변환 가능하다고 판단한다.)

다만 여러 가지 Reflection 기능의 사용 예시들을 보여주기 위해 함께 포스팅한다.