본문 바로가기

프로그래밍/C#

CLR via C# 17장 델리게이트

17장 델리게이트.

 
델리게이트 살펴보기.
 
 이번 장에서는 콜백 함수라는 프로그래밍 메커니즘에 대해서 이야기하려고 한다. .NET Framework는 이 매커니즘을 델리게이트라는 형태로 노출하고 있다.
 네이티브 C/C++에서 비멤버 함수의 주소는 단지 메모리 주소일 뿐이다. 이 주소는 다른 정보를 일절 포함하지 않는다. (함수 시그니쳐, 함수 호출 규칙 등의 정보)
 .NET Framework는 이와 같은 델리게이트라는 타입 안정성을 준수하는 메커니즘을 제공한다.
 
using System;
using System.IO;
 
internal delegate void Feedback(int value);
 
public sealed class Program {
 
       static void Main(string[] args) {
              StaticDelegateDemo();
       }
 
       private static void StaticDelegateDemo() {
              Console.WriteLine("---- Static Delegate Demo ----");
              Counter(1, 3, null);
              Counter(1, 3, new Feedback(Program.FeedbackToConsole));
              Counter(1, 3, new Feedback(FeedbackToMsgBox));
              Console.WriteLine();
       }
   
       private static void Counter(int from, int to, Feedback fb) {
              for (int val = from; val <= to; ++val) {
                     if (fb != null) fb(val);
              }
       }
 
       private static void FeedbackToConsole(int value) {
              Console.WriteLine("Item=" + value);
       }
 
       private static void FeedbackToMsgBox(int value) {
              Console.WriteLine("Item=" + value);
       }
 
       private void FeedbackToFile(int value) {
 
              using (StreamWriter sw = new StreamWriter("Status", true)) {
                     sw.WriteLine("Item=" + value);
              }
       }
}
 
 우선 맨 처음 우리가 사용할 델리게이트 타입 Feedback을 선언하고 있다. int를 매개변수로 받고 리턴타입은 없는 함수 시그니쳐이다. 이는 네이티브 C/C++에서 typedef를 사용하여 함수포인터 타입을 정의하던 것과 꽤 비슷한 형태이다.
 Counter 메서드에서는 fb라는 Feedback 델리게이트 객체에 대한 참조를 받는다. 객체에 대한 null 체크 후 현재 int값에 대한 델리게이트 콜백 메서드를 호출한다.
 
 

정적 메서드에 대한 콜백을 델리게이트로 구현하기.
 
 위 코드의 StaticDelegateDemo 함수를 살펴보자.
 
private static void StaticDelegateDemo() {
    Console.WriteLine("---- Static Delegate Demo ----");
    Counter(1, 3, null);                                    // null 객체 전달.
    Counter(1, 3, new Feedback(Program.FeedbackToConsole)); // Program의 static method 전달.
    Counter(1, 3, new Feedback(FeedbackToMsgBox));          // Program은 없어도 된다.
    Console.WriteLine();
}
 
두 번째 Counter 메서드를 호출했을 때, Feedback 객체를 하나 생성하여 전달하고 있다. 이 객체는 메서드를 포장하고 있으며, 간접적으로 호출할 수 있도록 도움을 준다.
FeedbackToConsole 메서드는 Program 타입 내부에서 private로 선언되었지만, Counter 메서드 내부에서 Program 타입의 private 메서드를 호출할 수 있다. (다른 타입으로 빼내더라도)
다른 타입의 private 멤버를 델리게이트를 통하여 호출하는 일은 문제되지 않는다.
 
위의 코드는 타입 안정성을 준수한다. 다시 말해서, Feedback 델리게이트 타입과 실제 Program.FeedbackToConsole, Program.FeedbackToMsgBox 메서드의 시그니쳐가 호황성이 있는지를 분명하게 검사한다.
또한 C#과 CLR은 델리게이트에 바인딩할 때 참조 타입에 대한 공변성과 반공변성을 허용한다. 이미 알고 있다시피, 값타입과 void타입에 대해서는 그 대상이 되지 않는다.
 
 

