C#/이것이 C#이다
13장. 대리자와 이벤트
개발자엄지희
2023. 5. 18. 15:51
반응형
(12장, 예외 처리하기 파트는 건너뛰었습니다)
1. 대리자란?
출장을 나온 상현이는 사장님께 급히 보고 드릴 일이 있어 회사에 전화를 걸었습니다. 그런데 전화를 받은 사람은 사장님이 아닌 사장님의 비서였습니다. 상현이는 비서에게 "사장님이 돌아오시면 제게 전화 부탁드린다고 전해주세요."라는 메모를 남기고 전화를 끊었습니다. 잠시 후, 돌아온 사장님은 비서의 메모를 받고 상현이에게 전화를 걸어 통화를 했습니다.

이 이야기에서 상현이가 비서에게 했던 부탁이 콜백(Callback)입니다.
콜백: 대신 어떤 일을 해줄 코드(비서)를 두고, 이 코드가 실행할 세부 코드는 컴파일 시점이 아닌 실행 시점에 부여하는 것
대리자: 메소드에 대한 참조, 대리자가 후에 메소드를 호출해줌
대리자 선언
한정자 delegate 반환형식 대리자이름 ( 매개변수목록 );
deletate int MyDelegate ( int a, int b );
대리자는 인스턴스가 아닌, 형식(Type)
int Plus ( int a, int b ) { return a + b; }
int Minus ( int a, int b ) { return a - b; }
MyDelegate Callback;
Callback = new MyDelegate( Plus );
Console.WriteLine ( Callback ( 3, 4 ) ); // 7 출력
Callback = new MyDelegate( Minus );
Console.WriteLine ( Callback ( 7, 5 ) ); // 2 출력
대리자를 이용하여 콜백 구현

