레이블이 C#인 게시물을 표시합니다. 모든 게시물 표시
레이블이 C#인 게시물을 표시합니다. 모든 게시물 표시

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();
        }
    }
}

다만 namespace 안에 있을 때와 클래스 안에 중복된 클래스에는 이런 차이가 있습니다.

using System.Reflection;

namespace SpaceName
{
    internal class ClassA
    {
    }
}
internal class ClassName
{
    internal class ClassB
    {
    }
}

object a = creator.CreateInstance("SpaceName.ClassA");
object b = creator.CreateInstance("ClassName+ClassB");


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로 꼭꼭 숨겨놓은 멤버도 이 방식으로 접근하면 값을 읽어오거나 바꿀 수 있습니다. 이런 것을 주의해야 합니다.

2016년 11월 15일 화요일

C# .. is

다음 프로그램을 봅시다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class BaseClass
    {
    }

    public class DerivedClass : BaseClass
    {
    }

    class Program
    {
        static void Main(string[] args)
        {
            BaseClass b = new BaseClass();
            BaseClass d = new DerivedClass();
            Console.WriteLine("b is BaseClass : {0}", b is BaseClass);
            Console.WriteLine("b is DerivedClass : {0}", b is DerivedClass);
            Console.WriteLine("d is BaseClass : {0}", d is BaseClass);
            Console.WriteLine("d is DerivedClass : {0}", d is DerivedClass);
        }
    }
}

실행하면 결과는 다음과 같습니다.

b is BaseClass : True
b is DerivedClass : False
d is BaseClass : True
d is DerivedClass : True

즉,

if(b is DerivedClass)
   .....



if(b as DerivedClass != null)
   .....

과 동일한 식이죠.


그러므로 obj라는 객체가 BaseClass인지 DerivedClass인지 알기 위해서는

if(obj is BaseClass)
    Console.WriteLine("obj is BaseClass");
else
    Console.WriteLine("obj is DerivedClass");

이것은 DerivedClass도 BaseClass이므로 항상 true가 됩니다.

그러므로

if(obj is DerivedClass)
    Console.WriteLine("obj is DerivedClass");
else
    Console.WriteLine("obj is BaseClass");

이렇게 해야 제대로 동작하게 되죠.

2016년 11월 8일 화요일

OOP 추상화(Abstraciton)

OOP의 특징 중 하나가 추상화(Abstraciton)입니다. 그런데 이 추상화가 뭘까요? 피카소의 그림일까요?

'추상화'란 것은, 어떤 기능을 가진 객체를 구현할때, 객체 외부에서는 그 기능의 정확한 묘사를 하지 않는 것입니다. 그 대신 그 기능의 '추상적 행위'를 묘사해야 합니다.

이를테면 다음과 같은 잠수함을 구현한다고 해 봅시다.

class Submarine
{
    private int x;    // x좌표
    private int y;    // y좌표
    private int depth;// 깊이

    public int GetX() { return x; }
    public int SetX(int x) { this.x = x; }
    ...........
}

void Function(Submarine ship)
{
    // 북쪽으로 이동
    ship.SetY(ship.GetY() - 1);


    // 수면으로 이동
    if(ship.GetDepth() > 0)
        ship.SetDepth(ship.GetDepth() - 1);
}

이런 식으로 해도 동작은 하겠죠. 하지만 이런 구현을 한다면 구태여 OOP 프로그램을 사용할 필요가 없습니다. 그냥 C로도 구현되는 수준이거든요. 오히려 C로 하면 저런 잡다한 멤버함수를 만들 필요도 없이 쉽게 끝납니다(하지만 그냥 C 프로그램에서도 이런 식의 프로그램은 좋지 않습니다*).

이것을 제대로 추상화를 구현한다면 이런 식이 되어야 합니다.

class Submarine
{
    private int x;    // x좌표
    private int y;    // y좌표
    private int depth;// 깊이

    public void ToEast() { ++x; }
    public void ToWest() { --x; }
    public void ToSouth() { ++y; }
    public void ToNorth() { --y; }

    public void Float()
    {
        if(depth > 0)
           --depth;
    }

    public void Sink() { ++depth; }
}

void Function(Submarine ship)
{
    // 북쪽으로 이동
    ship.ToNorth();

    // 수면으로 이동
    ship.Float();
}


이런 식으로 Submarine 밖에서는 Submarine의 멤버변수 자체를 생각하지 않고 코딩하는 것이 '추상화'입니다. x니 y니 따위를 생각하지 않고 오로지 '북쪽으로 이동'만 코딩하는 것이죠. '구체화'는 Submarine.ToNorth() 안에서 하고 말입니다.
캡슐화(Capsulation)는 멤버변수에 직접 접근이 힘들 뿐, 실제 멤버변수가 뭐가 있는지 알 수는 있다는 점에서, 추상화는 캡슐화보다 한단계 더 진행한 개념이라고 볼 수 있죠.

만약

class Ocean
{
    ......

    public bool IsReef(int x, int y)    // 암초가 있는가
    {
        .....
    }
}