인스턴스 메서드에 대한 콜백을 델리게이트로 구현하기.
 
private static void InstanceDelegateDemo()
{
    Console.WriteLine("---- Instance Delegate Demo ----");
    Program p = new Program(); // 인스턴스를 만든다.
    Counter(1, 3, new Feedback(p.FeedbackToFile));
    Console.WriteLine();
}
 
인스턴스를 만든뒤, 인스턴스의 FeedbackToFile 메서드를 Feedback 델리게이트 객체에 전달한다.
이를 통해 델리게이트가 해당 메서드에 대한 참조를 포장하고, Counter 메서드가 fb 매개변수에 지정된 콜백 메서드를 호출할 때, 인스턴스 메서드를 호출하기 위하여 내재된 this 매개변수로 직전에 만들어진 p 객체가 자동으로 전달된다.
 
 

델리게이트 파헤쳐보기.
 
앞에서 본 것과 같이, C#의 delegate 키워드를 사용하여 델리게이트를 정의할 수 있고, new 연산자를 사용하여 쉽게 인스턴스로 만들 수 있으며, 콜백을 호출하기 위하여 호출할 때와 같은 문법으로 쉽게 호출할 수 있다.
 
내부적으로는 좀 더 복잡한 일들이 일어나고 있는데, 컴파일러와 CLR은 이런 복잡한 면을 단순화하기 위해 많은 처리를 자동으로 해준다.
 
// 이렇게 델리게이트를 정의해주면...
internal delegate void Feedback(int value);
 
// 내부적으로는 아래와 같은 클래스로 새로 정의가 된다.
internal class Feedback : System.MulticastDelegate {
 
    public Feedback(object @object, IntPtr method);
 
    public virtual void Invoke(int value);
 
        // 이 아래의 메서드들은 .NET Framework의 비동기 프로그램 모델과 관련된 내용. 다만 요즘은 거의 사용되지 않는다. (27장 참조)
    public virtual IAsyncResult BeginInvoke(int value, AsyncCallback callback, object @object);
    public virtual void EndInvoke(IAsyncResult result);
}
 
이 클래스가 상속받는 System.MulticastDelegate는 System.Delegate를 상속받는 클래스이다.
FCL에서 델리게이트는 하나의 개념이지만, 안타깝게도 두 개의 클래스가 존재하고 양쪽의 클래스에 대해 고려해야할 필요가 있다. (나름의 사정이 있다고 한다)
 
기본적으로 델리게이트 또한 클래스이기 때문에, 델리게이트는 공개 범위에 대해서 분명하게 인지해야 한다.
모든 델리게이트 타입이 MulticastDelegate 타입을 상속하기 때문에, 이 클래스의 필드, 속성, 메서드도 같이 상속을 받는다.
 
필드
타입
설명
_target
System.Object
이 필드는 콜백 메서드가 호출되어야 할 대상 객체에 대한 참조를 가리키게 된다. 이 필드는 인스턴스 메서드에 암묵적으로 항상 전달되어야 하는 this 매개변수와 같다. 델리게이트 객체가 static method를 포장하는 경우, 이 필드는 null이 된다.
_methodPtr
System.IntPtr
CLR이 콜백으로 호출해야 하는 메서드를 식별하는 내부 정수 필드다.
_invocationList
System.Object
이 필드는 보통 null로 설정되는데, 이후 설명할 델리게이트 체인을 만들기 위한 배열을 가리킬 수 있다.
 