1) 대리자를 선언한다.
2) 대리자의 인스턴스를 생성한다. 인스턴스를 생성할 때는 대리자가 참조할 메소드를 매개 변수로 넘긴다.
3) 대리자를 호출한다.
2. 대리자는 왜, 그리고 언제 사용하나요?
"값" 이 아닌 "코드" 자체를 매개변수로 넘기고 싶어서
배열을 정렬하는 메소드를 만든다고 가정했을 때, 이 메소드가 오름차순으로 정렬되게 할까요, 아니면 내림차순? 아니면 다른 결과로 정렬할까요? 이런 고민 없이 메소드가 정렬을 수행할 때 사용하는 비교 루틴을 매개변수로 넘길 수 있습니다.
1) 대리자 선언
delegate int Compare ( int a, int b );
2) Compare 대리자가 참조할 비교 메소드 작성
static int AscendCompare(int a, int b)
{
if (a > b) return 1;
else if (a == b) return 0;
else return -1;
}
3) 정렬할 배열과 대리자를 매개 변수로 받는 정렬 메소드 작성
static void BubbleSort(int[] DataSet, Compare Comparer)
{
int i = 0;
int j = 0;
int temp = 0;
for ( i=0; i < DataSet.Length - (i + 1); j++)
{
if ( Comparer( DataSet[j], DataSet[j+1] ) > 0 )
{
temp = DataSet[j+1];
DataSet[j+1] = DataSet[j];
DataSet[j] = temp;
}
}
}
4) 정렬 방식이 분리된 정렬 코드 생성
int[] array = { 3, 7, 4, 2, 10 };
BubbleSort(array, new Compare(AscendComparer)); // array는 { 2, 3, 4, 7, 10 }
3. 일반화 대리자
delegate int Compare<T>(T a, T b);
static int AscendCompare<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b);
}
Int32, Double 등 모든 수치 형식과 string은 모두 IComparable을 상속해서 CompareTo() 메소드를 구현하고 있기 때문에, 이를 구현하여 호출하면 오름차순 정렬에 필요한 비교 결과를 얻을 수 있음
static void BubbleSort<T>(T[] DataSet, Compare<T> Comparer)
{
int i = 0;
int j = 0;
int temp = 0;
// ...
}
4. 대리자 체인
대리자 하나는 여러 개의 메소드를 동시에 참조할 수 있다.
+= 를 이용하여 대리자 체인 연결
delegate void ThereIsAFire( string location );
void Call119( string location ) {
Console.WirteLine($"소방서죠? 불났어요! 여기는 {location}");
}
void ShotOut( string location ) {
Console.WirteLine($"피하세요! {location}에 불이 났어요!");
}
void Escape( string location ) {
Console.WirteLine($"{location}에서 나갑시다!");
}
ThereIsAFire Fire = new ThereIsAFire( Call119 );
Fire += new ThereIsAFire( ShotOut );
Fire += new ThereIsAFire( Escape );
다음과 같이 한 번만 호출하면 자신이 참조하고 있는 메소드를 모두 호출함
Fire("우리집");
-=, Delegate, Remove() 등을 이용하여 대리자 체인 끊어내기
ThereIsAFire listener1 = new ThereIsAFire( Call119 );
ThereIsAFire listener2 = new ThereIsAFire( ShotOut );
ThereIsAFire listener3 = new ThereIsAFire( Escape );
ThereIsAFire Fire = listener1;
Fire += listener2;
Fire += listener3;
Fire -= listener3;
// 작동안되는 코드일 수 있음 주의
Fire = (ThereIsAFire)Delegate.Combine( listener1, listener2 );
Fire = (ThereIsAFire)Delegate.Remove( listener2 );
5. 익명 메소드
void DoSomething() // 어느 메소드나 이름만은 갖고 있음
{
}
이름이 없는 메소드, 익명 메소드
delegate ( int a, int b )
{
return a + b;
}
대리자 인스턴스 = delegate ( 매개변수_목록 )
{
// 실행하고자 하는 코드 ...
};
대리자가 참조할 메소드를 넘겨야 할 일이 생겼는데,
그 메소드가 두 번 다시 사용할 일이 없다고 판단되면 그 때가 익명 메소드를 사용할 타이밍
6. 이벤트: 객체에 일어난 사건 알리기
1) 대리자를 선언합니다. (클래스 안, 밖 상관없이)
2) 클래스 내에 1)에서 선언한 대리자의 인스턴스를 event 한정자로 수식해서 선언합니다.
3) 이벤트 핸들러를 작성합니다. 이벤트 핸들러는 1)에서 선언한 대리자와 일치하는 메소드면 됩니다.
4) 클래스의 인스턴스를 생성하고 이 객체의 이벤트에 3)에서 작성한 이벤트 핸들러를 등록합니다.
5) 이벤트가 발생하면 이벤트 핸들러가 호출됩니다.
// 1)
delegate void EventHandler(string message);
// 2)
class MyNotifier
{
public event EventHandler SomethingHappend; // EventHandler는 대리자
public void DoSomething(int number)
{
int temp = number % 10;
if ( temp != 0 && temp % 3 == 0 ) {
SomethingHappend($"{number} : 짝");
}
}
}
// 3)
class MainApp
{
static public void MyHandler(stringmessage)
{
Console.WriteLine(message);
}
// 4)
static void Main(string[] args)
{
MyNotifier notifier = new MyNotifier();
notifier.SomethingHappend += new EventHandler( MyHandler );
for (int i=1; i < 30; i++) {
notifier.DoSomething(i);
}
}
}
이벤트는 이벤트 처리기를 등록하지 않아도 컴파일 에러가 발생하지 않습니다. 덕분에 초기화하지 않은 이벤트를 쉽게 놓치곤 합니다. 저자의 친구 하나는 이런 문제 때문에 이벤트 선언 시 항상 비어 있는 메소드로 미리 초기화를 해둔다고 합니다. 최악의 경우에도 프로그램이 다운되는 것은 막을 수 있으니까요.
7. 대리자와 이벤트
이벤트가 대리자와 가장 크게 다른 점은 바로
이벤트는 외부에서 직접 사용할 수 없다는 데 있습니다.
delegate void EventHandler(string messgae);
class MyNotifier
{
public event EventHandler SomethingHappend;
// ...
}
class MainApp
{
static void Main(string[] args)
{
MyNotifier notifier = new Mynotifier();
notifier.SomethingHappened("테스트"); // 에러! 이벤트는 객체 외부에서 직접 호출할 수 없습니다
}
}
대리자와는 달리 이벤트가 호출될 수 없다는 사실은 견고한 이벤트 기반 프로그래밍에 대한 기대를 가능하게 합니다. 만약 여러분이 네트워크 상태 변화에 대한 사건을 알리는 클래스를 작성해서 동료에게 줬다고 해봅시다. 이벤트를 객체 외부에서 임의로 호출할 수 있게 된다면 동료 프로그래머가 실제 네트워크 상태와는 상관없이 객체의 외부에서 허위로 네트워크 상태 변화 이벤트를 일으킬 수 있게 됩니다. 이것은 객체의 상태를 밖는 것보다 더 나쁩니다. 객체의 상태를 허위로 나타낼 수 있으니 말입니다.
이런 위협은 대리자로는 막을 수 없으니, 대리자는 콜백 용도로, 이벤트는이벤트대로 객체의 상태 변화나 사건의 발생을 알리는 용도로 써야 합니다.
반응형