2018년 11월 2일 금요일

C# - 이름으로 객체 생성 2

밑에 C#에서 이름으로 객체를 생성하는 방법이 있습니다만, 가끔 이런 코드가 불가능한 경우가 있습니다. 특히 모바일용 Portable 코드를 사용할 때 말입니다.

using System.Reflection;

namespace ClassName
{
    internal class ClassA
    {
        internal void Write()
        {
            Console.WriteLine("ClassA");
        }
    }
    internal class ClassB
    {
        internal void Write()
        {
            Console.WriteLine("ClassB");
        }
    }
}

이런 상황에서는 대부분 Assembly의 GetExcutingAssembly 함수를 사용할 수가 없습니다.
이 경우에는 이런 식으로 사용할 수가 있죠.

    string className = "ClassName.ClassA";
    Type type = Type.GetType(className);
    if (type == null)
        throw new NotImplementedException();
    object obj = Activator.CreateInstance(type);
    var myClass = obj as SpObj.CompAI.AIBrain;
    if (myClass == null)
        throw new NotImplementedException();

2018년 6월 12일 화요일

C# - 이름으로 객체 생성

지난번에 이름으로 멤버변수 및 메서드에 접근하는 방법을 살펴보았습니다. 비슷한 방식으로 클래스 이름으로 객체를 만드는 방법 역시 존재합니다.


using System.Reflection;

namespace ClassName
{
    internal class ClassA
    {
        internal void Write()
        {
            Console.WriteLine("ClassA");
        }
    }
    internal class ClassB
    {
        internal void Write()
        {
            Console.WriteLine("ClassB");
        }
    }
}