모든 델리게이트는 객체에 대한 참조와, 콜백 메서드를 식별하는 정수 값을 받는 생성자를 가진다. 하지만 소스코드에서는 델리게이트 객체를 생성할 때, Program.FeedbackToConsole이나 p.FeedbackToFile과 같은 형식으로 매개변수를 전달하고 있다.
C# 컴파일러는 델리게이트를 생성하기 위해 소스 코드를 분석하여 어떤 객체와 메서드가 참조되어야 하는지를 확인할 수 있다. 따라서 자동으로 객체에 대한 참조를 생성자의 object 매개변수로 전달하고, 어떤 메서드가 호출되어야 하는지를 식별할 특별한 IntPtr값 (MethodDef또는 MethodRef 메타데이터 토큰으로부터 얻을 수 있는 값)을 method 매개변수로 전달한다.
각각은 위에 MulticastDelegate로 부터 상속받은 필드에 저장되고, 델리게이트 객체는 호출하려는 메서드와 객체에 대한 포장 역할을 맡게된다.
 
private static void Counter(int from, int to, Feedback fb) {
 
    for (int val = from; val <= to; ++val) {
        if (fb != null) fb(val);
    }
}
 
위에 코드에서 볼 수 있듯이 델리게이트를 호출하기 전 항상 null여부를 검사한다. 실제로 fb는 델리게이트 객체에 대한 참조를 나타내기 때문에 null을 가리킬 수 있다.
null 검사 이후에 C++에서의 functor를 사용한 것과 비슷한 모습으로 fb 델리게이트 객체를 사용하고 있다.
컴파일러는 위와 같은 코드에 도달하면 자동으로 델리게이트 객체의 Invoke 메서드를 호출하는 코드를 생성한다. 또한 Invoke 메서드를 명시적으로 호출하여 사용할 수도 있다.
필연적으로 Invoke 메서드의 시그니쳐는 델리게이트 형식과 일치하게 된다.
 
Invoke 메서드가 호출되면, 객체 내부의 _target과 _methodPtr 필드를 사용하여 어떤 객체의 어떤 메서드를 호출할지 파악하고 실행한다.
 

델리게이트를 사용하여 여러 메서드를 호출하기.
 
델리게이트는 이 뿐만 아니라, 여러 메서드를 연결하는 강력한 기능도 더불어 제공하고 있다.
메서드 연결은 델리게이트 객체들을 하나의 집합이나 컬렉션으로 묶는 기능으로, 집합 내의 모든 메서드들을 호출할 수 있다.
 
private static void ChainDelegateDemo1(Program p)
{
    Console.WriteLine("----- Chain Delegate Demo 1 -----");
    Feedback fb1 = new Feedback(FeedbackToConsole);
    Feedback fb2 = new Feedback(FeedbackToMsgBox);
    Feedback fb3 = new Feedback(p.FeedbackToFile);
 
 
    Feedback fbChain = null;
    fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
    fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
    fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
    Counter(1, 2, fbChain);
    Console.WriteLine();
 
 
    fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
    Counter(1, 2, fbChain);
}
 
초기의 fb1, 2, 3의 상태는 아래 그림과 같다.
 
각 델리게이트 객체들은 알맞은 _target과 _methodPtr을 저장하고 있다.
이후, Feedback 델리게이트 타입의 fbChain을 null로 초기화 하여 콜백으로 호출할 메서드가 없는 인스턴스로 만들었다.
다음으로 Delegate.Combine 메서드를 사용하여 체인에 새로운 델리게이트를 추가해주었다.
 
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
 
위의 코드가 실행되면, Combine 메서드는 null과 fb1을 연결하기 위하여 시도한다.
내부적으로 Combine 메서드는 단순하게 fb1을 반환하고, fbChain은 fb1을 가리키는 상태로 설정된다.
 
 
 
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
 
Delegate.Combine 메서드를 한 번 더 호출하게 되면, 내부적으로 fbChain이 이미 다른 델리게이트 객체를 가리키고 있음을 확인하고, 새로운 델리게이트 객체를 생성한다.
새로운 델리게이트 객체는 내부 필드인 _target, _methodPtr 필드를 어떤 값들로 초기화 하는데, 지금 논의하려는 내용과는 크게 관련이 없다.
중요한 것은 내부 필드인 _invocationList 필드를 초기화하여, 델리게이트 객체의 배열을 지정할 수 있도록 준비한다.
배열의 0번째 인덱스는 현재 fbChain이 가리키고 있는 대상인 FeedbackToConsole을 가리키도록 초기화되고, 배열의 1번째 인덱스는 fb2가 가리키고 있는 FeedbackToMsgBox를 가리키도록 초기화된다.
 
 
 
