C#/C# 기본

[C#] 리플렉션과 애트리뷰트

로파이 2021. 10. 19. 13:54

C#에서 제공하는 형식의 메타 정보를 의미하며 이 형식 정보를 이용하여 직접 개체를 생성하거나 프로퍼티를 사용, 메서드를 호출할 수 있다.

 

Object의 GetType() 메서드

모든 개체의 공통 조상인 Object에는 GetType() 메서드를 가지고 있다. 이 메서드가 반환하는 것은 해당 타입 정보를 가지고 있는 Type 이라는 클래스의 인스턴스이다.

int a = 0;

Type type = a.GetType();
FieldInfo[] fields = type.GetFields();

foreach (FieldInfo field in fields)
	Console.WriteLine($"Type : {field.FieldType.Name}, Name : {field.Name}");

위와 같이 Type 클래스는 a라는 인스턴스의 타입에 대한 모든 정보를 가지고 있다. 생성자, 필드, 프로퍼티, 메서드, 상속하는 인터페이스 등을 GetXXX()을 통해 가져올 수 있다. 이러한 Type의 정보를 가져오기 위해 System.Reflection 네임스페이스를 포함시킨다.

 

Type을 통해 가져올 수 있는 정보

메서드 반환 형식 설명
GetConstructors() ConstructorInfo[] 모든 생성자 목록을 반환한다.
GetFields() FieldInfo[] 모든 필드 목록을 반환한다.
GetInterfaces() Type[] 상속하는 모든 인터페이스 목록을 반환한다.
GetProperties() PropertyInfo[] 모든 프로퍼티 목록을 반환한다.
GetMethods() MethodInfo[] 모든 메서드 목록을 반환한다.
GetMembers() MemberInfo[] 모든 멤버 목록을 반환한다.

 

BindingFlag

필드나 메서드의 경우 접근자가 public/private/static 등으로 다양할 수 있다. GetFields()나 GetMethods()의 기본 행동은 public 공개되어있는 것들만 가져오는데, 해당 함수에서 BindingFlag.Public / BindingFlag.NonPublic / BindingFlag.Static / BindingFlag.Instance를 지칭하여 가져오는 대상을 지칭할 수 있다.

// 공개 인스턴스 필드
var public_instance_fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
// 비공개 인스턴스 필드
var private_instance_fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
// 공개 정적 필드
var public_static_fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
// 비공개 정적 필드
var private_static_fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static);

 

Object.GetType() 메서드를 사용하지 않고 형식 정보를 얻기

Object.GetType() 메서드는 반드시 인스턴스가 있어야 호출가능하다.

1) typeof(형식) 이용하기

Type a = typeof(int);
Console.WriteLine(a.FullName);

2) Type 클래스의 GetType(string typeName) 이용하기

네임스페이스를 포함한 전체 이름을 전달한다.

Type b = Type.GetType("System.Int32");
Console.WriteLine(b.FullName);

 

리플렉션을 이용한 객체의 생성과 사용

객체의 생성

System.Activator 클래스의 정적 메서드 CreateInstance(Type type)를 사용한다.  혹은 CreateInstance<T>()를 사용.

object a = Activator.CreateInstance(typeof(int));
List<int> list = Activator.CreateInstance<List<int>>();

Profile 클래스

    class Profile
    {
        private string name;
        private string phone;
        public Profile()
        {
            name = ""; phone = "";
        }
        public Profile(string name, string phone)
        {
            this.name = name;
            this.phone = phone;
        }

        public void Print()
        {
            Console.WriteLine($"{name}, {phone}");
        }

        public string Name { get { return name; } set{ name = value; } }
        public string Phone { get { return phone; } set { phone = value; } }
    }

 

프로퍼티 정보 개체 (PropertyInfo)를 이용하여 해당 클래스의 프로퍼티를 수정

GetProperty(string propertyName) 을 이용하여 PropertInfo 인스턴스를 가져오고 SetValue나 GetValue로 프로퍼티 값을 수정하거나 읽을 수 있다. SetValue의 세번째 인자는 인덱서를 위한 인덱스 매개변수이다.

            Type type = Type.GetType("DynamicInstance.Profile");
     
            object profile = Activator.CreateInstance(type, "김아무개", "010-2333-2211");
            profile = Activator.CreateInstance(type);

            PropertyInfo nameProperty = type.GetProperty("Name");
            PropertyInfo phoneProperty = type.GetProperty("Phone");

            nameProperty.SetValue(profile, "박찬호", null);
            phoneProperty.SetValue(profile, "992-020202", null);

메서드 정보를 담는 MethodInfo를 이용하여 해당 클래스의 메서드를 호출

GetMethod(string methodName)을 이용하여 MethodInfo 인스턴스를 가져오고 Invoke를 통해 함수를 호출한다. Invoke 함수는 첫번째 인자에 호출할 인스턴스와 매개변수를 전달한다.

MethodInfo methodInfo = type.GetMethod("Print");

methodInfo.Invoke(profile, null);

Console.WriteLine($"{nameProperty.GetValue(profile, null)}, {phoneProperty.GetValue(profile, null)}");

 

형식 내보내기

C#에서는 System.Reflection.Emit 네임스페이스를 포함하여 동적으로 클래스를 만들어서 새로운 모듈을 컴파일한 모듈을 만들어낼 수 있다.

 

순서

