[C#] 대리자 (Delegate, Event)
1. Delegate
Delegate는 C++의 함수 포인터와 비슷한 개념으로 함수의 레퍼런스를 리스트에 저장하여 사용한다.
(정확히는 객체의 인스턴스 참조와 메서드의 참조를 동시에 갖고 있다.)
함수 포인터는 CallBack (함수가 함수를 부름)을 하기 위해 주로 사용되었는데,
Delegate도 메서드를 다른 메서드로 전달할 수 있도록 하기 위해 만들어졌다.
Delegate는 메서드의 파라미터로 호출되어 다른 메서드에 접근이 가능하고, 그냥 호출되어 모든 함수를 부를 수도 있다.
또 자료형, 매개변수가 같은 비슷한 역할의 메서드를 서로 연결해서 한 번에 활용할 수도 있다.
Delegate Chain이라고 부르고 연결된 메서드 리스트는 .Net MultiCastDelegate 클래스에서 관리한다.
// 델리게이트 선언. 파라미터로 사용하기 위해서 public 처리해 주었다.
public delegate int DelEx(params int[] a);
// 델리게이트 예시 클래스. 싱글톤을 이용해 만들어보았다.
public class DelegateEx
{
private static DelegateEx _instance;
public static DelegateEx instance
{
get {
if (_instance == null) _instance = new DelegateEx();
return _instance;
}
}
// 델리게이트에 넣어줄 함수 1.
public int Plus(params int[] a) {
int sum = 0;
foreach (var i in a) { sum += i; };
Console.WriteLine("Plus에 들렀습니다 : {0}", sum);
return sum;
}
// 델리게이트에 넣어줄 함수 2.
public int Minus(params int[] a) {
int sum = 0;
foreach (var i in a) { sum -= i; };
Console.WriteLine("Minus에 들렀습니다 : {0}", sum);
return sum;
}
// 델리게이트 사용 함수.
public void DelChain()
{
// 최초 인스턴스 생성 시 반드시 할당 해주어야 한다.
DelEx del = new DelEx(Plus);
// new를 생략하여 사용도 가능.
del = Minus;
// 연산자를 이용하여 델리게이트 내부에 더하거나 빼줄수 있음.
del += Plus;
// 무명 메서드를 활용.
del += delegate { Console.WriteLine("무명 메서드를 활용 : {0}", 777); return 15; };
Console.WriteLine("가장 마지막 함수의 반환값입니다 : {0}", del(5, 5, 5));
DelExample(del);
}
// 메서드의 파라미터로도 사용할 수 있다.
public void DelExample(DelEx ex)
{
// WriteLine은 가장 끝의 인덱스에 있는 리턴 정보를 가져온다. 즉, Minus의 반환값이 아닌 무명 메서드의 반환값만 가져온다.
Console.WriteLine("가장 마지막 함수의 반환값입니다 : {0}", ex(5, 5, 5));
}
}
class Program
{
static void Main(string[] args)
{
DelegateEx.instance.DelChain();
}
}
Minus에 들렀습니다 : -15
Plus에 들렀습니다 : 15
무명 메서드를 활용 : 777
가장 마지막 인덱스의 반환값입니다 : 15
Minus에 들렀습니다 : -15
Plus에 들렀습니다 : 15
무명 메서드를 활용 : 777
가장 마지막 메서드의 반환값입니다 : 15
이 코드를 작성하면서 알게 된 점인데, 디버깅 시 자주 쓰는 Console.WriteLine 함수에
복수의 레퍼런스를 가진 델리게이트가 들어가면 가장 마지막에 들어간 레퍼런스의 메서드 리턴값만 호출된다.
2. Event
델리게이트는 보안 수준이 낮으면 클래스 내부에 있어도 다른 멤버처럼 외부에서 접근할 수 있다.
또 할당을(=) 통해 리스트의 멤버를 초기화 할 수도 있는데,
이러한 특징에서 나오는 단점을 보완하기 위하여 사용되는 것이 Event이다.
Event는 클래스 내부에서는 델리게이트와 비슷하게 동작하나
클래스 외부(정확히는 다른필드)에서는 호출과 할당을 할 수 없으며, 오직 연산자를 이용한 멤버 증감만 가능하다.
Event는 이름 그대로 어떤 메서드의 이벤트가 된다.
Event에 참여한 메서드는 Event가 발생 시(호출) 그 쪽을 주목(Notify)하고 행동한다.
디자인 패턴 중 옵저버 패턴이 이와 같은 특징을 가지고 Event를 사용하여 자주 구현된다.
public class EventEx
{
public delegate int ExDel(params int[] a);
// event는 delegate와 보안 수준을 맞춰주어야 한다.
public event ExDel exEvent;
// 이벤트에 넣어줄 함수 1.
public int Plus(params int[] a)
{
int sum = 0;
foreach (var i in a) { sum += i; };
Console.WriteLine("Plus에 들렀습니다 : {0}", sum);
return sum;
}
// 이벤트에 넣어줄 함수 2.
public int Minus(params int[] a)
{
int sum = 0;
foreach (var i in a) { sum -= i; };
Console.WriteLine("Minus에 들렀습니다 : {0}", sum);
return sum;
}
public void UseEvent() {
// 클래스 내부에서는 호출 및 할당 둘 다 가능.
ExCallBack(exEvent);
exEvent = Plus;
}
void ExCallBack(ExDel exDel) {
exDel(5, 5, 5, 5, 5);
}
}
class Program
{
static void Main(string[] args)
{
EventEx eventEx = new EventEx();
// 증감 연산자를 이용한 접근만 가능.
eventEx.exEvent += eventEx.Plus;
eventEx.exEvent += eventEx.Minus;
// 클래스 외부에서 호출 및 할당 불가.
// eventEx.exEvent = eventEx.Plus;
// eventEx.exEvent();
eventEx.UseEvent();
}
}
Plus에 들렀습니다 : 25
Minus에 들렀습니다 : -25
델리게이트는 메서드 간의 연결을 담당할 수 있다는 점에서 굉장히 매력적이지만 메서드와 객체의 레퍼런스를 가지고 있다는 점에서 다루기 예민한 부분도 있다. (물론, 레퍼런스가 아닌 주소를 직접 가진 함수 포인터보다는 안전하다.)
예민한 부분을 다루는 델리게이트를 개발 중 실수를 방지하는 것이 Event의 역할이라고 볼 수 있다.