마찬가지로 fb3에 대하여 Combine 메서드를 호출하는 순간, fbChain이 이미 다른 델리게이트 객체를 가리키고 있으므로 새 객체를 생성한다.
그 전 객체의 _invocationList를 확인하고, 새 객체의 _invocationList를 이와 동일하게 가르키도록 한다. 그리고 추가적으로 fb3를 _invocationList의 마지막 인덱스에 집어넣는다.
이후 그 전 객체는 가비지 컬렉팅의 대상이 된다.
 
 
 
 
자 그럼 델리게이트 체인이 만들어지는 과정은 모두 이해했다.
그러면 결국 이 체인은 어떻게 실행될까?
 
본문에서는 Invoke 메서드가 다음과 유사한 형태로 구성되어 있다고 한다.
 
public void Invoke(int value) {
    Delegate[] delegates = _invocationList as Delegate[];
 
    if (delegates != null) {
        foreach (Feedback d in delegates) {
            d(value);
        }
    } else {
        _methodPtr.Invoke(_target, value);
    }
}
 
주목할 부분은 Delegate의 public static 메서드인 Remove 메서드를 사용하면 체인으로부터 델리게이트를 제거하는 것도 가능하다는 점이다.
Remove 메서드가 호출되면 내부의 델리게이트 리스트를 순회하면서 일치하는 _target과 _methodPtr 필드를 갖는 델리게이트 항목을 찾게 된다.
만약 지우려는 델리게이트 이외에 하나의 델리게이트 객체만이 남은 경우 해당 객체가 반환되고, 더 많은 객체가 남아있을 경우 위와 같이 새로운 객체를 만들고 _invocationList를 복사한뒤 델리게이트 체인 객체를 반환한다. 단일 델리게이트 항목이었다면 당연하게도 null을 반환한다. 
추가적으로 서술하면 Remove 메서드는 _target과 _methodPtr이 일치하는 항목이 여러번 등장하더라도 한 번에 하나씩만 제거한다.
 
 
C#의 델리게이트 체인에 대한 지원
 
private static void ChainDelegateDemo2(Program p)
{
    Console.WriteLine("----- Chain Delegate Demo 2 -----");
    Feedback fb1 = new Feedback(FeedbackToConsole);
    Feedback fb2 = new Feedback(FeedbackToMsgBox);
    Feedback fb3 = new Feedback(p.FeedbackToFile);
 
 
    Feedback fbChain = null;
    fbChain += fb1;
    fbChain += fb2;
    fbChain += fb3;
    Counter(1, 2, fbChain);
    Console.WriteLine();
 
 
    fbChain -= new Feedback(FeedbackToMsgBox);
    Counter(1, 2, fbChain);
}
 
 C# 개발자들의 편의를 위하여, C# 컴파일러는 델리게이트 타입의 인스턴스에  +=와 -= 연산자를 자동으로 오버로드해준다.
 이 연산자는 각각 Delegate.Combine과 Delegate.Remove 메서드를 자동으로 호출한다.
 이 연산자를 사용하는 것과 Delegate.Combine, Remove 메서드를 호출하는 것은 완전히 동일한 IL 코드를 생성한다.
 
 
델리게이트 체인 호출을 커스터마이징하는 방법
 
델리게이트 체인을 만들고 연결된 메서드들을 한 번에 호출하는 방법을 알아보았다.
이 방식은 간결하고 대부분의 경우에서 적합할지 모르겠으나,  상당한 제약이 있는 것 또한 사실이다.
 