class Submarine
{
    private int x;    // x좌표
    private int y;    // y좌표
    private int depth;// 깊이

    public int GetX() { return x; }
    public int SetX(int x) { this.x = x; }
    ...........
}

void Function(Ocean ocean, Submarine ship)
{
    // 북쪽으로 이동
    ship.SetY(ship.GetY() - 1);

    // 암초에 걸렸는지 체크
    if(ocean.IsReef(ship.GetX(), ship.GetY())
        ship.Destroyed();
}

이런 것은 '캡슐화'는 되었지만(x, y변수가 '캡슐화되었죠) '추상화'가 된 코드는 아닙니다. '추상화'를 시킨다면

class Ocean
{
    ......

    public bool IsReef(int x, int y)    // 암초가 있는가
    {
        .....
    }
}

class Submarine
{
    private int x;    // x좌표
    private int y;    // y좌표
    private int depth;// 깊이

    public void ToEast() { ++x; }
    public void ToWest() { --x; }
    public void ToSouth() { ++y; }
    public void ToNorth() { --y; }

    public bool IsSafe(Ocean ocean)
    {
        if(ocean.IsReef(x, y))  // 암초에 걸렸으면
            return false;
        // 다른 상황 체크

        // 모두 통과했으면 안전
        return true;
    }
    ...........
}

void Function(Ocean ocean, Submarine ship)
{
    // 북쪽으로 이동
    ship.ToNorth();

    // 배가 안전한지 체크
    if(ship.isSafe(ocean))
        ship.Destroyed();
}


가 되어야 합니다.


* 위에서 저런 식의 코딩은 C에서도 좋지 않다고 했는데, C에서의 코딩은 다음과 같이 하는 것이 좋습니다.

struct Ocean
{
    ......
}

struct Submarine
{
    int x;    // x좌표
    int y;    // y좌표
    int depth;// 깊이
}

void ToEast(Submarine ship) { ++ship.x; }
void ToWest(Submarine ship) { --ship.x; }
void ToSouth(Submarine ship) { ++ship.y; }
void ToNorth(Submarine ship) { --ship.y; }

bool IsSafe(Ocean ocean, Submarine ship)
{
    if(ocean.IsReef(ship.x, ship.y))  // 암초에 걸렸으면
        return false;
    // 다른 상황 체크

    // 모두 통과했으면 안전
    return true;
}

void Function(Ocean ocean, Submarine ship)
{
    // 북쪽으로 이동
    ToNorth(ship);

    // 배가 안전한지 체크
    if(isSafe(ocean, ship))
        Destroyed(ship);
}

2016년 8월 31일 수요일

C# User defined Enumerable

1, 2, 3, 4, 5....의 순으로 인수를 얻어오기 위한 방식은 간단합니다.

for (int k = 1; k <= 5; ++k)
{
    Console.WriteLine(p);
};


그런데 이를테면 2, 3, 5, 7, 11, ... 순으로 소수를 얻어오려면 IEnumerable을 사용하면 좀 더 간단히 할 수 있습니다. Iterator를 사용해서 말입니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PrimeNumber
{
    internal class PrimeNumber
    {
        List<int> m_Prime = new List<int>();

        internal IEnumerable<int> Prime()
        {
            m_Prime.Add(2);
            yield return 2; // 최초에 2 리턴

            for (int num = 3; ; num += 2)
            {
                // 소수 갯수가 100개 넘었다면 끝
                if (m_Prime.Count >= 100)
                    yield break;

                // 소수계산 공식
                bool isPrime = true;
                foreach (int p in m_Prime)
                {
                    if (num % p == 0)
                    {
                        isPrime = false;
                        break;
                    }
                    if (p * p > num)
                        break;
                }

                if (isPrime)
                {
                    m_Prime.Add(num);
                    yield return num; // 소수를 찾았으면 소수 리턴
                }
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            PrimeNumber prime = new PrimeNumber();
            foreach (var p in prime.Prime())
                Console.WriteLine(p);
        }
    }
}

어디서든지 yield return을 만나면 일단 리턴을 한 후, 다음 함수가 불리면 yield문 이후부터 다시 실행 시작합니다. 반면 yield break는 완전한 중지로 Main()의 foreach구문을 빠져나오게 됩니다.

소수찾기 루틴을 코딩하기가 복잡하다면? 다음과 같이 하면 됩니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PrimeNumber
{
    internal class PrimeNumber
    {
        List m_Prime = new List();

        internal IEnumerable<int> Prime()
        {
            yield return 2;
            yield return 3;
            yield return 5;
            yield return 7;
            yield return 11;
            yield return 13;
            yield return 17;
            yield return 19;
            yield return 23;
            yield return 29;
            yield return 31;
            yield return 37;
            yield return 41;
            yield return 43;
            yield return 47;
            yield return 53;
            yield return 59;
            yield return 61;
            yield return 67;
            yield return 71;
            yield return 73;
            yield return 79;
            yield return 83;
            yield return 89;
            yield return 97;
            yield return 101;
            yield return 103;
            yield return 107;
            yield return 109;
            yield return 113;
            yield return 127;
            yield return 131;
            yield return 137;
            yield return 139;
            yield return 149;
            yield return 151;
            yield return 157;
            yield return 163;
            yield return 167;
            yield return 173;
            yield return 179;
            yield return 181;
            yield return 191;
            yield return 193;
            yield return 197;
            yield return 199;
            yield return 211;
            yield return 223;
            yield return 227;
            yield return 229;
            yield return 233;
            yield return 239;
            yield return 241;
            yield return 251;
            yield return 257;
            yield return 263;
            yield return 269;
            yield return 271;
            yield return 277;
            yield return 281;
            yield return 283;
            yield return 293;
            yield return 307;
            yield return 311;
            yield return 313;
            yield return 317;
            yield return 331;
            yield return 337;
            yield return 347;
            yield return 349;
            yield return 353;
            yield return 359;
            yield return 367;
            yield return 373;
            yield return 379;
            yield return 383;
            yield return 389;
            yield return 397;
            yield return 401;
            yield return 409;
            yield return 419;
            yield return 421;
            yield return 431;
            yield return 433;
            yield return 439;
            yield return 443;
            yield return 449;
            yield return 457;
            yield return 461;
            yield return 463;
            yield return 467;
            yield return 479;
            yield return 487;
            yield return 491;
            yield return 499;
            yield return 503;
            yield return 509;
            yield return 521;
            yield return 523;
            yield return 541;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            PrimeNumber prime = new PrimeNumber();
            foreach (var p in prime.Prime())
                Console.WriteLine(p);
        }
    }
}

2016년 8월 20일 토요일

동일한 여러개 인수 전달

함수를 호출할 때 컴파일러가 인수 타입을 체크


void Func(int a, ClassTypeA *b, StructTypeB &c, int *d);

main()
{
    int varA;
    ClassTypeA clsB;
    StructTypeB strC;

    Func(varA, &clsB, strC, &varA);  // 컴파일 성공
    Func(&clsB, varA, &varA, strC);  // 컴파일 에러
}


하지만 만약 이 여러개의 인수가 동일한 타입을 가지고 있다면?


void Func(int id, int height, int weight, int birthYear, int score, int familyNumber)
{
    printf("Height %d\n", height);
    printf("Weight %d\n", weight);
}

main()
{
    int Id = 1;
    int weight = 67;      // 몸무게
    int height = 175;    // 키
    int birth = 1992;   // 태어난 해
    int family = 4;    // 가족 수
    int score = 96;   // 성적


    Func(Id, height, weight, birth, score, family);  // 컴파일 성공
    Func(Id, weight, height, birth, family, score);  // 컴파일 성공
    Func(family, height, score, birth, weight, Id);  // 컴파일 성공
}

즉 프로그래머의 실수를 컴파일러가 찾을 수 없다는 뜻.


C/C++

struct Arg
{
    int Id;
    int height;           // 키
    int weight;          // 몸무게
    int birthYear;      // 태어난 해
    int score;         // 성적
    int familyNumber; // 가족 수
};

void Func(Argument *arg)
{
    printf("Height %d\n", arg->height);
    printf("Weight %d\n", arg->weight);
}

main()
{
    Argument arg;
    arg.Id = 1;
    arg.weight = 67;          // 몸무게
    arg.height = 175;        // 키
    arg.birthYesr = 1992;   // 태어난 해
    arg.familyNumber = 4;  // 가족 수
    arg.score = 96;       // 성적


    Func(&arg);  // 컴파일 성공
}

물론

    arg.weight = 175;
    arg.height = 67;

와 같은 실수는 감지 못하지만 이것은 눈으로 볼 수 있으므로 실수할 가능성 적음


Lua

루아에서는 타입 자체가 없으므로 이런 버그가 발생할 가능성 더 큼. 루아에서는 테이블을 이용하여 간단히 구현 가능

function Func(m)
   print("Height " .. m.height)
   print("weight " .. m.weight)
end

Func({Id = 1,
      weight = 67;      -- 몸무게
      height = 175;     -- 키
      birthyear = 1992, -- 태어난 해
      familyNumber = 4; -- 가족 수
      score = 96;       -- 성적
     }
    )


C#

C#에서는 변수 이름으로 인수를 전달하는 기능이 있으므로 위와 같이 인수 순서가 헷갈릴 때 유용함


namespace Space
{
    class Program
    {
        static void Func(int id, int height, int weight,
                         int birthYear, int score, int familyNumber)
        {
            Console.WriteLine("Height {0}", height);
            Console.WriteLine("Weight {0}", weight);
        }
        static void Main(string[] args)
        {
            int Id = 1;
            int weight = 67;      // 몸무게
            int height = 175;    // 키
            int birth = 1992;   // 태어난 해
            int family = 4;    // 가족 수
            int score = 96;   // 성적

            Func(id:Id, score:score, weight:weight,
                 familyNumber:family, height:height, birthYear:birth);  // 컴파일 성공
        }
    }
}