C# 6.0 완벽 가이드/ 동적 프로그래밍

DLR(동적 언어 런타임)

    • C#은 DLR(dynamic language runtime; 동적 언어 런타임)에 의존해서 동적 바인딩(dynamic binding)을 수행한다.
    • 이름이 주는 느낌과는 달리 DLR은 CLR의 동적 버전이 아니다. DLR은 System.Xml.Dll 같은 다른 모든 라이브러리와 마찬가지로 그냥 CLR 위에 놓인 하나의 라이브러리이다. DLR의 주된 역할은 정적 형식 언어와 동적 형식 언어 모두에서 동적 프로그래밍의 통합을 위한 실행시점 서비스들을 제공하는 것이다.
      • DLR 덕분에 C#이나 VB, IronPython, IronRuby 같은 여러 언어는 동적으로 함수를 호출할 때 동일한 규약을 따른다.
      • 결과적으로 이 언어들은 같은 라이브러리를 공유할 수 있으며, 다른 언어로 작성된 코드를 호출할 수 있다.
    • 또한 .NET에서 새로운 동적 언어를 작성하기가 비교적 쉬운 것도 DLR 덕분이다. 동적 언어를 작성할 때 IR 코드를 직접 산출하는 코드를 작성하는 대신, 표현식 트리를 다루는 코드를 작성하면 된다.
    • 더 나아가서 DLR은 모든 소비자가 호출 사이트 캐싱(call-site caching)의 혜택을 받게 한다. 호출 사이트 캐싱은 동적 바인딩 과정에서 잠재적으로 비싼 멤버 환원 결정을 되풀이 하지 않기 위해 DLR이 사용하는 하나의 최적화 기법이다.
  • DLR은 .NET Framework 4.0에서 처음으로 .NET Framework 자체에 포함되었다. 그 전에는 Codeplex에서 따로 내려받아야 하는 라이브러리였다. 지금도 Codeplex 사이트에는 동적 언어 작성자에 유용한 몇 가지 추가 자원이 있다.

호출 사이트란?

  • 컴파일러는 동적 표현식(dynamic expression)을 실행시점에서 누가 평가할지 알지 못한다. 예컨대 다음과 같은 메서드를 생각해 보자.
public dynamic Foo (dynamic x, dynamic y)
{
  return x / y; // 동적 표현식
}
  • 매개변수 x, y는 실행시점에서 임의의 CLR 객체일 수도 있고 COM 객체일 수도 있으며 심지어 다른 동적 언어에 담긴 객체일 수도 있다.
    • 따라서 컴파일러는 이런 코드를 통상적인 방식으로 컴파일할 수 없다. 즉, 컴파일러는 알려진 형식의 알려진 메서드를 호출하는 IL 코드를 산출하지 못한다.
    • 대신 컴파일러는 해당 연산을 서술하는 표현식 트리를 실행시점에서 만들어 내는 코드를 산출한다. 그 표현식 트리는 DLR이 실행시점에서 바인딩할 호출 사이트가 관리한다. 호출 사이트는 호출자와 호출 대상 사이의 중재자 역할을 한다.
  • 호출 사이트를 나타내는 클래스는 System.Core.dll의 CallSite<>이다. 이 점은 앞의 메서드를 역어셈블하면 알 수 있다.
    • Foo를 역어셈블 하면 다음과 같은 결과가 나온다.
static CallSite<Func<CallSite,object,object,object>> divideSite;

[return: Dynamic]
public object Foo([Dynamic] object x, [Dynamic] object y)
{
  if (divideSite == null)
    divideSite = CallSite<Func<CallSite,object,object,object>>.Create(Microsoft.CSharp.UntimeBinder.Binder.BinaryOperation(CSharpBinderFlags.None, ExpressionType.Divide, /* 간결함을 위해 나머지 인수들 생략 */));
  return divideSite.Target(divideSite, x, y);
}
  • 여기서 보듯이 호출 사이트는 하나의 정적 필드에 보관된다. 따라서 메서드를 호추할 때마다 호출 사이트를 다시 생성하는 비용은 발생하지 않는다. 더 나아가서 DLR은 바인딩 단계의 결과와 실제 메서드 대상들도 캐시에 담아둔다.(x와 y의 형식에 따라서는 여러 개의 대상이 있을 수 있다.)
  • 이후 피연산자 x와 y를 인수로 해서 호출 사이트의 Target(대리자)를 호출함으로써 실제 동적 호출이 일어난다.
  • 이 예제에서 Binder는 C#에 특화된 바인더 클래스임을 주의하기 바란다. 동적 바인딩을 지원하는 모든 언어는 자신에 특화된 바인더를 제공한다.
    • 이 바인더는 DLR이 표현식을 해당하는 언어에 맞는 방식으로 해석하는데 도움을 준다.
    • 예컨대 정수 5와 2로 Foo를 호출했을 때 C#의 바인더는 2라는 결과가 나오게 하지만, VB.NET의 바인더는 2.5가 나오게 한다.