예를 들면 콜백 메서드의 반환 값이 가장 마지막 것을 제외하고는 모두 소실된다는 문제가 있다.
혹은 메서드들을 실행하는 중에 예외가 발생하거나 메서드 실행이 너무 오래걸린다면? 알고리즘이 체인 내의 모든 항목을 호출해야 함에도 불구하고 그러지 못하고 중간에 방해를 받을 가능성이 있게 된다.
 
이 상황에 대비하기 위해서 MulticastDelegate 클래스는 인스턴스 메서드로 GetInvocationList라는 메서드를 제공한다.
이를 이용하면 체인의 각 델리게이트 항목을 명시적으로 호출할 수 있을 뿐만 아니라, 순회 방식을 변경할 수도 있다.
GetInvocationList는 함수 이름에서 알 수 있듯이 내부 필드인 _invocationList를 이용하여 Delegate 타입의 배열을 반환하는데, 만약 _invocationList가 null이라면 자기 자신만을 포함하는 배열을 반환한다.
 
 
 

이미 정의되어 있는 델리게이트 활용하기 (제네릭 델리게이트)
 
.NET Framework를 개발하기 시작할 때부터 사용한 MS의 델리게이트들이 존재한다.
MSCORLIB.DLL에만도 50여개 이상의 델리게이트들이 이미 정의되어 있다.
 
이제는 .NET Framework가 제네릭을 지원하기 때문에, 몇 종류의 일반화된 델리게이트들을 이용하여 최대 열여섯 개의 매개변수를 받는 메서드들을 모두 가리킬 수 있다.
 
public delegate void Action();
public delegate void Action<T>(T obj);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
...
public delegate void Action<T1, ..., T16>(T1 arg1, ..., T16 arg16);
 
아예 인자가 없는 경우부터 최대 열여섯개의 인자를 갖는 함수까지 가리킬 수 있는 Action 델리게이트를 지원한다.
Action 뿐만 아니라, .NET Framework는 열일곱가지 Func 델리게이트들도 제공하는데, 이 경우는 콜백 메서드가 반환 값을 가지는 경우를 대비하기 위한 것이다.
 
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T obj);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
...
public delegate TResult Func<T1, ..., T16, TResult>(T1 arg1, ..., T16 arg16);
 
개발자들이 델리게이트를 새롭게 정의하기보다는 이미 정의되어있는 이런 제네릭 델리게이트를 사용할 것을 권장한다.
이를 사용하면 시스템에 포함하는 타입의 숫자를 줄일 수 있고, 코딩을 더욱 간결하게 할 수 있다.
그러나 매개변수를 ref나 out 키워드로 정의하는 메서드를 가리키기 위해서는 여전히 고유의 델리게이트를 따로 정의해야한다.
그 외에도 params 키워드를 이용한 가변 인자나, 매개변수 기본값 설정, 제네릭 타입 인자에 제약 조건을 추가하려는 경우도 직접 델리게이트를 정의해야한다.
 
이런 델리게이트를 정의할 경우에는 공변성과 반공변성을 어떻게 활용할지를 잘 검토하여야 할 것이다.
 
 

델리게이트를 위한 C#의 문법적 편의사항
 
대부분의 개발자들에게 델리게이트 객체를 생성하여 메서드의 주소를 가리키도록 바인딩 하는 방식은 부자연스럽게 보인다.
하지만 CLR에서는 반드시 델리게이트 객체를 만들어 메서드를 포장하여 타입 안정성을 보장하면서 호출하기를 원한다.
 
다행히도 MS의 C# 컴파일러에서는 개발자들에게 델리게이트에 관한 몇 가지 문법적 편의사항을 제공한다.
다시 말하면 아래의 내용은 어디까지나 C#에 한정되는 것이며, 다른 컴파일러들은 이러한 편의를 제공해주지 않을 수도 있다.
 
#1 : 델리게이트 객체를 생성할 필요가 없다.
 
C#은 델리게이트 객체 생성을 하지 않고, 콜백 메서드의 이름을 직접 지정하는 것을 허용한다.
 
