프로그래밍/C#

[C#] 대리자 (delegate)

Victory_HA 2021. 9. 26. 16:53

https://tapito.tistory.com/45 에서 가져온 글임을 밝힙니다.

이를 테면, C 언어의 함수 포인터에 해당하는 기능이 C#에도 있다는 거죠. 오히려 C 언어의 함수 포인터보다 기능이 더 강화되었습니다.

이번 시리즈에서는 C#이 갖고 있는 3가지 기능. 대리자, 무명 메서드, 람다식에 대해 알아보겠습니다.

1. 대리자(Delegate)

모든 파생된 기법의 근원

C# 초기 버전부터 있던 기능입니다.

C 언어의 함수 포인터를 그대로 차용한 거나 다름없죠. 메서드의 위치를 간직하고 있으면서 그 메서드를 대신 실행해 주는 역할을 합니다. 이게 왜 필요한가? 이렇게 이해하시면 간단합니다.

해당 메서드를 직접 호출 할 수 없는 경우, 예를 들면 외부 어셈블리에 있다거나, 그 메서드가 private라던가, 아니면 호출해야 하는 메서드가 런타임 도중 동적으로 바뀌는 경우 등.

그래서 바깥에서 이 함수를 직접 호출할 수 없을 때, 대리자에게 "야! 저기 국경너머 저 함수 보이지? 여기 매개변수 줄 테니까 넘어가서 실행 좀 하고 와라!" 내지는 "야! 대리자! 나 지금 귀찮으니까 저기 있는 함수 네가 대신 좀 실행시켜라!" 뭐 이런 뜻이라고 보면 됩니다. ㅎㅎ

그럼 대리자를 어떻게 사용하느냐? 예제를 한번 봅시다.

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CsTest
{
    class Program
    {
        delegate int DelegateMethod(int a, int b); // 이런 형식의 함수를 대신 호출해 주겠다의 의미입니다.

        static int Average(int a, int b)
        {
            return (int)((a + b) / 2);
        }

        static void Main(string[] args)
        {
            // Average 메서드를 직접 호출하는 대신 dm이라는 인스턴스를 통해 간접 호출합니다.
            DelegateMethod method = Average; 
            int a, b;

            Console.Write("값 a를 입력하세여 >> ");
            a = int.Parse(Console.ReadLine());
            Console.Write("\n");

            Console.Write("값 b를 입력하세여 >> ");
            b = int.Parse(Console.ReadLine());
            Console.Write("\n");

            // 여기서 Average가 간접 호출 됩니다.
            Console.WriteLine("(a = {0})과 (b = {1})의 연산 결과는 {2}입니다.", a, b, method(a, b));

            Console.WriteLine("끝.");
            Console.ReadLine();
        }
    }
}

실행 결과는 다음과 같습니다.

Average메서드를 직접 실행한 것과 method를 거쳐서 실행한 것의 결과는 같습니다. 그러나 내부적으로는 이런 차이가 있죠.

대리자의 작동 과정을 보았으니 이제 대리자의 선언과 실제 사용 부분을 볼까요? DelegateMethod는 이렇게 선언 되었습니다.

 delegate int DelegateMethod(int a, int b);

이 선언을 인간의 언어로 풀이하자면
int형 매개변수 2개를 받아서 int형으로 반환하는 함수 꼴을 DelegateMethod라고 이름 붙이겠다
이런 의미입니다. 즉 붕어빵으로 치면 붕어빵 틀을 만든 거죠. 이제 이 틀을 이용합니다.

DelegateMethod method = Average;

method라는 대리자를 하나 만들고 여기에 Average라는 메서드를 채워 넣습니다.
이때 Average 메서드는 int형 매개변수를 2개 받고, 반환 값의 형태가 int이니까
위에서 선언했던 DelegateMethod로 이름 붙인 형식에 꼭 들어맞습니다.
따라서 별다른 에러 없이 Average 메서드 주소가 method 대리자 속에 폭 안깁니다.

이제 실행하는 일만 남았습니다.

Console.WriteLine("(a = {0})과 (b = {1})의 연산 결과는 {2}입니다.", a, b, method(a, b));

method(a, b) 을 보면,

특이하게도 method 인스턴스 그 자체가 마치 함수처럼 호출이 되고 있습니다.
대리자이기에 가능한 거죠.

그리고 method는 Average의 메서드 정보를 품고 있으니까 Average를 실행하게 됩니다.

여기까지가 C의 것과 다를 바 없는 함수 포인터로서의 대리자였다면
이제부터는 C#(더 나아가서 .NET에만)에는 있는 아주 특별한 대리자의 성질을 보여드리겠습니다.

우선 예제부터 볼까요?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CsTest
{
    class Program
    {
        delegate void DelegateMethod(int a);

        static void Method1(int a)
        {
            Console.WriteLine("Method1이 호출되었습니다. a == {0}", ++a);
        }
        static void Method2(int a)
        {
            Console.WriteLine("Method2이 호출되었습니다. a == {0}", ++a);
        }
        static void Method3(int a)
        {
            Console.WriteLine("Method3이 호출되었습니다. a == {0}", ++a);
        }
        static void Method4(int a)
        {
            Console.WriteLine("Method4이 호출되었습니다. a == {0}", ++a);
        }
        static void Method5(int a)
        {
            Console.WriteLine("Method5이 호출되었습니다. a == {0}", ++a);
        }
        static void Method6(int a)
        {
            Console.WriteLine("Method6이 호출되었습니다. a == {0}", ++a);
        }
        static void Method7(int a)
        {
            Console.WriteLine("Method7이 호출되었습니다. a == {0}", ++a);
        }
        static void Method8(int a)
        {
            Console.WriteLine("Method8이 호출되었습니다. a == {0}", ++a);
        }
        static void Method9(int a)
        {
            Console.WriteLine("Method9이 호출되었습니다. a == {0}", ++a);
        }

        static void Main(params string[] args)
        {
            DelegateMethod method = null;

            method += Method1;
            method += Method2;
            method += Method3;
            method += Method4;
            method += Method5;
            method += Method6;
            method += Method7;
            method += Method8;
            method += Method9;

            method(0);

            Console.WriteLine("끝.");
            Console.ReadLine();
        }
    }
}

결과는 다음과 같습니다.

C의 함수 포인터에는 없었던 기가 막힌 기능입니다.
바로 한 대리자가 여러 메서드를 끌어 안고 있다가 연쇄적으로 몽땅 실행할 수 있다는 거죠.
단, 이 때 조건은 반환 형이 void이어야 된다는 겁니다.

생각해 보세요.여러 개의 메서드가 같은 매개변수를 받기는 했지만, 반환 값이 제각각 다르다면
그 다른 것 중에서 무엇을 선택해야 할 지가 .NET Framework의 입장에서는 모호합니다.그렇기 때문에 여러 개의 메서드를 대리자 안에 넣고 싶다면 반드시 반환형이 void이어야 합니다.

위의 예제를 그림으로 표현하면 다음과 같습니다.

반대로 연쇄 호출 대상에서 메서드를 뺄 수도 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CsTest
{
    class Program
    {
        delegate void DelegateMethod(int a);

        static void Method1(int a)
        {
            Console.WriteLine("Method1이 호출되었습니다. a == {0}", ++a);
        }
        static void Method2(int a)
        {
            Console.WriteLine("Method2이 호출되었습니다. a == {0}", ++a);
        }
        static void Method3(int a)
        {
            Console.WriteLine("Method3이 호출되었습니다. a == {0}", ++a);
        }
        static void Method4(int a)
        {
            Console.WriteLine("Method4이 호출되었습니다. a == {0}", ++a);
        }
        static void Method5(int a)
        {
            Console.WriteLine("Method5이 호출되었습니다. a == {0}", ++a);
        }
        static void Method6(int a)
        {
            Console.WriteLine("Method6이 호출되었습니다. a == {0}", ++a);
        }
        static void Method7(int a)
        {
            Console.WriteLine("Method7이 호출되었습니다. a == {0}", ++a);
        }
        static void Method8(int a)
        {
            Console.WriteLine("Method8이 호출되었습니다. a == {0}", ++a);
        }
        static void Method9(int a)
        {
            Console.WriteLine("Method9이 호출되었습니다. a == {0}", ++a);
        }

        static void Main(params string[] args)
        {
            DelegateMethod method = null;

            method += Method1;
            method += Method2;
            method += Method3;
            method += Method4;
            method += Method5;
            method += Method6;
            method += Method7;
            method += Method8;
            method += Method9;

            method -= Method2;
            method -= Method4;
            method -= Method6;
            method -= Method8;

            method(0);

            Console.WriteLine("끝.");
            Console.ReadLine();
        }
    }
}

실행 결과는 다음과 같습니다.

네. 정말 재미있는 기능이죠.
그렇다면 이런 건 어떨까요?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CsTest
{
    class Program
    {
        delegate void DelegateMethod(int a);

        static void Method1(int a)
        {
            Console.WriteLine("Method1이 호출되었습니다. a == {0}", ++a);
        }
        static void Method2(int a)
        {
            Console.WriteLine("Method2이 호출되었습니다. a == {0}", ++a);
        }
        static void Method3(int a)
        {
            Console.WriteLine("Method3이 호출되었습니다. a == {0}", ++a);
        }
        static void Method4(int a)
        {
            Console.WriteLine("Method4이 호출되었습니다. a == {0}", ++a);
        }
        static void Method5(int a)
        {
            Console.WriteLine("Method5이 호출되었습니다. a == {0}", ++a);
        }
        static void Method6(int a)
        {
            Console.WriteLine("Method6이 호출되었습니다. a == {0}", ++a);
        }
        static void Method7(int a)
        {
            Console.WriteLine("Method7이 호출되었습니다. a == {0}", ++a);
        }
        static void Method8(int a)
        {
            Console.WriteLine("Method8이 호출되었습니다. a == {0}", ++a);
        }
        static void Method9(int a)
        {
            Console.WriteLine("Method9이 호출되었습니다. a == {0}", ++a);
        }
        static void Method10(int a)
        {
            Console.WriteLine("Method10이 호출되었습니다. a == {0}", ++a);
        }

        static void Main(params string[] args)
        {
            DelegateMethod method = null;

            method += Method1;
            method += Method2;
            method += Method3;
            method += Method4;
            method += Method5;
            method += Method6;
            method += Method7;
            method += Method8;
            method += Method9;

            method -= Method2;
            method -= Method4;
            method -= Method6;
            method -= Method8;

            method -= Method10;

            method(0);

            Console.WriteLine("끝.");
            Console.ReadLine();
        }
    }
}

Method10이라는 메서드가 새로 만들어졌습니다.
그러나 이거는 method 대리자 입장에서는 자신에게 추가되지 않았기 때문에 '듣보잡'입니다.
그런데 중간에 method -= Method10; 이런 코드가 있습니다.
원래 갖고 있지 않던 것을 빼라는 거죠. 그러면 이때는 .NET Framework가 어떤 반응을 보일까요?

네. 그냥 잘 실행 됩니다. 없는 거를 빼라는 구문을 만나면
그냥 그것에 대해서는 아무 작업도 하지 않고 스므스하게 넘어가죠.

바로 이러한 대리자를 이용해 탄생한 개념이 '이벤트'입니다.
Windows API의 경우 '이벤트'는 윈도우 프로시져로 들어오는 WM_PAINT, WM_DESTROY 등의
메시지 상수에 따라 작업을 처리했지만 완전히 객체지향화 된 C#에서는 대리자를 활용해서
'이벤트'라는 독립된 구문을 하나 만들어버렸습니다.

예를 들어서 '창 클릭'이라는 동작에 대해 처리해야 할 것이 있다면,
'창 클릭'이라는 대리자를 특별히 이벤트라는 표시를 해서 선언하고
그 대리자에게 창 클릭 시 처리한 함수들을 몰아 넣는 거죠.
그러면 실제 창이 클릭 될 때, 대리자가 품고 있던 메서드들이 연쇄적으로 모두 실행 됩니다.

이벤트를 선언하는 방법은 너무나 간단합니다. 아래와 같은 선언을 클래스 안에다 넣으면 되죠.

public event DelegateMethod WindowClick;

그리고 여느 대리자들과 마찬가지로 +=와 -= 연산자를 이용해 메서드를 몰아 넣을 수도,
부분 뺄 수도 있습니다. 실행하는 방법도 위와 동일합니다.

여기까지가 C# 초기 버전부터 있었던 대리자 기능에 대한 설명이었습니다.
다음에는 무명 메서드에 대해 설명하겠습니다. 읽어주셔서 감사합니다.