수치 형식 통합

  • 4장에서 dynamic을 이용해서 하나의 메서드가 모든 수치 형식에 맞게 작동하게 하는 방법을 살펴보았다. 해당 예제는 다음과 같다.
static dynamic Mean(dynamic x, dynamic y) => (x + y) / 2;

static void Main()
{
  int x = 3, y = 5;
  Console.WriteLine(Mean(x, y));
}
  • 그러나 이 예는 정적 형식 안전성을 헛되이 희생한다. 다음 코드는 오류 없이 컴파일 되지만, 실행시점에서 문제를 일으킨다.
string s = Mean(3, 5);  // 실행시점 오류
  • 이 문제는 제네릭 형식 매개변수를 하나 도입하고 그것을 계산 표현식 안에서 dynamic으로 캐스팅하면 해결 된다.
static T Mean<T>(T x, T y) 
{
  dynamic result = ((dynamic)x + y) / 2;
  return (T) result;
}
  • 계산 결과를 명시적으로 다시 T로 캐스팅했음을 주목하기 바란다. 이 캐스팅을 제거하면 암묵적 형식 변환이 적용되는데, 언뜻 보기에는 암묵적 형식 변환으로도 문제가 없을 것 같다. 그러나 8비트 또는 16비트 정수 형식으로 이 메서드를 호출하면 문제가 드러난다.
    • 이해를 돕기 위해 우선 형식이 정적으로 적용되는 경우 두 8비트 수를 더할 때 어떤 일이 일어나는지 생각해 보자.
byte b = 3;
Console.WriteLine((b + b).GetType().Name);  // Int32
  • 두 8비트 수를 더한 결과는 Int32이다. 이는 컴파일러가 8비트 또는 16비트 수를 먼저 Int32로 승격(promotion)한 다음에 산술 연산을 수행하기 때문이다.
    • 일관성을 위해 C# 바인더 역시 DLR에게 정확히 같은 방식으로 연산을 수행하라고 알려준다.
    • 결과적으로 DLR은 Int32 형식의 결과를 산출하며, 더 작은 수치 형식의 결과를 얻으려면 앞의 예처럼 명시적인 캐스팅이 필요하다.
    • 물론 지금처럼 평균을 내는 것이 아니라 두 수의 합을 구하는 경우에는 작은 형식으로의 명시적 캐스팅 때문에 overflow가 발생할 여지가 있다.
  • 동적 바인딩은 성능에 약간의 부담을 가중한다. 호출 사이트 캐싱이 적용되어도 그 추가부담이 완전히 없어지지는 않는다.
    • 이러한 추가부담을 완화하는 한 가지 방법은 아주 흔히 쓰이는 형식들에 대해 정적 형식 중복적재 버전을 추가하는 것이다.
    • 예컨대 프로그램의 성능을 프로파일링했더니 double로 Mean을 호출하는 것이 병목임을 알게 되었다고 하자. 그러면 다음과 같은 중복적재를 추가해서 성능 문제를 완화할 수 있을 것이다.
static double Mean(double x, double y) => (x + y) / 2;
  • 컴파일 시점에서 형식이 double임을 알 수 있는 인수들로 Mean을 호출하는 호출문에 대해서는 컴파일러가 이 중복적재 버전을 선택한다.

동적 멤버 중복적재 해소

  • 정적으로 형식이 알려진 메서드를 동적 형식 인수들로 호출하면, 멤버 중복적재해소가 컴파일 시점에서 실행시점으로 미뤄진다.
    • 이러한 중복적재 해소 지연은 특정 프로그래밍 과제를 단순화 하는데 도움이 된다.
    • 이를테면 방문자(Visitor) 설계 패턴의 구현ㅇ늘 단순화하는데 이러한 지연을 활용할 수 있다.
    • 또한 이는 C#의 정적 형식 적용 때문에 생기는 한계를 우회할 때도 유용하다.