internal sealed class A {
    public static void CallbackWithoutNewingADelegateObject() {
        ThreadPool.QueueUserWorkItem(SomeAsyncTask, 5); // 함수 이름을 직접 지정.
    }
 
    private static void SomeAsyncTask(Object o) {
        Console.WriteLine(o);
    }
}
 
QueueUserWorkItem 메서드는 WaitCallback 델리게이트를 매개변수로 요구하지만, C# 컴파일러는 이러한 상황에서 무엇을 해야하는지 유추할 수 있으므로, WaitCallback 델리게이트 객체를 생성하는 작업을 생략할 수 있도록 해준다.
 
#2 : 콜백 메서드를 정의하지 않아도 된다. (람다 표현식)
 
internal sealed class A {
    public static void CallbackWithoutNewingADelegateObject() {
        ThreadPool.QueueUserWorkItem(obj => Console.WriteLine(obj), 5); // 람다 표현식으로 생성.
    }
}
 
앞의 코드를 람다 표현식을 이용하여 더 간단하게 작성할 수 있다.
컴파일러는 람다 표현식을 확인하면 자동으로 이를 private 메서드로 만들고, 델리게이트화 시켜준다.
이 메서드를 익명 메서드라고 하는데, 컴파일러가 자동으로 이름을 생성하기 때문에 개발자 쪽에서는 해당 메서드의 이름을 알 방법이 없기 때문이다.
 
하지만 ILDASM같은 도구를 사용하여 확인해보면 <CallbackWithoutNewingADelegateObject>b__0과 같이 생성했다는 것을 알 수 있다.
리플렉션을 사용하면 메서드의 이름을 문자열로 전달하여 해당 메서드에 접근할 수 있긴 하지만, C# 명세에 이 규칙을 정하고 있지 않기 때문에 이름은 컴파일러마다 달라질 수 있다.
또한 System.Runtime.ComplierServices.ComplierGeneratedAttribute 사용자 정의 특성을 이 익명 메서드에 추가하여 해당 메서드가 프로그래머가 아닌 컴파일러에 의하여 만들어졌다는 사실을 알 수 있다.
 
앞의 코드를 컴파일하면 C# 컴파일러가 다음과 같이 모드를 작성하게 된다.
 
internal sealed class A {
    [CompilerGenerated]
    private static WaitCallback <>9__CachedAnonymousMethodDelegate1;
 
    public static void CallbackWithoutNewingADelegateObject() {
        if (<>9__CachedAnonymousMethodDelegate1 == null) {
            <>9__CachedAnonymousMethodDelegate1 = new WaitCallback(<CallbackWithoutNewingADelegateObject>b__0);
        }
        ThreadPool.QueueUesrWorkItem(<>9__CachedAnonymousMethodDelegate1, 5);
    }
 
    [CompilerGenerated]
    private static void <CallbackWithoutNewingADelegateObject>b__0(object obj) {
        Console.WriteLine(obj);
    }
}
 
람다 메서드가 private으로 보호 되고 있고, 인스턴스의 멤버에 접근하지 않기 때문에 static 메서드로 선언되어있다.
물론 인스턴스 멤버를 참조하도록 할 수도 있다.
 
람다 표현식에는 몇가지 규칙이 있다. 차라리 따로 떼서 설명하지 왜 여기서 설명해서 분량이 늘어나는지는 모르겠지만 하여간...
 
// 매개변수가 없다면 ()로 표기한다.
Func<string> f = () => "Jeff";
 
// 한 개 이상의 매개변수를 받는다면 명시적으로 매개 변수의 타입을 지정할 수 있다.
Func<int, string> f2 = (int n) => n.ToString();
Func<int, int, string> f3 = (int n1, int n2) => (n1 + n2).ToString();
 
// 반대로 매개 변수의 타입을 컴파일러가 유추하게 할 수도 있다.
Func<int, string> f4 = (n) => n.ToString();
Func<int, int, string> f5 = (n1, n2) => (n1 + n2).ToString();
 
