언어/C#

C# - 델리게이트

tsyang 2021. 2. 21. 23:56

.NET Framwork는 콜백 함수 메커니즘을 델리게이트라는 형태로 노출한다. 델리게이트는 타입 안정성을 보장함으로써 C++등 다른 언어의 콜백 메커니즘은 다르게 더 강력한 기능을 제안한다. (그만큼 성능은 떨어질수도)

 

델리게이트


네이티브 C/C++에서 비멤버 함수의 주소는 단지 메모리 주소일 뿐이다. 이 주소는 다른 정보(매개변수, 반환 타입, 호출 규칙 등)를 일절 포함하지 않는다. 결국 타입 안정성이 없다. (대신 가볍고 빠르게 동작한다.)

 

.NET Framework는 델리게이트라는 타입 안정성을 준수하는 메커니즘을 제공한다. 

 


델리게이트 정의

delegate void SomeDel(int value);

public class Program
{
    public static void Main()
    {
        StaticDelegateFunc(1, null);
        StaticDelegateFunc(2, new SomeDel(Program.WriteConsole));
        StaticDelegateFunc(3, WriteMsgBox); //new SomeDel()은 생략가능
    }

    private static void StaticDelegateFunc(int val, SomeDel del)
    {
        if (del != null)
            del(val);
    }


    private static void WriteConsole(int val)
    {
        Console.WriteLine("val = " + val);
    }

    private static void WriteMsgBox(int val)
    {
        MessageBox.Show("val = " + val);
    }
}

 

SomeDel 델리게이트를 정의한뒤 Main에서 델리게이트를 사용하고 있는데 new SomeDel(함수) 이런식으로 호출하고 있다. 보통 new SomeDel 부분은 빼고 함수만 넘기는데 원형은 이거다. 왜냐면 컴파일러는 델리게이트의 정의를 보고 새로운 클래스를 정의해서 사용하기 때문이다. 이건 아래 델리게이트 내부구조에서 더 다룬다.

 

아무튼 위의 코드는 타입 안정성을 준수한다. 컴파일러는 WriteConsole등의 메서드가 SomeDel 델리게이트가 정의하는 원형과 호환성이 있는지를 검사한다. 

 

또한 C#과 CLR은 메서드를 델리게이트에 바인딩할 때 참조 타입에 대한 공변성과 반공변성을 지원한다. 공변성은 반환 타입의 자식 타입을 반환할 수 있는 성질이며 반공변성은 매개변수의 부모타입을 매개변수로 받을 수 있는 성질이다. 값타입의 경우는 이 둘을 지원하지 않는데, 참조 타입의 경우 메모리 구조는 항상 포인터로 대표되지만 값타입은 메모리 구조가 각기 다르기 때문이다.

 

 

델리게이트 내부구조


delegate void SomeDel(int value);

 

위와 같은 코드를 컴파일러가 만나게 되면, 컴파일러는 델리게이트를 아래와 같은 클래스로 새로 정의한다.

 

class SomeDel : System.MulticastDelegate
{
    //생성자
    public SomeDel(Object @object, IntPtr method);

    //소스코드에서 정의한것과 동일한 프로토타입
    public virtual void Invoke(int value);

    //비동기 호출 관련 메서드
    public virtual IAsyncResult BeginEnvoke(int val, AsyncCallback callback, Object @object);
    public virtual void EndInvoke(IAsyncResult result);
}

 

모든 델리게이트는 System.MulticastDelegate를 상속하며 다시 MulticastDelegate는 System.Delegate를 상속한다. MulticastDeletgate는 다음과 같은 필드를 포함한다.

필드 타입 설명
_target System.Object 콜백 메서드가 호출되어야 할 대상 객체에 대한 참조를 가리킨다. 만약 정적 메서드라면 null을 가진다.
_methodPtr System.IntPtr CLR이 콜백으로 호출해야 하는 메서드를 식별하는 내부 정수 필드
_invocationList System.Object 델리게이트 체인을 위한 배열

 

또 컴파일러가 다음과 같은 코드를 만나면,

del(val);

 

다음과 같이 코드를 바꾼다

del.Invoke(val);

 

 

델리게이트 체인


 

구조

var p = new Program();

SomeDel sd1 = new SomeDel(WriteConsole);
SomeDel sd2 = new SomeDel(WriteMsgBox);
SomeDel sd3 = new SomeDel(p.WriteFile);

SomeDel sdChain = null;                             // A.
sdChain = (SomeDel) Delegate.Combine(sdChain, sd1);  // B.
sdChain = (SomeDel) Delegate.Combine(sdChain, sd2);  // C.
sdChain = (SomeDel) Delegate.Combine(sdChain, sd3);  // D.