방문자 패턴의 단순화

  • 본질적으로 방문자 패턴을 이용하면 어떤 클래스 계통구조의 기존 클래스들을 수정하지 않고도 계통구조에 메서드를 ‘추가’ 할 수 있다.
    • 이 패턴이 유용하긴 하지만, 정적 구현이 다른 대부분의 설계 패턴에 비해 다소 까다롭고 비직관적이다.
    • 또한 방문 되는 클래스들은 반드시 Accept 메서드를 노출하는 ‘방문자 친화적’ 클래스어이여 한다.
    • 따라서 독자가 직접 고칠 수 없는 클래스는 방문자 친화적인 방문 대상이 될 수 없다.
  • 동적 바인딩을 이용하면 같은 목표를 좀 더 쉽게 달성할 수 있다. 또한 기존 클래스를 수정하지 않아도 된다.
    • 이해를 돕기 위한 예로 다음과 같은 클래스 계통 구조를 생각해 보자.
class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }

  // Friends 컬렉션은 Customers 객체들과 Employees 객체들을 담는다.
  public readonly IList<Person> Friends = new Collection<Person>();
}

class Customer : Person { public decimal CreditLimit { get; set; } }
class Employee : Person { public decimal Salary { get; set; } }
  • 이제 자동으로 Person 객체의 세부사항을 XML 요소(XElement) 형태로 출력하는 메서드를 작성한다고 하자.
    • 가장 명백한 해법은 Person의 속성들로 채운 XElement 객체를 돌려주는 ToXElement라는 가상 메서드를 Person 클래스에 추가하고, Person 파생 클래스들에서 그 메서드를 적절히 구현하는 것이다.
    • 예컨대 Customer 클래스와 Employee 클래스의 해당 메서드는 각각 CreditLimit 속성과 Salary 속성으로 XElement를 채워햐 할 것이다.
  • 그런데 이러한 패턴에는 문제가 있다 그 이유는 다음 두 가지이다.
    • Person, Customer, Employee 클래스가 독자의 소유가 아닐 수 있다. 그러면 메서드들을 추가할 수 없다 (그리고 확장 메서드는 다형적으로 작동하지 않는다)
    • Person, Customer, Employee 클래스가 이미 커다란 클래스일 수 있다. 흔히 보는 안티패턴의 하나로 ‘전지전능한 객체(god object)’라는 것이 있는데, Person 같은 클래스에 너무나 많은 기능이 추가되어서 유지보수가 불가능할 정도가 되는 것을 말한다.
      • 그런 안티패턴을 피하는 좋은 방법 하나는 Person의 전용(private) 상태에 접근할 필요가 없는 함수들은 Person의 멤버로 추가하지 않는 것이다. 그리고 ToXElement가 바로 그러한 함수이다.
  • 동적 멤버 중복적재 해소(dynamic member overload resolution) 기법을 이요하면 ToXElement의 기능을 개별적인 클래스로 둘 수 있다. 게다가 형식에 따른 멤버 선택을 지저분한 switch 문으로 구현할 필요도 없다.
class ToXElementPersonVisitor
{
  public XElement DynamicVisit(Person p) => VIsit ((dynamic)p);

  XElement Visit (Person p)
  {
    return new XElement("Person",
      new XAttribute("Type", p.GetType().Name),
      new XElement("FirstName", p.FirstName),
      new XElement("LastName", p.LastName),
      p.Friends.Select(f => DynamicVisit(f)));
  }

  XElement Visit (Customer c) 
  {
    XElement xe = Visit((Person)c);  // 기반 메서드 호출
    xe.Add(new XElement("CreditLimit", c.CreditLimit));
    return xe;
  }

  XElement Visit (Employee e)
  {
    XElement xe = Visit((Person)e);  // 기반 메서드 호출
    xe.Add(new XElement("Salary", c.Salary));
    return xe;
  }
}
  • DynamicVisit 메서드는 소위 동적 배분(dynamic dispatch)을 수행한다. 즉, 이 메서드는 실행시점에서 결정된, Visit의 가장 구체적인 버전을 선택해서 호출한다.
    • 위의 예제에서 Friends 컬렉션의 각 ‘친구’에 대해 DynamicVisit를 호출하는 부분을 주목하기 바란다. 이 부분 덕분에 친구가 고객인지 직원인지에 따라 적절한 중복적재 버전이 호출된다.
  • 다음은 이 클래스의 사용 예이다.