// 매개변수가 한 개만 있다면 () 기호를 생략할 수 있다.
Func<int, string> f6 = n => n.ToString();
 
// 만약 델리게이트가 ref / out 형태의 매개변수를 사용한다면 ref / out과 함께 매개변수의 타입과 이름을 모두 사용해야 한다.
Bar b = (out int n) => n = 5;
 
// 만약 메서드 본문을 하나 이상의 문장으로 구성한다면, 반드시 중괄호를 열고 닫아야 한다.
// 그리고 델리게이트가 반환 값을 요구한다면, 반드시 return 문을 본문 안에 기재해야 한다.
Func<int, int, string> f7 = (n1, n2) => { int num = n1 + n2; return num.ToString(); }
 
 
#3 : 클래스 내의 로컬 변수를 포장하여 명시적으로 콜백 메서드로 전달할 필요가 없다.
 
가끔 콜백 함수의 코드가 정의된 메서드 내의 로컬 매개변수나 변수를 참조해야하는 경우가 있다.
 
internal sealed class A {
    public static void UsingLocalVariableInTheCallbackCode(int numToDo) {
        int[] squares = new int[numToDo];
        AutoResetEvent done = new AutoResetEvent(false);
 
        for (int n = 0; n < squares.Length; ++n) {
            ThreadPool.QueueUserWorkItem(
                obj => {
                    int num = (int)obj;
 
                    squares[num] = num * num;
 
                    if (Interlocked.Decrement(ref numToDo) == 0)
                        done.Set();
                },
            n);
        }
 
        done.WaitOne();
 
        for (int n = 0; n < squares.Length; ++n) {
            Console.WriteLine("Index {0}, Square={1}", n, squares[n]);
        }
    }
}
 
람다 표현식에서는 numToDo라는 매개변수와 squares, done이라는 두 개의 로컬 변수를 참조하고 있다.
람다 표현식의 코드가 별도의 메서드로 정의되어 있다고 생각한다면, 어떻게 위와 같은 동작을 할 수 있도록 만들까?
유일한 방법은 새로운 도우미 클래스를 정의하여 참조하게 될 변수들을 그 안에 정의하고, UsingLocalVariableInTheCallbackCode메서드에서 도우미 클래스의 인스턴스를 생성/초기화 한뒤 델리게이트 객체 내부의 콜백 코드에 연결해야 할 것이다.
 
 
internal sealed class A {
    public static void UsingLocalVariableInTheCallbackCode(int numToDo) {
        
        WaitCallback callback1 = null;
 
        <>c_DisplayClass2 class1 = new <>c_DisplayClass2();
 
        class1.numToDo = numToDo;
        class1.squares = new int[class1.numToDo];
        class1.done = new AutoResetEvent(false);
        
        for (int n = 0; n < class1.squares.Length; ++n) {
            if (callback1 == null) {
                callback1 = new WaitCallback(class1.<UsingLocalVariableInTheCallbackCode>b__0);
            }
 
            ThreadPool.QueueUserWorkItem(callback1, n);
        }
 
        class1.done.WaitOne();
 
        for (int n = 0; n < squares.Length; ++n) {
            Console.WriteLine("Index {0}, Square={1}", n, class1.squares[n]);
        }
    }
 
    [ComplierGenerated]
    private sealed class <>c__DisplayClass2 : Object {
 
        public int[] squares;
        public int numToDo;
        public AutoResetEvent done;
 
        public <>c_DisplayClass2 { }
 
        public void <UsingLocalVariableInTheCallbackCode>b__0(object obj) {
            int num = (int)obj;
            squares[num] = num * num;
 
            if (Interlocked.Decrement(ref numToDo) == 0)
                done.Set();
        }
    }
}
 