이와 같은 두개의 클래스가 있습니다. 여기서 클래스 이름("ClassA" 또는 "ClassB"를 가지고 객체를 만들기 위해서는 다음과 같이 할 수 있습니다.

using System.Reflection;

namespace ClassName
{
    class Program
    {
        static void Main(string[] args)
        {
            Assembly creator = Assembly.GetExecutingAssembly();
            object obj = creator.CreateInstance("ClassName.ClassA");
            if (obj is ClassA)
                (obj as ClassA).Write();
            obj = creator.CreateInstance("ClassName.ClassB");
            if (obj is ClassB)
                (obj as ClassB).Write();
        }
    }
}

이와 같이 Assembly 객체를 만들고 나서 CreateInstance메서드로 객체를 생성할 수 있습니다. 단, 이 경우에 클래스는 반드시 namespace까지 포함하고 있는 이름이어야 합니다. 이 경우에 Program과 ClassA, ClassB가 같은 namespace에 있으므로 코드에서 객체를 만들 때는 ClassName을 생략할 수 있지만, 문자열로 객체를 만들 때는 생략할 수 없습니다.

또한 CreateInstance의 반환값은 object형이므로 is, as연산자를 통해 형변환 후 작업을 해야겠죠.

using System.Reflection;

namespace ClassName
{
    class Program
    {
        static void Main(string[] args)
        {
            string clsName = Console.ReadLine();
            Assembly creator = Assembly.GetExecutingAssembly();
            object obj = creator.CreateInstance("ClassName." + clsName);
            if (obj is ClassA)
                (obj as ClassA).Write();
            else if (obj is ClassB)
                (obj as ClassB).Write();
        }
    }
}

만약 이런 식으로 형을 하나하나 검사하기 힘들다면 상속관계를 이용할 수 있습니다.


using System.Reflection;

namespace ClassName
{
    internal abstract class ClassBase
    {
        internal abstract void Write();
    }
    internal class ClassA : ClassBase
    {
        internal override void Write()
        {
            Console.WriteLine("ClassA");
        }
    }
    internal class ClassB : ClassBase
    {
        internal override void Write()
        {
            Console.WriteLine("ClassB");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            string clsName = Console.ReadLine();
            Assembly creator = Assembly.GetExecutingAssembly();
            object obj = creator.CreateInstance("ClassName." + clsName);
            if (obj is ClassBase)
                (obj as ClassBase).Write();
        }
    }
}

2018년 5월 30일 수요일

C# - 이름으로 변수/메서드 접근

public class Exam
{
    public int korean;
    public int english;
    public int math;
}

public class ExamWrite
{
    public void Write(Exam exm, string subject)
    {
        if(subject == "korean")
            Console.WriteLine("korean : {0}", exm.korean);
        if(subject == "english")
            Console.WriteLine("english : {0}", exm.english);
        if(subject == "math")
            Console.WriteLine("math : {0}", exm.math);
    }
}

와 같은 프로그램이 있다고 해 봅시다. subject를 검색해서 같은 이름의 멤버변수값을 출력하는 프로그램이죠.
그런데 만약 과목이 수십개라면 어떨까요? 저런 if문이 수십개가 들어가야 합니다. 이럴 경우에는 문자열로 직접 변수이름을 가져올 수 있습니다. 이런 기능을 reflection이라고 합니다. 그러므로 다음과 같은 using문이 필요합니다.

using System.Reflection;

reflection 기능을 사용하기 위해서는 먼저 해당 객체의 타입을 필요로 합니다. typeof함수로서 Exam이라는 클래스의 정보를 얻어옵니다.

            Type tp = typeof(Exam);

다음에는 이 Exam이라는 클래스의 정보가 담긴 tp라는 객체에서 필드의 정보를 얻어와야겠죠. 필드의 정보를 얻기 위해서는 GetField라는 메서드를 사용합니다.
즉 다음 명령어는 subject에 담겨있는 문자열과 같은 이름의 멤버변수를 찾으라는 명령입니다.

            FieldInfo fld = tp.GetField(subject, BindingFlags.Instance |
                                                 BindingFlags.Static |
                                                 BindingFlags.Public |
                                                 BindingFlags.NonPublic);

여기서 BindingFlags는 각각 일반멤버변수, 정적멤버변수, 공용번수, 공용이 아닌 변수들을 모두 찾으라는 명령어죠. 이 외에 많은 플래그가 있지만, 가장 중요한 것은 이것들입니다. 필요한 것만 골라서 조합하면 됩니다.
만약 제대로 찾았다면 fld에는 그 변수에 대한 정보가 들어갑니다. 찾지 못했다면 null이 반환됩니다.
이 변수의 값을 얻기 위해서는

            object point = fld.GetValue(exm);

와 같이 할 수 있습니다. 위에서 찾은 subject라는 변수의 정보를 exm이라는 객체에서 찾아 그 값을 반환하라는 명령이죠. 다만 아직까지 이 필드의 정확한 타입을 알 수 없기에 반환값은 object형입니다. 하지만 이 명령어가 제대로 실행되었다면 (점수는 모두 정수형이므로) point에는 정수형 값이 들어가 있을 것입니다. 그러므로 값을 출력하기 위해서는

            if (point is int)
                Console.WriteLine("{0} : {1}", fld.Name, (int)point);

와 같이 쓸 수 있습니다.(이때 fld.Name에는 위에서 받아온 필드정보의 이름입니다)
즉,

using System.Reflection;

....

public class ExamWrite
{
    public void Write(Exam exm, string subject)
    {
        Type tp = typeof(Exam);
        FieldInfo fld = tp.GetField(subject, BindingFlags.Instance |
                                             BindingFlags.NonPublic |
                                             BindingFlags.Public |
                                             BindingFlags.NonPublic);
        object point = fld.GetValue(exm);
        if (point is int)
            Console.WriteLine("{0} : {1}", fld.Name, (int)point);
    }
}

와 같이 사용할 수 있죠.

그런데 이런 식으로 하면 한번에 하나의 성적만 출력 가능하죠. 모든 성적을 한번에 출력할 방법은 없을까요?
GetField가 필드 하나의 정보를 얻어오는 것이라면 GetFields는 모든 필드의 정보를 가져오는 메서드입니다. 그러므로 이 함수를 사용하면 모든 필드에 대한 조작을 할 수 있습니다.

using System.Reflection;

....

public class ExamWrite
{
    public void WriteAll(Exam exm)
    {
        Type tp = typeof(Exam);
        FieldInfo[] flds = tp.GetFields(BindingFlags.Instance |
                                        BindingFlags.Static |
                                        BindingFlags.Public |
                                        BindingFlags.NonPublic);
        foreach (var f in flds)
        {
            object point = f.GetValue(exm);
            if (point is int)
                Console.WriteLine("{0} : {1}", f.Name, (int)point);
        }
    }
}

마찬가지로 멤버변수에 값을 넣기 위해서는 FieldInfo의 GetValue 대신 SetValue를 사용할 수 있습니다.

using System.Reflection;

....

public class ExamWrite
{
    public void Perfect(Exam exm)
    {
        Type tp = typeof(Exam);
        FieldInfo[] flds = tp.GetFields(BindingFlags.Instance |
                                        BindingFlags.Static |
                                        BindingFlags.Public |
                                        BindingFlags.NonPublic);
        foreach (var f in flds)
        {
            object point = f.GetValue(exm);
            if (point is int)
                f.SetValue(exm, 100);
        }
    }
}

이것은 exm의 모든 변수값을 100점으로 만드는 함수가 되겠습니다.

만약 다음과 같이

public class Exam
{
    public const int korean;
    public const int english;
    public const int math;
}

와 같이 모두 const로 설정되어 있을 경우에도 이상없이 실행됩니다(다만 Perfect메서드에서는 const변수의 값을 바꾸려 시도하고 있으므로 TargetInvocationException이 뜹니다).

---------------------------

마찬가지로 메서드 역시 이름으로 호출이 가능합니다. 이 경우에는 GetField 대신 GetMethod를 사용할 수 있습니다.

        Type tp = typeof(ExamWrite);
        MethodInfo method = tp.GetMethod("WriteAll");

그리고 이 메서드를 호출하기 위해서는 Invoke 메서드를 사용할 수 있죠.

        method.Invoke(ew, new object[] { exm });

이것 역시 ew라는 객체를 통해 위에서 얻어온 "WriteAll"이라는 메서드를 호출하라는 뜻이죠. 이 메서드의 인수는 object의 배열을 통해 전달됩니다.

using System.Reflection;

....

        static void Main(string[] args)
        {
            Exam exm = new Exam();
            ExamWrite ew = new ExamWrite();

            Type tp = typeof(ExamWrite);
            MethodInfo method = tp.GetMethod("WriteAll");
            method.Invoke(ew, new object[] { exm });
        }

---------------------------

멤버변수나 메서드들은 모두 접근한정자(private, public 등)를 가지고 있습니다. 이 접근한정자는 컴파일시 확인을 하게 됩니다.
그러나 이 방식으로 멤버에 접근하는 것은 컴파일시가 아니라 실행시이기 때문에 접근한정자가 의미가 없습니다. 즉 private로 꼭꼭 숨겨놓은 멤버도 이 방식으로 접근하면 값을 읽어오거나 바꿀 수 있습니다. 이런 것을 주의해야 합니다.