var cust = new Customer
{
  FirstName = "Joe", LastName = "Bloggs", CreditLimit = 123
}

cust.Friends.Add(
  new Employee { FirstName = "Sue", LastName = "Brown", Salary = 50000 }
);

Console.WriteLine(new ToXElementPersonVisitor().DynamicVisit(cust));

변형들

  • 방문자 클래스를 여러 개 만들 계획이라면, 방문자 클래스를 위한 추상 기반 클래스를 정의해 두는 형태의 변형이 유용하다.
abstract class PersonVisitor<T>
{
  public T DynamicVisit (Person p) { return Visit((dynamic)p); }
  protected abstract T Visit (Person p);
  protected virtual T Visit (Customer c) { return Visit ((Person) c); }
  protected virtual T Visit (Employee e) { return Visit ((Person) e); }
}
  • 이런 추상 기반 클래스가 있으면 파생 클래스들은 자신만의 DynamicVisit 메서드를 구현할 필요가 없다. 그냥 자신에 맞게 특수화하고자 하는 Visit 버전들을 재정의하기만 하면 된다.
    • 또한 이 패턴에는 Person 계통 구조를 훑는 메서드들을 한 곳에 중앙집중화하며 구현자들이 기반 메서드들을 좀 더 자연스럽게 호출할 수 있다는 장점도 있다.
class ToXElementPersonVisitor : PersonVisitor<XElement>
{
  protected override XElement Visit (Person p)
  {
    return new XElement("Person",
      new XAttribute("Type", p.GetType().Name),
      new XElement("FirstName", p.FirstName),
      new XElement("LastName", p.LastName),
      p.Friends.Select(f => DynamicVisit(f)));
  }

  protected override XElement Visit (Customer c) 
  {
    XElement xe = base.Visit(c);
    xe.Add(new XElement("CreditLimit", c.CreditLimit));
    return xe;
  }

  protected override XElement Visit (Employee e)
  {
    XElement xe = base.Visit(e);
    xe.Add(new XElement("Salary", c.Salary));
    return xe;
  }
}
  • 심지어 ToXElementPersonVisitor 자체를 기반으로 삼아서 또 다른 방문자 클래스를 파생할 수도 있다.

다중 배분

  • C#과 CLR은 항상 가상 메서드 호출이라는 형태로 제한적이나마 동적 다형성을 지원해 왔다. 이러한 동적 다형성은 C#의 dynamic 키워드를 이용한 동적 바인딩과는 다르다. 주된 차이점은 가상 메서드 호출에서는 컴파일러가 반드시 특정 가상 멤버를 호출된 멤버의 서명과 이름에 기초해서 컴파일 시점에서 결정해야 한다는 것이다. 이는 다음 두 가지를 의미한다.
    • 호출 표현식을 컴파일러가 반드시 완전하게 이해해야 한다(예컨대 컴파일러는 컴파일 시점에서 대상 멤버가 필드인지 아니면 속성인지 구분할 수 있어야 한다)
    • 중복적재 해소를 컴파일러가 전적으로 컴파일 시점 인수 형식들에 기초해서 완료한다.
  • 두 번째 사항 때문에 가상 메서드 호출은 항상 단일 배분(single dispatch) 방식으로 일어난다. 다음과 같은 메서드 호출을 예로 그 이유를 섦여해 보겠다(Walk가 가상 메서드라고 가정한다.)
animal.Work(owner);
  • 실행시점에서 이를테면 개의 Walk 메서드를 호출할 것인지 아니면 고양이의 Walk 메서드를 호출할 것인지는 오직 수신자(receiver)인 animal의 형식 하나로만 결정된다. 그래서 단일 배분인 것이다.
    • Walk 메서드에 서로 다른 종류의 소유자(owner)를 받는 여러 중복적재 버전이 있다고 할 때, 그중 어떤 것이 선택되는지는 owner 객체의 실제 실행시점 형식과는 무관하게 컴파일 시점에서 결정(해소)된다.
    • 다른 말로 하면, 호출되는 메서드는 오직 수신자의 실행 시점 형식에 따라서만 달라진다.
  • 반면 다음과 같은 동적 호출에서는 중복적재 해소가 실행시점으로 미루어진다.
