본문 바로가기

.NET/C# Reflection

[C# Reflection] GetValue()와 SetValue() 메서드

지난 글에서 GetField(), GetFields() 메서드를 통해 필드 정보를 FieldInfo 개체 형식으로 받아오는 방법을 알아봤다.

 

[C# Reflection] GetField(), GetFields() 메서드와 FieldInfo 클래스

Type 클래스에 대해 GetField() 또는 GetFields() 메서드를 사용해서 타입에 속한 필드의 정보를 가져올 수 있으며 필드의 정보는 FieldInfo 클래스의 인스턴스 형태로 반환된다. 오늘은 이들에 대해 알아

cs-solution.tistory.com

이번에는 가져온 FieldInfo 개체를 사용해서 필드의 값을 읽거나 쓰는 방법을 배워본다.

 

1. GetValue(object) - 필드(또는 속성)의 값을 가져오기

class MyClass
{
	public int Field_A;
    
    public MyClass()
    {
    	Field_A = 0;
    }
    public MyClass(int a)
    {
    	Field_A = a;
    }
}

static void Main(string[] args)
{
	var obj = new MyClass(5);
    
    Console.WriteLine($"Field_A : {obj.GetType().GetField("Field_A").GetValue(obj)}");
}

GetValue()의 매개변수는 object 형식 1개인데, 이곳에 값을 읽어올 필드를 포함하는 객체를 직접 전달한다.

위 코드에서는 읽고자 하는 필드인 Field_A를 실제로 포함하고 있는 obj를 직접 전달했다.

 

obj.GetType().GetField()... 이런 식으로 obj부터 시작된 코드에 obj를 다시 매개변수로 전달해야 하니 이상하게 느껴질 수도 있다. 실제로는 obj.GetType()은 obj에서 obj의 상위 개념인 Type 개체를 반환하는 것으로, Type을 반환받는 순간 실제 인스턴스인 obj와의 연결은 끊어지게 된다.

좀 더 쉽게 얘기해보면 obj.GetType()을 호출하면 obj의 형식을 나타내는 MyClass 타입이 반환될 것이고, MyClass 타입은 obj가 실제로 가지고 있는 값과는 연결고리가 없다. 즉, GetType()을 통해 설계도를 가져오는 셈이다.

가져온 Type(= 설계도)에서 GetField()로 우리가 원하는 필드(= 위치)를 찾고, 필드에서 값을 읽어오기 위해 인스턴스(= 설계도를 기반으로 생성된 실물)를 매개변수로 전달하면 설계도 상의 위치는 실물에서도 존재해야 하기 때문에 설계도 상 위치와 동일한 실물 상 위치로부터 값을 읽어온다고 생각하면 이해하기 쉽다.

 

GetValue()의 반환형은 object로, 실제 필드가 가진 값을 object 타입으로 박싱하여 돌려주기 때문에 필요에 따라 캐스팅하여 사용하면 된다. 위 코드에서는 값을 그저 문자열로 출력하면 되기 때문에 굳이 형변환을 하지 않았다.

 

2. SetValue(object, object) - 필드(또는 속성)의 값을 설정하기

SetValue() 메서드는 매개변수가 2개이다. 첫 번째 매개변수는 GetValue() 메서드와 동일하게 필드를 포함하는 개체를 전달하고, 두 번째 매개변수로는 설정할 값을 전달한다.

class MyClass
{
	public int Field_A;
    
    public MyClass()
    {
    	Field_A = 0;
    }
    public MyClass(int a)
    {
    	Field_A = a;
    }
}

static void Main(string[] args)
{
	var obj = new MyClass(10);
    
    // obj.Field_A의 값을 10으로 설정
    obj.GetType().GetField("Field_A").SetValue(obj, 10);
}

SetValue() 메서드는 GetValue() 메서드보다 사용시 주의해야 할 점이 많다. SetValue() 메서드의 대부분의 문제는 런타임에서 발생하는데, 설정할 값을 object 형식으로 받아가기 때문에 컴파일 시간에서 오류를 잡아내기 불가능하며(object 형식은 모든 형식의 최상위 형식이기 때문에 어떠한 형식도 object 형식으로의 박싱이 가능하다.), 오류가 발생했다면 매우 높은 확률로 형변환 과정에서 발생하는 문제일 것이다. 따라서 SetValue() 메서드에서 문제가 발생했다면 값을 설정하려는 필드의 형식과 매개변수로 전달하는 값의 형식이 일치하는지 다시 한번 검토해보자.

 

만일 여러분이 사용자로부터 입력받은 값이나 파일로부터 읽어온 값으로 필드의 값을 설정하려고 한다면, 다음과 같은 문제를 해결할 필요가 있을 것이다.

입력받은 값 또는 읽어온 값은 문자열이다. 하지만 필드의 형식은 반드시 문자열은 아닐 것이다. 그렇다면 문자열을 필드의 형식에 맞게 적절히 형변환을 해줄 필요가 있다. 어떻게 형변환할 것인가?

class MyClass
{
	public int Field_A;
}

static void Main(string[] args)
{
	var obj = new MyClass();
    
	Console.Write("변경할 값을 입력하세요 : ");
    
    var value = Console.ReadLine();
    
    // 입력받은 값 value는 문자열이고, 값을 설정해야 하는 필드 Field_A는 int형이다.
    // 따라서 string을 int로 변환하기 위해 int.Parse()를 써본다.
    obj.GetType().GetField("Field_A").SetValue(obj, int.Parse(value));
}

위 코드에서는 입력 받은 문자열을 int.Parse() 메서드를 사용해서 int 형식으로 형변환하여 전달했다.

만약 여기서 double, float 등 형식이 추가되고, 설정할 필드의 형식이 위처럼 int라고 미리 알고 작성하는 것이 아니라면 어떻게 해야 할까?

 

class MyClass
{
	public int Field_A;
    public double Field_B;
    public float Field_C;
}

static void Main(string[] args)
{
	var obj = new MyClass();
    
    Console.Write("변경할 필드의 이름을 입력하세요 : ");
    var field = obj.GetType().GetField(Console.ReadLine());
    
    if(field != null)
    {    
		Console.Write("변경할 값을 입력하세요 : ");
    
    	var line = Console.ReadLine();
        object value;
        
        if(field.FieldType == typeof(int))
        {
        	value = int.Parse(line);
        }
        else if(field.FieldType == typeof(double))
        {
        	value = double.Parse(line);
        }
        else if(field.FieldType == typeof(float))
        {
        	value = float.Parse(line);
        }
        else 
        {
        	value = null;
        }
        
        if(value != null)
        {
        	field.SetValue(obj, value);
            Console.WriteLine($"값을 변경했습니다. {field.Name} = {field.GetValue(obj)}");
		}
        else
        {
        	Console.WriteLine("값을 변경하지 못했습니다. 변환이 지원되지 않는 형식입니다.");
        }
    }
    else
    {
    	Console.WriteLine("존재하지 않는 필드입니다.");
    }
}

위 코드에서는 값을 설정하려는 필드의 형식(FieldType)에 따라 다른 파싱 메서드를 호출했다. 그러나 이렇게 가능할 수 있는 타입에 따라 일일이 코드를 작성하는 데는 한계가 있다. 따라서 앞으로는 Convert.ChangeType() 메서드를 사용하자.

 

class MyClass
{
	public int Field_A;
    public double Field_B;
    public float Field_C;
}

static void Main(string[] args)
{
	var obj = new MyClass();
    
    Console.Write("변경할 필드의 이름을 입력하세요 : ");
    var field = obj.GetType().GetField(Console.ReadLine());
    
    if(field != null)
    {    
		Console.Write("변경할 값을 입력하세요 : ");
    
    	var line = Console.ReadLine();
        try
        {
        	object value = Convert.ChangeType(line, field.FieldType);

        	field.SetValue(obj, value);
            Console.WriteLine($"값을 변경했습니다. {field.Name} = {field.GetValue(obj)}");
		}
        catch
        {
        	Console.WriteLine("값을 변경하지 못했습니다. 변환이 지원되지 않는 형식입니다.");
        }
    }
    else
    {
    	Console.WriteLine("존재하지 않는 필드입니다.");
    }
}

Convert.ChangeType(object, Type) 메서드는 미리 정의된 상호 변환 가능한 타입들에 대해 적절한 형변환을 수행해 줄 것이다. 적절한 변환이 구현되지 않은 타입 간의 변환을 시도하는 경우 InvalidCastException, FormatException 등의 예외를 발생시키므로 try-catch 구문으로 예외를 잡아서 처리해주는 것을 잊지 말자.

 

필드가 값-형식 타입인 경우 SetValue() 메서드에 설정할 값을 전달할 때 캐스트 연산자를 사용하여 캐스팅하는 것은 썩 좋은 방법은 아니다. 만약 여러분이 uint 형식 필드에 값을 설정하기 위해 아래와 같이 사용했다면 프로그램은 오류를 발생시킬 것이다.

class MyClass
{
	public uint Field_A;
}

static void Main(string[] args)
{
	var obj = new MyClass();
    
    var value = 10;
    
    // 아래 구문은 예외를 발생시킨다.
    obj.GetType().GetField("Field_A").SetValue(obj, value);
    
    // 'Int32 형식을 UInt32 형식으로 변환할 수 없다.'는 예외 메시지에 여러분은 아래처럼 코드를 수정한다.
    obj.GetType().GetField("Field_A").SetValue(obj, (uint)value);
    
    // 그러나 여전히 같은 예외가 발생한다.
}

위 오류의 원인은 int 형식이 박싱된 object 형식을 uint 형식으로 직접 형변환하려고 시도했기 때문이다.

박싱 된 형식을 언박싱할 때는 박싱 되기 이전의 형식으로만 언박싱이 가능하다.

 

박싱과 언박싱에 대한 자세한 설명은 아래 링크를 참고하자.

 

Boxing 및 Unboxing - C# 프로그래밍 가이드

C# 프로그래밍의 boxing 및 unboxing에 대해 알아봅니다. 코드 예제를 살펴보고 사용 가능한 추가 리소스를 확인합니다.

docs.microsoft.com

따라서 위 코드를 올바르게 동작하도록 수정하려면 크게 아래 세 가지 방법이 있다. 어떤 방법이 가장 좋은 방법인지는 여러분도 잘 알 것이다.

1. var value = (uint)10;

    -> value를 uint를 박싱하도록 한다.

2. obj.GetType().GetField("Field_A").SetValue(obj, (uint)(int)value);

    -> value를 int로 언박싱 한 후 uint로 캐스팅한다.

3. Convert.ChangeType()

    -> Convert.ChangeType()을 사용해서 FieldInfo.FieldType 형식으로 형변환한다.