ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 17장. 델리게이트
    CLR Via C# 2022. 3. 9. 15:21

    델리게이트는 .NET Framework의 콜백 함수 메커니즘이다. 이는 네이티브 C/C++과 달리 타타입 안정성을 준수하는 메커니즘이다.

    using System;
    using System.Windows.Forms;
    using System.IO;
    
    internal delegate void Feedback(int value);
    
    public sealed class Program{
        public static void main() {
            Program p = new Program();
            Feedback fb1 = new Feedback(p.FeedbackToFile);
            fb1(1);
            Feedback fb2 = new Feedback(FeedbackToConsole);
            fb2(2);
        }
    
        private void FeedbackToFile(int value){
            ...
        }
    
        private static void FeedbackToConsole(int value){
            ...
        }
    }

    int타입 매개변수를 받고 반환 값이 없는 Feedback이라는 델리게이트 타입을 하나 정의했다.

    이 델리게이트에 메서드를 지정할 때는 타입 안정성을 준수한다. 또한 공변성과 반공변성을 허용한다. 공변성이란 델리게이트의 원형에서의 반환 타입의 하위 클래스를 바인드할 수 있음을 나타내는 성질이다. 반공변성이란 델리게이트의 원형에서의 매개변수의 상위 클래스를 바인드할 수 있음을 나타내는 성질이다. 이는 값 타입이나 반환 타입이 없음을 나타내는 void 타입에 대해서는 적용되지 않는다.

    delegate Object MyCallback(FileStream s);
    // 이 델리게이트 타입을 이용하여 다음과 같은 메서드를 지정하는 것이 가능하다. 
    String SomeMethod(Stream s);

    델리게이트의 내부적 동작

    다음과 같이 델리게이트를 정의하면

    internal delegate void Feedback(int value);

    내부적으로 컴파일러는 실제로는 아래와 같이 완전한 클래스로 새로 정의한다.

    internal class Feedback : System.MulticastDelegate {
        // 생성자 
        public Feedback(Object @object, IntPtr method);
    
        // 소스 코드에서 정의했던 것과 동일한 프로토타입으로 정의한다.
        public virtual void Invoke(int value);
    
        // 콜백을 비동기적으로 호출할 수 있도록 해주는 메서드들이다. 
        public virtual IAsyncResult BeginInvoke(int value,
            AsyncCallback callback, Object @object);
        public virtual void EndInvoke(IAsyncResult result);
    }

    여기선 생성자와 Invoke 메서드만 다루겠다. BeginInvoke와 EndInvoke 메서드는 비동기 프로그래밍 모델과 관련된 내용으로 27장에서 다루겠다.

    모든 델리게이트 타입이 MulticastDelegate 타입을 상속하기 때문에, MulticastDelegate의 필드, 속성, 메스드도 같이 상속을 받는다. 아래는 중요한 세 개의 내부 필드이다.

    필드 타입 설명

    _target System.Object 델리게이트 객체가 정적 메서드를 포장하는 경우, 이 필드는 null을 가지게 된다. 만약 델리게이트 객체가 인스턴스 메서드를 포장하는 경우, 이 필드는 콜백 메서드가 호출되어야 할 대상 객체에 대한 참조를 가리키게 된다. 바꾸어 말하면, 이 필드는 인스턴스 메서드에 암묵적으로 항상 전달되어야 하는 this 매개변수와 같다.
    _methodPtr System.IntPtr CLR이 콜백으로 호출해야 하는 메서드를 식별하는 내부 정수 필드다.
    _invocationList System.Object 이 필드는 보통 null로 설정된다. 이 장 후반부에서 설명할 델리게이트 체인을 만들기 위한 배열을 가리킬 수 있다.

    주목할 만한 부분은 모든 델리게이트가 두 개의 매개변수를 받는 생성자를 가지고 있다는 점인데, 객체에 대한 참조와 콜백 메서드를 식별하는 정수 값을 매개변수로 전달 받는다. 하지만 C#컴파일러는 IntPtr 값(메서드의 명세)만 method 매개변수로 전달하면 알아서 어떤 객체와 메서드가 참조되어야 하는지를 확인한다.

    또한 다음과 같이 델리게이트 객체를 이용해 델리게이트에 등록된 메서드를 호출한다.

            //Feedback fb1 = new Feedback(p.FeedbackToFile);
            fb1(1);

    사실 위의 fb1은 델리게이트 이름일 뿐 함수의 이름이 아니다. 위에 코드는 내부적으로 다음과 같이 바꾸어 코드가 생성된다.

            fb1.Invoke(1);

    또는 Invoke를 명시적으로 사용할 수도 있다.

    델리게이트를 사용하여 여러 메서드를 호출하기(메서드 연결하기)(체인)

    Delegate 클래스의 public 정적 메서드인 Combine 메서드를 사용하여 여러 델리게이트를 연결할 수 있다.

    fbChain = (Feedback) Delegate.Combine(fbChain, fb1);

    fbChain에 메서드가 2개 이상 연결돼 있을 때, 메서드를 추가하면 새로운 델리게이트 객체를 생성해 연결된 있던 메서드에 새로 추가된 메서드까지 연결시켜 _invocationList의 배열로 저장한다. 이전에 fbChain이 가리키던 객체와 그 객체가 가리키던 배열은 가비지 컬렉터의 대상이 된다. 메서드가 하나일 때 Combine을 시도하면 null이었던 _invocationList에 기존 메서드 하나 + Combine으로 전달된 메서드를 연결해 배열로 저장한다. 여기서 배열에 저장하는 것은 메서드의 주소가 아닌 합치는 델리게이트의 객체가 가리키고 있는 대상을 가리키도록 초기화한다.(_target, _methodPtr, _invocationList 를 가지고 있는 객체)

    Combine의 반대로 Remove로 메서드 연결을 제거할 수 있다.

    fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToConsole));

    Remove로 제거할 때는 일치하는 필드를 다수 발견하더라도 한 번에 하나씩만 제거한다. 만약 체인이 아닌 단일 델리게이트 항목에 대해서 이 메서드를 호출한 경우 null을 반환한다.

    C#은 편의성을 고려해 +=와 -=연산자를 자동으로 오버로드해준다. Combine을 +=로 Remove를 -=로 치환해 사용할 수 있다.

    fbChain += fb1;
    fbChain -= new Feedback(FeedbackToConsole);

    또한 체인의 요소 하나하나를 직접 제어할 수 있도록 델리게이트 체인으로부터 델리게이트 객체의 배열을 얻어올 수 있는 GetInvocationList 인스턴스 메서드를 제공한다.

    public abstract class MulticastDelegate : Delegate {
        public sealed override Delegate[] GetInvocationList();
    }
    Delegate[] arrayOfDelegates = fbChain.GetInvocationList();

    이미 정의되어 있는 델리게이트 활용하기(제네릭 델리게이트)

    .Net Framework는 제네릭을 지원하기 때문에, 몇 종류의 일반화된 델리게이트들(System 네임스페이스에 정의되어 있다)을 이용해서 최대 열여섯 개의 매개변수를 받는 메서드들을 모두 가리킬 수 있다.

    public delegate void Action();
    public delegate void Action<T>(T arg);
    public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
    ...
    public delegate void Action<T1, ..., T16>(T1 arg1, ..., T16 arg16);
    
    // 콜백 메서드가 반환 값을 가지는 경우를 대비하기 위한 것이다.
    public delegate TResult Func(TResult);
    public delegate TResult Func<T, TResult>(T arg);
    public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
    ...
    public delegate TResult Func<T1, ..., T16, TResult>(T1 arg1, ..., T16 arg16);

    이제 개발자들이 새로이 델리게이트 타입들을 정의하기 보다는 이미 정의되어 있는 이 같은 델리게이트 타입을 사용할 것을 권고하고 있다. 이렇게 하면 시스템에 포함하는 타입의 숫자를 줄일 수 있고, 코딩을 더욱 간결하게 할 수 있다. 하지만 매개변수를 ref나 out키워드로 정의하는 메서드를 가리키기 위해서는 여전히 고유의 델리게이트를 따로 정의해야만 한다.

    delegate void Bar(ref int z);

    또한 C#의 params 키워드를 사용하여 가변 인자를 델리게이트를 통해서 받기를 원할 때에도 직접 델리게이트를 정의해야 하며, 델리게이트의 매개변수에 기본값을 지정하기를 원하거나, 델리게이트의 제네릭 타입 인자에 제약조건을 추가하려는 경우에도 직접 델리게이트를 정의해야만 한다.

    C#에서의 문법적 편의사항

    • 델리게이트 객체를 생성할 필요가 없다 : 델리게이트 객체 생성을 하지 않고 콜백 메서드의 이름을 직접 지정하는 것을 허용한다.
    • 콜백 메서드를 정의하지 않아도 된다 : 콜백 메서드 대신 람다 표현식을 사용하면 된다. 람다 표현식은 다음과 같이 사용한다. 컴파일러는 람다 표현식을 확인하면 자동으로 해당 클래스에 새로운 private 메서드를 하나 추가한다. 이 메서드를 익명 메서드라고 부른다. 람다 표현식은 다음과 같이 사용한다.
    ThreadPool.QueueUserWorkItem( obj => Console.WriteLine(obj), 5);
    • 클래스 내의 로컬 변수를 포장하여 명시적으로 콜백 메서드로 전달할 필요가 없다 : 람다 표현식을 사용해 클래스 내의 로컬 변수를 참조하면 컴파일러는 도우미 클래스를 정의해서 콜백 코드가 차조하는 변수들을 그 안에 포함한다. 만들어진 도우미 클래스의 인스턴스를 생성해 개별 필드를 콜백 코드가 참조하는 로컬 변수들을 사용하여 초기화하고, 델리게이트 객체를 하나 만들어 도우미 객체 내의 콜백 코드에 연결한다.

    델리게이트와 리플렉션

    지금까지 다룬 내용은 델리게이트를 사용하기 위해서는 메서드의 프로토타입을 알아야 하며, 이를 통해서 호출이 가능하다는 것이었다.

    그러나 일부 특별한 상황에서는 개발자들이 이러한 정보들을 컴파일 시점에 확인할 방법이 마땅치 않은 경우가 있다.

    다행스럽게도, System.Reflection.MethodInfoCreateDelegate라는 메서드를 제공하여 실행할 때 델리게이트를 만들 수 있는 방법을 제공한다. 델리게이트를 만든 이후에는 DelegateDynamicInvoke 메서드를 사용하여 호출할 수 있다.

    Type delType = Type.GetType(args[0]);
    Delegate d;
    try {
        MethodInfo mi = typeof(DelegateReflection).GetTypeInfo().
                            GetDeclaredMethod(args[1]);
        d = mi.CreateDelegate(delType);
        Object result = d.DynamicInvoke(args[2]);
    }
    catch(Exception){
        ...
    }

     

     

    ※델리게이트 예시를 쉽게 설명해 놓은 좋은 글 링크

    https://itmining.tistory.com/43

    'CLR Via C#' 카테고리의 다른 글

    19장. Null 값 타입  (0) 2022.06.21
    18장. 사용자 정의 특성  (0) 2022.03.16
    16장. 배열  (0) 2022.02.28
    15장. 열거 타입과 비트 플래그  (0) 2022.02.23
    14장. 문자, 문자열, 텍스트 사용하기  (0) 2022.02.21
Designed by Tistory.