animal.Work((dynamic)owner);
  • 이 경우 Walk 메서드의 어떤 버전이 최종적으로 선택되는지는 animal의 형식과 owner의 형식 모두에 의존한다. 호출할 Walk 메서드 버전의 결정에 수신자의 실행시점 형식뿐만 아니라 인수들의 실행시점 형식들도 관여한다는 점에서, 이를 다중 배분(multiple dispatch)이라고 부른다.

제네릭 형식 멤버의 익명 호출

  • C#의 엄격한 정적 형식 적용은 양날의 검이다. 한편으로는 컴파일 시점에서 코드의 정확성을 어느 정도 보장하지만, 또 한편으로는 특정 종류의 코드를 표현하기 어렵게 또는 불가능하게 만드릭도 한다.
    • 후자의 경우 반영 기능보다는 동적 바인딩을 사용하는 것이 더 깔끔하고 효율적인 해결책이다.
  • 한 예로 G<T> 형식의 객체를 다루어야 하는데, T를 컴파일 시점에서 알 수 없는 상황이라면 동적 바인딩이 좋은 해결책이 된다. 구체적인 예로 다음과 같은 클래스가 있다고 하자.
public class Foo<T> { public T Value; }
  • 그리고 다음과 같은 메서드를 작성한다고 하자.
static void Write(object obj)
{
  if (obj is Foo<>)  // 위법
    Console.WriteLine((Foo<>)obj).Value);  // 위법
}
  • 위 메서드는 컴파일 되지 않는다 .묶이지 않은(unbound; 바인딩이 완료되지 않은) 제네릭 형식의 멤버는 호출할 수 없기 때문이다.
  • 동적 바인딩은 이 문제를 우회하는 두 가지 수단을 제공한다. 첫째는 다음처럼 Value 멤버에 동적으로 접근하는 능력이다.
static void Write(dynamic obj)
{
  try { Console.WriteLine(obj.Value); }
  catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) { ... }
}
  • 이러한 접근방식에는 Value라는 필드나 속성이 있는 그 어떤 객체에 대해서도 메서드가 작동한다는 잠재적인 장점이 있다. 그러나 문제점도 두 가지 있다.
    • 첫째로 이런 방식에서는 예외를 잡기가 다소 지저분하고 비효율적이다(게다가 DLR에게 ‘이 연산이 성공하겠는가?’라고 미리 물어볼 방도도 없다)
    • 둘째로 이 접근방식은 만일 Foo가 인터페이스(이를테면 IFoo<T>)이고 다음 두 조건 중 하나라도 참이면 작동하지 않는다.
      • Value가 명시적으로 구현되었다.
      • IFoo<T>를 구현하는 형식에 접근할 수 없다
  • 더 나은 해법은 Value를 조회하는 메서드, 이를테면 GetFooValue라는 메서드를 따로 두어서 여러 버전으로 중복적재하고, 그 메서드에 대해 동적 멤버 중복적재 해소 기법을 적용하는 것이다.
static void Write(dynamic obj)
{
  object result = GetFooValue(obj);
  if (result != null)  Console.WriteLine(result);
}

static T GetFooValue<T> (Foo<T> foo) { return foo.Value; }
static object GetFooValue (object foo) { return null; }
  • object 형식의 매개변수를 받도록 중복적재된 GetFooValue 버전은 임의의 형식에 대한 최후의 보루 역할을 한다. 실행시점에서 C# 동적 바인더는 동적 인수로 GetFooValue를 호출하는 표현식에 대해 최적의 중복적재 버전을 선택한다.
    • 만일 해당 객체가 Foo<T>와 호환되는 형식이 아니면, 예외를 던지는 대신 object 형식의 매개변수를 받는 버전을 선택한다.
  • 또 다른 방법은 그냥 첫 GetFooValue 중복적재만 작성하고 RuntimeBinderException 예외를 잡는 것이다. 이 방법에는 foo.Value가 널인 경우를 구분할 수 있다는 장점이 있다. 단점은, 예외를 던지고 잡는데 필요한 성능상의 비용이 추가된다는 점이다.
  • 19장에서는 인터페이스에 관한 이와 동일한 문제를 반영을 이용해서 해결했는데, 지금보다 코드가 훨씬 복잡했다.
    • 거기에서는 IEnumerable이나 IGrouping<,> 같은 객체를 이해하는 좀 더 강력한 ToString의 구현을 예제로 삼았다.
    • 다음은 같은 문제를 동적 바인딩을 이용해서 좀 더 우아하게 해결한 것이다.
