[C#] 델리게이트(Delegate)와 이벤트(Event)

Running boy·2023년 8월 6일
0

컴퓨터 공학

목록 보기
9/36

델리게이트(Delegate)

메서드를 값으로서 가리킬 수 있는 타입을 뜻하며 C# 클래스의 확장 기능 중 하나이다.

아래와 같은 문법으로 정의할 수 있다.

[접근 제한자] delegate [대상 메서드의 반환 타입] [식별자]([대상 메서드의 매개변수 목록1]...);
// ※델리게이트의 식별자를 정할 때 "~Delegate"와 같이 정하는 것이 관례이다.

C/C++에서는 델리게이트를 '함수 포인터'라고 부르기도 하는데 C# 9.0부터 공식적으로 해당 기능이 추가되었다.


델리게이트는 타입이다. 즉 메서드의 인자, 반환값, 클래스의 멤버로 사용될 수 있다.

델리게이트를 사용하기 위해선
1. delegate 키워드를 사용해서 타입을 정의하고
2. 정의한 타입으로 메서드를 담을 변수를 선언해야 한다.

class Program
{
    class Dog
    {
        string name;

        public Dog(string name)
        {
            this.name = name;
        }

        public void Bark()
        {
            Console.WriteLine(name + " Bark!");
        }
    }

    delegate void DogDelegate(); // 1. delegate 키워드를 사용해서 타입을 정의하고

    static void Main(string[] args)
    {
        Dog puppy = new Dog("Puppy");
        Dog corgi = new Dog("Corgi");

        DogDelegate barks = puppy.Bark; // 2. 정의한 타입으로 메서드를 담을 변수를 선언해야 한다.
        barks += corgi.Bark;

        barks();

        barks -= corgi.Bark;

        barks();
    }
}

// 출력:
// Puppy Bark!
// Corgi Bark!
// Puppy Bark!

위의 예시에서 +=, -=과 같은 연산을 통해 하나의 델리게이트 변수가 여러개의 메서드를 가리키게 할 수 있는 이유는 델리게이트가 'System.MulticastDelegate' 타입을 상속받은 클래스이기 때문이다.

delegate 키워드는 이런 복잡한 클래스 구현을 생략한 간편 표기법을 제공한다.

단 -=을 사용할 경우 델리게이트 값이 null이 될 수 있다. 이 경우 오작동을 일으킬 수 있으므로 유효성 검사를 해줘야 한다.


이벤트(Event)

델리게이트의 사용 패턴을 일반화하여 더 사용하기 편하게 만든 것으로, 주로 콜백 메서드 구현에 사용된다.

class PrimeCallbackArg : EventArgs // 콜백 값을 담는 클래스
{
    public int Prime;
    public PrimeCallbackArg(int prime)
    {
        this.Prime = prime;
    }
}

class PrimeGenerator
{
    public event EventHandler PrimeGenerated;

    public void Run(int limit)
    {
        for (int i = 2; i <= limit; i++)
        {
            if (IsPrime(i) == true && PrimeGenerated != null)
            {
                PrimeGenerated(this, new PrimeCallbackArg(i));
            }
        }
    }

    private bool IsPrime(int candidate)
    {
        if ((candidate & 1) == 0)
        {
            return candidate == 2;
        }

        for (int i = 3; (i * i) <= candidate; i += 2)
        {
            if ((candidate % i) == 0) return false;
        }

        return candidate != 1;
    }
}

class Program
{
    static void PrintPrime(object sender, EventArgs arg)
    {
        Console.Write((arg as PrimeCallbackArg).Prime + ", ");
    }

    static int Sum;

    static void SumPrime(object sender, EventArgs arg)
    {
        Sum += (arg as PrimeCallbackArg).Prime;
    }

    static void Main(string[] args)
    {
        PrimeGenerator gen = new PrimeGenerator();

        gen.PrimeGenerated += PrintPrime;
        gen.PrimeGenerated += SumPrime;

        gen.Run(10);
        Console.WriteLine();
        Console.WriteLine(Sum);

        gen.PrimeGenerated -= SumPrime;
        gen.Run(15);
    }
}

// 출력:
// 2, 3, 5, 7,
// 17
// 2, 3, 5, 7, 11, 13,

델리게이트 사용법과 상당히 유사하다.

실제로 이벤트는 델리게이트와 결이 같기 때문에 이벤트로 구현된 코드는 델리게이트로도 구현할 수 있다.


event 키워드

만약 위의 Main 메서드에서 아래의 코드를 실행하면 컴파일 에러가 발생할 것이다.

gen.PrimeGenerated(gen, new PrimeCallbackArg(i)); // 컴파일 에러 발생

event 키워드로 정의할 경우 반드시 선언한 클래스에서만 콜백을 발생시킬 수 있다.

반대로 event 키워드가 없다면 위의 코드가 실행될 수 있다. 이는 의도하지 않은 결과를 불러올 수 있기 때문에 필드를 외부로부터 은닉해야 된다.

이 경우 구독/해지를 위한 추가적인 메서드를 제공해야 되기 때문에 코드가 복잡해진다.


EventHandler/EventArgs

위 코드는 콜백을 발생시킬 때, 콜백을 발생시킨 인스턴스와 콜백 값을 담은 인스턴스를 인자로 넘긴다.

이는 이미 EventHandler가 두 개의 인자를 받도록 약속된 델리게이트 타입이기 때문이다.

namespace System
{
    public delegate void EventHandler(object? sender, EventArgs e);
}

즉 EventHandler를 사용한다는 것은 이미 정의된 델리게이트 타입을 사용하는 것이다.

EventArgs를 상속받은 클래스를 정의하여 데이터를 담기만 하면 되기 때문에 유연한 코드를 작성할 수 있다.


내용이 어려워 간단히 정리하면 다음과 같다.

  1. 이벤트는 델리게이트를 좀 더 사용하기 편하게 만든 개념이다. (언제든 델리게이트로 대체가 가능하다.)
  2. 이벤트는 외부에서 구독/해지가 가능하고 내부에서 이벤트를 발생시키는 패턴에 적합하다.
  3. event 키워드는 반드시 선언한 클래스에서만 콜백을 발생시킬 수 있게 해준다.
  4. EventHandler는 System에 정의된 델리게이트 타입이다.
  5. EventHandler를 사용할 경우 EventArgs를 상속받은 클래스를 추가로 정의하여 콜백시 해당 클래스의 인스턴스를 인자로 넘겨줘야 한다.

참고 자료
시작하세요! C# 10 프로그래밍 - 정성태

profile
Runner's high를 목표로

0개의 댓글