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비트 수를 더할 때 어떤 일이 일어나는지 생각해 보자.