static string GetGroupKey<TKey, TElement>(IGrouping<TKey, TElement> group)
{
  return "Group with key=" + group.Key + ": " ;
}

static string GetGroupKey(object source { return null; }

public static string ToStringEx(object value)
{
  if (value == null) return "<null>";
  if (value is string) return (string) value;
  if (value.GetType().IsPrimitive) return value.ToString();

  StringBuilder sb = new StringBuilder();

  string groupKey = GetGroupKey((dynamic)value);  // 동적 배분
  if (groupKey != null) sb.Append(groupKey);

  if (value is IEnumerable)
    foreach (object element in ((IEnumerable)value))
      sb.Append(ToStringEx(element) + " " );

  if (sb.Length == 0) sb.Append(value.ToString());

  return "\r\n" + sb.ToString();
}
  • 이제 다음을 실행하면
Console.WriteLine(ToStringEx("xyyzzz".GroupBy(c => c)));
  • 다음과 같은 결과가 출력된다.
Group with key=x: x
Group with key=y: y y
Group with key=z: z z z
  • 이 예제는 19장의 문제를 동적 멤버 중복적재 해소를 이용해서 풀었다. 만일 해당 부분을 다음과 같이 구현했다면,
dynamic d = value;
try { groupKey = d.Value); }
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) { ... }
  • 코드가 의도한 대로 작동하지 않는다. 왜냐하면 LINQ의 GroupBy 연산자는 IGrouping<,>를 구현하는 형식의 객체를 돌려주는데, 그 형식은 LINQ의 내부 클래스라서 외부에서는 접근할 수 없기 때문이다.
internal class Grouping : IGrouping<TKey, TElement>, ...
{
  public TKey Key;
  ...
}
  • Key 속성이 public 으로 선언되어 있지만, 그 속성이 속한 클래스 자체가 internal이기 때문에 외부에서는 오직 IGrouping<,> 인터페이스를 통해서만 Key에 접근할 수 있다.
    • 그리고 4장에서 설명했듯이 Value 멤버의 동적 호출을 그 인터페이스에 묶으라고 DLR에게 알려줄 방법은 없다.

동적 객체의 구현

  • 어떤 형식을 만들 때 객체의 바인딩 의미론을 직접 제어하려면 IDynamicMetaObjectProvider 인터페이스를 구현해야 한다. 이를 좀 더 쉽게 구현하는 방법은 DynamicObject 클래스를 상속하는 것이다. 이 클래스는 그 인터페이스의 기본 구현을 제공한다.
static void Main()
{
  dynamic d = new Duck();
  d.Quack();
  d.Waddle();
}

public class Duck : DynamicObject
{
  public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
  {
    Console.WriteLine(binder.Name + " 메서드가 호출되었음");
    result = null;
    return true;
  }
}

DynamicObject 클래스

  • 앞의 예제는 소비자가 동적 객체의 메서드를 호출할 수 있도록 DynamicObject 클래스의 TryInvokeMember 메서드를 재정의했다.
    • DynamicObject는 그 밖에도 소비자가 다른 프로그래밍 구축 요소들을 사용할 수 있게 하는 여러 가상 메서드를 제공한다. 다음은 그러한 가상메서드들과 그에 해당하는 C# 구축 요소를 정리한 것이다.
메서드 프로그래밍 구축 요소
TryInvokeMember 메서드
TryGetMember, TrySetMember 속성 또는 필드
TryGetIndex, TrySetIndex 인덱서
TryUnaryOperation ! 같은 단항 연산자
TryBinaryOperation == 같은 이항 연산자
TryConvert 다른 형식으로의 변환(캐스팅)
TryInvoke 객체 자체의 호출(이를테면 d(“foo”))

 

  • 이 메서드들은 만일 해당 연산이 성공했으면 true를 돌려준다. 만일 이 메서드들이 false를 돌려주면, DLR은 언어의 기본 바인더를 이용해서 주어진 요구에 맞는 멤버를 DynamicObject(의 파생 클래스) 자체에서 찾는다. 만일 그런 멤버가 없으면 RuntimeBinderException을 던진다.
  • TryGetMember와 TrySetMember의 활용 방법을 보여주는 예로 XML 요소의 한 특성에 동적으로 접근하는 클래스를 살펴보자.