람다 표현식을 사용하면 이런 동작들을 자동으로 만들어준다. 또한 도우미 클래스 객체의 생명주기를 참조하는 변수들의 생명주기와 동일하게 관리한다.
보통, 매개변수와 로컬 변수는 메서드 내에서 마지막으로 해당 변수들을 사용하는 시점까지 유지된다.
하지만 변수들을 필드로 바꾼다면 객체의 생명주기는 해당 객체가 필드들을 가지고 있는 동안 계속 유지되는 것으로 바뀐다.
 
리쳐 아저씨는 스스로 람다 표현식의 본문이 세 줄 이상이라면 직접 추가 메서드를 정의하기로 스스로 규칙을 정의했다고 하는데, 잘 모르겠다.
개인적으로는 재사용성이 있느냐 아니냐로 판단하는 규칙을 좋아한다.
 
 

델리게이트와 리플렉션
 
지금까지 델리게이트를 사용하기 위해서 메서드의 원형을 알아야 했으며, 이는 별 문제가 없었다.
 
internal delegate void Feedback(int value);
 
Feedback fb = something;
fb(value); // value는 int타입이겠지!
 
그러나 일부 특별한 상황에서는 개발자들이 이러한 정보들을 컴파일 시점에 확인할 방법이 마땅치 않은 경우가 있다.
예를 들면 11장의 EventSet 클래스에서 Dictonary<EventKey, Delegate> 타입으로 이벤트를 관리하는 경우다.
 
런타임에서 이벤트를 발생시키기 위해 Dictonary 내부에서 델리게이트 객체를 찾아 실행할 수 있었지만, 컴파일 시점에서는 실제로 델리게이트의 호출을 위해서 어떤 시그니쳐가 필요한지 알아내는 것이 불가능했다.
 
다행히, System.Reflection.MethodInfo의 CreateDelegate 메서드를 이용하여 컴파일 시점에 델리게이트를 충분히 알 수 없는 상황이여도 런타임에 델리게이트를 만들 수 있는 방법을 제공한다.
 
public abstract class MethodInfo : MethodBase {
 
    public virtual Delegate CreateDelegate(Type delegateType);
 
    public virtual Delegate CreateDelegate(Type delegateType, object target);
}
 
public abstract class Delegate {
 
    public object DynamicInvoke(params object[] args);
}
 
이를 사용하려면, 먼저 델리게이트로 포장하려는 메서드의 MethodInfo 객체를 얻어오고, CreateDelegate 메서드를 호출하여 매개변수인 delegateType으로 확인한 타입의 델리게이트를 객체를 생성한다.
System.Delegate의 DynamicInvoke를 이용하면 델리게이트 객체가 포장하고 있는 콜백 메서드를 호출할 수 있다.
컴파일 타임에는 알 수 없지만, 런타임에 파악할 수 있는 매개변수들을 모아 배열로 전달하고, 실제 호출 콜백 메서드의 매개 변수 정보와 호환이 가능하면 메서드를 호출, 그렇지 않으면 ArgumentException을 발생시킨다.
아래는 간단히 요약한 사용 방법이다.

 

 
using System;
using System.Reflection;
using System.IO;
 
 
internal delegate Object OneString(string s);
 
 
public static class DelegateReflection {
    // args example
    //    args[0] : OneString
    //    args[1] : Reverse
    //    args[2] : \"Hello there\"
    public static void Main(string[] args) {
        Type delType = Type.GetType(args[0]);
 
        Delegate d;
 
        try {
            MethodInfo mi = typeof(DelegateReflection).GetTypeInfo().GetDeclaredMethod(args[1]);
            d = mi.CreateDelegate(delType);
        }
        catch (ArgumentException) {
             return;
        }
 
        Object[] callbackArgs = new Object[args.Length - 2];
 
        if (d.GetType() == typeof(OneString)) {
            Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);
        }
 
        try {
            Object result = d.DynamicInvoke(callbackArgs);
        }
        catch (TargetParameterCountException) {
            return;
        }
    }
 
    private static Object Reverse(string s) {
        return new String(s.Reverse().ToArray());
    }
}