sdChain = (SomeDel) Delegate.Remove(sdChain,         
            new SomeDel(WriteMsgBox));
            
 //////////////////////////////////////////////////////////////////           
 //아래의 코드와 동일하다
 /////////////////////////////////////////////////////////////////

var p = new Program();

SomeDel sd1 = new SomeDel(WriteConsole);
SomeDel sd2 = new SomeDel(WriteMsgBox);
SomeDel sd3 = new SomeDel(p.WriteFile);

SomeDel sdChain = null;                             // A.
sdChain += sd1;                                     // B.
sdChain += sd2;                                     // C.
sdChain += sd3;                                     // D.

sdChain -= new SomeDel(WriteMsgBox);

위와 같은 코드가 있다.

 

ABCD에서의 델리게이트 변수의 상태는 다음과 같다.

 

 

마지막 D를 보면 새로운 SomeDel 객체가 생성되는것을 볼 수 있다. 원래 있던 객체는 더 이상 사용되지 않으며 GC에 의해 수거된다.

 

 


Invoke

Invoke 메서드는 다음과 유사한 형태로 구성되어있다고 볼 수 있다. (의사코드임)

 

public void Invoke(int val)
{
    Delegate[] delegateSet = _invocationList as Delegate[];
    if (delegateSet != null)
    {
        foreach (var d in delegateSet)
            d(val);
    }
    else
    {
        _methodPtr.Invoke(_target, val);
    }
}

 


Remove

Remove메서드가 호출되면 내부의 델리게이트 배열을 순회하면서 _target과 _methodPtr이 일치하는 델리게이트 항목을 찾는다. 그 뒤 새로운 델리게이트 객체를 생성한 뒤, 해당 객체의 _invocationList 배열에 제거하려는 항목을 제외한 나머지 델리게이트 항목들을 추가한 뒤 이 객체를 반환한다. (단, 제거하려는 객체를 제외하고 항목이 하나라면 그대로 그 항목을 반환) 만약 체인이 아닌 단일 델리게이트 항목에서 이 메서드를 호출한 경우 null을 반환한다. 주의할 점은 _target과 _methodPtr이 일치하는 항목이 여러 번 등장해도 한 번의 remove당 하나씩만 제거한다.

 


연산자 오버로드 (+=, -=)

 

C# 컴파일러는 델리게이트 타입의 인스턴스에 +=와 -=연산자를 자동으로 오버로드 해준다. 이 연산자들은 각각 Delegate.Combine과 Delegate.Remove 메서드를 호출한다. 

 


 

델리게이트 반환과 체인 호출 커스터마이징

 

델리게이트 체인의 대표적인 제약은 콜백 메서드의 반환 값이 가장 마지막 것을 제외하고는 모두 소실된다는 점이다. 그뿐 아니라 델리게이트 체인 내의 항목들을 실행하는 중간에 방해를 받는다면 나머지는 정상적으로 실행되지 못할 것이다.

 

이런 상황에 대비하기 위해 MulticastDelegate 클래스는 인스턴스 메서드로 GetInvocationList라는 메서드를 제공한다. 이 메서드를 이용하면 체인의 각 델리게이트 항목에 접근할 수 있다.

 

    Delegate[] delArr = sdChain.GetInvocationList();

    foreach(SomeDel del in delArr)
    {
        del.Invoke(3);
    }

 

 

기타 - 제네릭, 람다


  • 델리게이트의 제네릭 버전의 Action, Func이다. 
  • C#에서는 코드에 람다 표현식을 사용하면 컴파일러가 자동으로 이를 델리게이트로 인지한다. 그리고 나서 클래스에 새로운 private 메서드를 추가한다. 이를 익명 메서드라고 부른다. 메서드의 이름은 C#에서 사용할수 없는 <등의 기호를 이용해 메서드 이름이 겹치지 않도록 한다. 
  • 익명 메서드는 private로 생성되며 코드가 인스턴스 멤버에 접근하지 않으면 static으로 접근하면 인스턴스 메서드로 정의한다. (static이 인스턴스 메서드보다 빠르기 때문이다. 왜냐면 메서드 look-up 비용이 적어서)
  • 제프리 리처는 세 줄 이상의 코드는 람다를 안쓰고 직접 메서드를 추가하기로 했다고 한다.

'언어 > C#' 카테고리의 다른 글

C# Nullable, Null 결합 연산자  (0) 2021.03.06
C# 사용자 정의 특성  (0) 2021.02.28
C# - 배열  (0) 2021.02.10
C# - 문자열 - 1  (0) 2021.02.02
C# 인터페이스  (0) 2021.01.27