static class XExtensions
{
  public static dynamic DynamicAttributes (this XElement e) => new XWrapper(e);

  class XWrapper : DynamicObject
  {
    XElement _element;
    public XWrapper (XElement e) { _element = e; }

    public override bool TryGetMember(GetMebmerBinder binder out object result)
    {
      result = _element.Attribute(binder.Name).Value;
      return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
      _element.SetAttributeValue(binder.Name, value);
      return true;
    }
  }
}
  • 다음은 이 클래스를 사용하는 예이다.
XElement x = XElement.Parse(@"<Label Text=""Hello"" Id=""5""/>");
dynamic da = x.DynamicAttributes();
Console.WriteLine(da.Id);  // 5
da.Text = "Foo";
Console.WriteLine(x.ToString());  // <Label Text="Foo" Id="5" />
  • 또 다른 예로 다음은 System.Data.IDataRecord에 대해 비슷한 일ㅇ르 하는 ㅋ르래스이다. 이런 클래스가 있으면 자료 판독기(data reader)를 사용하기가 한결 쉬워진다.
public class DynamicReader : DynamicObject
{
  readonly IDataRecord _dataRecord;
  public DynamicReader (IDataRecord dr) { _dataRecord = dr; }

  public override bool TryGetMember(GetMemberBinder binder, out object result)
  {
    result = _dataRecord[binder.Name];
    return true;
  }
}
...
using (IDataReader reader = someDbCommand.ExecuteReader())
{
  dynamic dr = new DynamicReader(reader);
  while (reader.Read())
  {
    int id = dr.ID;
    string firstName = dr.FirstName;
    DateTime dob = dr.DateOfBirth;
    ...
  }
}
  • 다음은 TryBinaryOperation과 TryInvoke에 관한 예제이다.
static void Main()
{
  dynamic d = new Duck();
  Console.WriteLine(d + d);  // foo
  Console.WriteLine(d(78, 'x'));  // 123
}

public class Duck : DynamicObject
{
  public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result)
  {
    Console.WriteLine(binder.Operation);  // Add
    result = "foo";
    return true;
  }
  public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
  {
    Console.WriteLine(args[0]);  // 78
    result = 123;
    return true;
  }
}
  • DynamicObject는 또한 동적 언어의 장점을 활용하기 위한 가상 메서드도 몇 개 제공한다. 특히 GetDynamicMemberNames를 적절히 재정의함으로써 동적 객체가 제공하는 모든 멤버 이름의 목록을 소비자에게 돌려줄 수 있다.
  • GetDynamicMemberNames를 재정의하는 또 다른 목적은 Visual Studio의 디버거에 동적 객체의 내용이 표시되게 하는 것이다.

ExpandoObject 클래스

  • DynamicObject의 또 다른 간단한 용도는 문자열을 키로 해서 객체들을 사전에 저장하고 조회하는 동적 클래스를 작성하는 것이다. 그런데 그런 기능을 제공하는 클래스가 이미 존재한다. ExpandoObject가 바로 그것이다.
dynamic x = new ExpandoObject();
x.FavoriteColor = ConsoleColor.Green;
x.FavoriteNumber = 7;
Console.WriteLine(x.FavoriteColor);  // Green
Console.WriteLine(x.FavoriteNumber);  // 7
  • ExpandoObject는 IDictionary<string, object>를 구현하므로 예제의 x 객체를 다음과 같이 사용하는 것도 가능하다.
var dict = (IDictionary<string,object>)x;
Console.WriteLine(dict["FavoriteColor"]);  // Green
Console.WriteLine(dict["FavoriteNumber"]);  // 2
Console.WriteLine(dict.Count);  // 2

동적 언어와의 상호운용

  • C#이 dynamic 키워드를 통해서 동적 바인딩을 지원하긴 하지만, 문자열로 표현식을 실행시점에서 직접 실행할 정도의 동적 능력을 제공하지는 않는다.