1) 코드 정의를 담는 어셈블리를 만든다.

2) 어셈블리안에 모듈을 만들어 담는다.

3) 모듈안에 정의할 클래스를 만든다.

4) 클래스에 각종 필드, 프로퍼티, 메서드를 만들고 메서드의 경우 CPU가 실행할 코드를 ILGenerator로 만들 수 있다.

5) 클래스 타입을 완성하고 CLR에 제출한다.

 

이렇게 생성한 타입을 새롭게 정의한 클래스처럼 사용할 수 있다.

예시)

        static void Example2()
        {
            // 1. 어셈블리를 만든다.
            AssemblyBuilder newAssemby = AssemblyBuilder.DefineDynamicAssembly(
                                        new AssemblyName("CalculatorAssembly"), AssemblyBuilderAccess.Run);

            // 2. 모듈을 만든다.
            ModuleBuilder newModule = newAssemby.DefineDynamicModule("Calculator");

            // 3. 클래스를 만든다.
            TypeBuilder newType = newModule.DefineType("Sum1To100");

            // 4. 메소드를 만든다.
            MethodBuilder newMethod = newType.DefineMethod("Calculate", MethodAttributes.Public, typeof(int), new Type[0]);

            // 5. 실행할 IL 명령어를 넣는다.
            ILGenerator generator = newMethod.GetILGenerator();

            generator.Emit(OpCodes.Ldc_I4, 1); // 32 bit 정수(1)를 계산 스택에 넣는다.

            for(int i =2; i <= 100; ++i)
            {
                generator.Emit(OpCodes.Ldc_I4, i); // 32 bit 정수(i)를 계산 스택에 넣는다.
                generator.Emit(OpCodes.Add); // 계산 후 계산 스택에 담겨있는 두개의 값을 더한 후 그 결과를 다시 계산 스택에 넣는다.                          
            }

            generator.Emit(OpCodes.Ret); // 계산 스택에 담겨 있는 값을 반환한다.

            // 6 클래스를 CLR에 제출한다.
            newType.CreateType();

            // 1 ~ 100을 더하는 클래스를 이용한 계산
            object sum1To100 = Activator.CreateInstance(newType);
            MethodInfo Calculate = sum1To100.GetType().GetMethod("Calculate");
            Console.WriteLine(Calculate.Invoke(sum1To100, null));
        }

 

 

애트리뷰트 Attribute

클래스/프로퍼티/필드/메서드에 메타 정보를 추가하는 기능이다.

[애트리뷰트_이름( 애트리뷰트_매개변수)]로 사용하며 애트리뷰트도 System.Attribute를 상속한 클래스이며 타입 Type 정보에 포함된다.

 

컴파일러 경고를 나타내는 Obsolete 애트리뷰트

class MyClass
{
	[Obsolete("MyMethod is Deprecated")]
	public void MyMethod()
	{
		Console.WriteLine("Deprecated Method");
	}
	public void NewMethod()
	{
		Console.WriteLine("OK");
	}
}

호출자 정보 애트리뷰트

C/C++에서 제공하는 __FILENAME__, __LINE__, __FUNCTION__ 등의 매크로 기능이 C# 5.0 이상부터 애트리뷰트를 사용하여 사용할 수 있다. System.Runtime.CompilerServices 네임스페이스에 포함되어 있다.

public static class Trace   
{
public static void Method(string message,
				[CallerFilePath] string file = "",
				[CallerLineNumber] int line = 0,
				[CallerMemberName] string member = "")
	{
		Console.WriteLine("{0}(Line:{1}) {2} : {3}", file, line, member, message);
	}
}
  • CallerFilePath : 호출된 메서드가 포함된 소스 파일의 전체 경로
  • CallerLineNumber : 소스 파일에서 호출된 메서드의 행 번호
  • CallerMemberName : 소스 파일에서 호출된 메서드 혹은 프로퍼티 이름을 나타낸다.

애트리뷰트로 수식한 매개변수는 사용자가 해당 매개변수를 전달하지 않아도 된다.

 

새로운 애트리뷰트를 정의하고 사용하기

애트리뷰트는 System.Attribute를 상속하는 클래스이므로 이를 상속하여 새로운 애트리뷰트를 정의할 수 있다.

        [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
        class History : System.Attribute
        {
            private string writer;

            public string Date { get; set; }

            public string Notes { get; set; }

            public History(string writer)
            {
                this.writer = writer;
                Date = "2021.10.10";
                Notes = "First Release";
            }

            public string Writer
            {
                get {return writer; }
            }
        }

        [History("Sean", Date = "2021.10.10", Notes = "Created MyClass2")]
        [History("Ahn", Date = "2021.10.12", Notes = "Added Func() Method")]
        class MyClass2        {
            public void Func() { }
        }

System.AttributeUsage를 통해 애트리뷰트 클래스에도 애트리뷰트를 만들 수 있다. 해당 애트리뷰트는 클래스를 설명하고 둘 이상의 설명 애트리뷰트를 사용하기 위해 AllowMultiple = true를 지정하여 사용하고 있다.

'C# > C# 기본' 카테고리의 다른 글

[C#] 파일 입출력  (0) 2021.10.21
[C#] dynamic 타입  (0) 2021.10.19
[C#] LINQ  (0) 2021.10.18
[C#] 람다식  (0) 2021.10.15
[C#] 대리자와 이벤트  (0) 2021.10.15