본문 바로가기

.NET/C#

[C#] delegate와 delegate 체인

delegate(델리게이트)는 C++의 함수 포인터와 비슷한 기능을 하는 C#의 기능으로, '대리자'라는 의미처럼 메서드를 대신 호출해주는 기능을 한다.

 

메서드를 대신 호출해주는 기능이 왜 필요할까?

 

사실 알게 모르게 여기저기에서 우리는 delegate를 사용하고 있다. 가장 대표적인 곳이 WinForm에서 이벤트 처리기이다. 이는 WinForm 애플리케이션이 동작하는 중에 이벤트가 발생하는 각 포인트에서 적절한 대리자를 사용하여 대리자에 등록된 메서드를 호출해주는 것을 의미하며, 각 이벤트에 대한 세부적인 처리를 정의하는 것은 공백으로 남겨두어 WinForm 개발자들이 자유롭게 사용할 수 있도록 한다.

 

가령 TextBox와 같은 컨트롤들은 키가 입력됨을 판단하여 적절한 위치에서 KeyDown, KeyPress, KeyUp, TextChanged와 같은 이벤트 처리기를 이용해 등록된 메서드들을 호출한다. 만약 사용자가 위와 같은 이벤트 처리기들에 메서드를 등록해놓았다면, 등록된 메서드가 호출될 것이다. 아무런 메서드가 등록되지 않았다면 당연히 아무 메서드도 호출되지 않는다.

실제로 컨트롤의 이벤트를 생성한 다음 *.Designer.cs 파일을 열어보게 되면 아래와 같이 버튼의 생성 코드 부분에서 각 이벤트 처리기에 생성된 메서드가 등록되는 것을 확인할 수 있다.

맨 아랫줄에서 button1의 Click 이벤트 핸들러에 사용자가 생성한(혹은 디자이너가 자동으로 생성해주는) this.Button1_Click 메서드를 등록하고 있다.

가끔 이벤트를 등록했다가 메서드를 지워버리면 디자이너가 깨지는 오류가 발생하는 것을 경험할 수 있는데, Designer.cs 파일에서 이벤트 처리기에 등록된 메서드를 찾을 수 없어서 발생하는 오류이다. 이 경우 디자이너 파일을 열어 삭제한 메서드를 등록하는 문장을 지워줘야 한다.

 


위와 같은 코드에서 Click을 마우스 우클릭 후 [정의로 이동]을 선택해보자.

이렇게 Control 클래스에 public event EventHandler Click이라고 정의되어 있는 것을 확인할 수 있다. (event 키워드는 신경 쓰지 말자. 지금은 delegate에 대해서만 설명한다.)

여기서 Click은 EventHandler라는 대리자의 이름이다.

 

다시 EventHandler의 정의로 이동해보면,

이렇게 object 타입, EventArgs 타입을 매개변수로 가지고 반환값은 void인 delegate로 정의가 되어있다.

 


정리하자면 델리게이트는 일반적으로 아래와 같이 '정의', '생성', '등록', '호출'의 네 단계를 통해 사용할 수 있다.

 

1. 정의

새로운 델리게이트를 정의하는 단계이다. 델리게이트의 이름, 반환형, 매개변수 목록 등을 정의한다.

delegate void MyDelegate(object obj);

 

2. 생성

정의한 델리게이트를 사용하는 호출자를 생성하는 단계이다. (여기서 호출자는 필자가 이해를 돕기 위해 임의로 부여한 명칭이다.)

delegate void MyDelegate(object obj);

MyDelegate Caller;

 

3. 등록

호출자에 메서드를 등록하는 단계이다.

delegate void MyDelegate(object obj);

MyDelegate Caller;

void Initialize()
{
	// Caller 호출자에 Func 메서드를 등록한다.
	Caller += Func;
}

// 아래는 등록할 메서드
void Func(object obj)
{
	// Do something
}

 

4. 호출

호출자를 통해 등록한 메서드를 호출하는 단계이다. (실사용 단계)

delegate void MyDelegate(object obj);

MyDelegate Caller;

void Initialize()
{
	// Caller 호출자에 Func 메서드를 등록한다.
	Caller += Func;
}

// 아래는 등록할 메서드
void Func(object obj)
{
	// Do something
}

public static void Main(string[] args)
{
	Initialize();
    
    // Caller를 사용해 Caller에 등록된 Func()를 호출한다.
    Caller(null);
}

 

 


 

델리게이트 체인이란, 하나의 호출자에 여러개의 메서드를 등록하는 것을 말한다.

하나의 호출자에 여러개의 메서드를 등록할 수 있는데, 이러한 경우 등록된 모든 메서드가 동기적, 순차적으로 호출된다. (동기적이라 함은 동시에 실행되지 않는다는 의미로, 먼저 호출된 메서드가 종료된 후 다음 메서드가 호출된다.)

 

예를 들어 어떤 이벤트 처리기에 모듈화 된 여러 액션이 순서대로 실행되야할 경우 하나의 호출자에 메서드를 여러 개 등록하여 처리할 수 있다.

 

아래 코드는 이해를 돕기 위해 '어떤 적 객체를 죽인 플레이어에게 보상을 주는 시스템'을 설계한 것으로, 실제로 아래 코드와 비슷한 형식으로 게임 개발이 진행되는지의 여부는 보장할 수 없으니 오직 참고용으로만 확인할 것.

public abstract class Enemy
{
	public delegate void EnemyEventHandler(Enemy enemy, Player player);
	public EnemyEventHandler EnemyDead;

	Player lastHitPlayer;

	// 계속 호출되는 메서드
	void Update()
	{
		// 현재 객체의 HP가 0 이하로 내려간 경우
		if(this.HP <= 0)
    	{
    		if(EnemyDead != null) 
       		{
        		// 막타를 친 플레이어에 대해 EnemyDead 이벤트 호출
        		EnemyDead(this, lastHitPlayer);
            	// 현재 객체 삭제
            	Remove(this);
        	}
    	}
	}

	// 피격시 호출되는 메서드
	void Hit(Player player)
	{
		// 현재 객체의 HP를 감소시키고, 마지막으로 타격한 플레이어를 lastHitPlayer에 등록
		this.HP -= (lastHitPlayer = player).Attack;
	}
}
public static class System
{
	public static void GiveExp(Enemy enemy, Player player)
	{
		player.AddExp(enemy.RewardExp);
	}

	public static void GiveGold(Enemy enemy, Player player)
	{
		player.AddGold(enemy.RewardGold);
	}

	public static void GiveItems(Enemy enemy, Player player)
	{
		player.AddItems(enemy.GetDropItems());
	}
}
public clsss Forest_1_1 : Field
{
	public int MaxMobs = 10;
	List<Enemy> fieldMobs = new List<Enemy>();

	void Update()
	{
		if(fieldMobs.Count < MaxMobs)
    	{
			Enemy enemy = RadomMobMaker.Make(FieldType.Forest);
    
    		// Enemy의 EnemyDead 이벤트 처리기에 메서드 등록
    		enemy.EnemyDead += System.GiveExp;
    		enemy.EnemyDead += System.GiveGold;
    		enemy.EnemyDead += System.GiveItems;
    
			fieldMobs.Add(enemy);
    	}
	}
}