string expr = "2 * 3";
// expr을 '실행' 할 수는 없다.
  • 이는 문자열을 표현식 트리로 바꾸는 코드를 작성하려면 어휘 분석기(lexer)와 의미론 파서(semantic parse)가 필요하기 때문이다.
    • 그런 기능은 C# 컴파일러에 내장되어 있을 뿐, 실행시점의 서비스 형태로 제공되지는 않는다. 실행시점에서 C#이 제공하는 것은 바인더 뿐이며, 바인더는 이미 만들어진 표현식 트리의 해석 방식을 DLR에게 알려주는 역할만 한다.
  • IronPython이나 IronRuby 같은 진정한 동적 언어들은 임의의 문자열을 실행하는 기능을 제공한다. 그러한 기능은 스크립팅, 동적 구성 설정, 동적 규칙 엔진 구현 같은 과제에 유용하다.
    • 따라서 독자가 응용 프로그램의 대부분을 C#으로 작성한다고 해도 그런 과제에 대해서는 동적 언어의 손을 빌리면 개발이 편해진다.
    • 더 나아가서 응용 프로그램에 필요한 어떤 기능이 .NET 라이브러리에는 없지만 동적 언어로 작성된 API에는 있을 수도 있다.
  • 아래 코드를 실행하려면 먼저 IronPython을 설치해야 한다.
using System;
using IronPyton.Hosting;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;

class Calculator
{
  static void Main()
  {
    int result = (int) Caculate("2 * 3");
    Console.WriteLine(result);  // 6
  }

  static object Calculate (string expression)
  {
    ScriptEngine engine = Python.CreateEngine();
    return engine.Execute(expression);
  }
}
  • 문자열을 Python에 넘겨주는 것이므로 문자열 안의 표현식은 C#이 아니라 Python의 규칙에 따라 평가된다. 이는 단순한 산술 표현식 뿐만 아니라 목록(list)  같은 Python 언어의 기능들도 사용할 수 있음을 뜻한다.
var list = (IEnumerable) Calculate("[1, 2, 3] + [4, 5]");
foreach (int n in list)  Console.WriteLine(n);  // 12345

C#과 스크립트 사이의 상태 교화

  • C#에서 Python으로 변수를 전달하려면 몇 가지 단계가 더 필요하다. 다음 예제는 그 단계들을 보여준다. 이 예제를 규칙 엔진(rules engine)의 기본 틀로 사용해도 좋을 것이다.
// 실제 응용에서는 파일이나 데이터베이스에서 표현식 문자열을 가져올 수도 있다.
string auditRule = "taxPaidLastYear / taxPaidThisYear > 2"l

ScriptEngine engine = Python.CreateEngine();

ScriptScope scope = engine.CreateScope();
scope.SetVariable("taxPaidLastYear", 20000m);
scope.SetVariable("taxPaidThisYear", 8000m);

ScriptSource source = engine.CreateScriptSourceFromString(auditRule, SourceCodeKind.Expression);
bool audiRequired = (bool) source.Execute(scope);
Console.WriteLine(auditRequired);  // True
  • 반대로 Python 변수를 C#으로 가져올 때는 GetVariable 메서드를 사용한다.
string code = "result = input * 3";

ScriptEngine engine = Python.CreateEngine();
ScriptScope scope = engine.CreateScope();
scope.SetVariable("input", 2);

ScriptSource source = engine.CreateScriptSourceFromString(code, SourceCodeKind.SingleStatement);
source.Execute(scope);
Console.WriteLine(scope.GetVariable("result"));  // 6
  • 이번에는 SourceCodeKind.SingleStatement를 (Expression이 아니라) 지정했음을 주목하기 바란다. 이 값은 엔진에게 하나의 문장을 실행하라고 지시하는 역할을 한다.
  • 형식들은 .NET 세계와 Python 세계 사이에서 자동으로 인도(마샬링)된다. 심지어 스크립팅 쪽에서 .NET 객체의 멤버들에 접근하는 것도 가능하다.
string code = @"sb.Append(""World"")";
ScriptEngine engine = Python.CreateEngine();
ScriptScope scope = engine.CreateScope();

var sb = new StringBuilder("Hello");
scope.SetVariable("sb", sb);

ScriptSource source = engine.CreateScriptSourceFromString(code, sourceCodeKind.SingleStatement);
source.Execute(scope);
Console.WriteLine(sb.ToString());  // HelloWorld

 

[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.