메서드를 값으로서 가리킬 수 있는 타입을 뜻하며 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이 될 수 있다. 이 경우 오작동을 일으킬 수 있으므로 유효성 검사를 해줘야 한다.
델리게이트의 사용 패턴을 일반화하여 더 사용하기 편하게 만든 것으로, 주로 콜백 메서드 구현에 사용된다.
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,
델리게이트 사용법과 상당히 유사하다.
실제로 이벤트는 델리게이트와 결이 같기 때문에 이벤트로 구현된 코드는 델리게이트로도 구현할 수 있다.
만약 위의 Main 메서드에서 아래의 코드를 실행하면 컴파일 에러가 발생할 것이다.
gen.PrimeGenerated(gen, new PrimeCallbackArg(i)); // 컴파일 에러 발생
event 키워드로 정의할 경우 반드시 선언한 클래스에서만 콜백을 발생시킬 수 있다.
반대로 event 키워드가 없다면 위의 코드가 실행될 수 있다. 이는 의도하지 않은 결과를 불러올 수 있기 때문에 필드를 외부로부터 은닉해야 된다.
이 경우 구독/해지를 위한 추가적인 메서드를 제공해야 되기 때문에 코드가 복잡해진다.
위 코드는 콜백을 발생시킬 때, 콜백을 발생시킨 인스턴스와 콜백 값을 담은 인스턴스를 인자로 넘긴다.
이는 이미 EventHandler가 두 개의 인자를 받도록 약속된 델리게이트 타입이기 때문이다.
namespace System
{
public delegate void EventHandler(object? sender, EventArgs e);
}
즉 EventHandler를 사용한다는 것은 이미 정의된 델리게이트 타입을 사용하는 것이다.
EventArgs를 상속받은 클래스를 정의하여 데이터를 담기만 하면 되기 때문에 유연한 코드를 작성할 수 있다.
내용이 어려워 간단히 정리하면 다음과 같다.
- 이벤트는 델리게이트를 좀 더 사용하기 편하게 만든 개념이다. (언제든 델리게이트로 대체가 가능하다.)
- 이벤트는 외부에서 구독/해지가 가능하고 내부에서 이벤트를 발생시키는 패턴에 적합하다.
- event 키워드는 반드시 선언한 클래스에서만 콜백을 발생시킬 수 있게 해준다.
- EventHandler는 System에 정의된 델리게이트 타입이다.
- EventHandler를 사용할 경우 EventArgs를 상속받은 클래스를 추가로 정의하여 콜백시 해당 클래스의 인스턴스를 인자로 넘겨줘야 한다.
참고 자료
시작하세요! C# 10 프로그래밍 - 정성태