C# 문법 | 콜백을 위한 델리게이트, 델리게이트 체인, 델리게이트 이벤트 | 차이점, 예시 코드

seunghyun·2024년 9월 16일
0

🔗 출처 : https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/

델리게이트

정의와 사용법

델리게이트는 함수에 대한 참조라고 볼 수 있다. 함수의 주소값을 가지고 해당 함수를 대신 호출하는 역할을 한다. 의미를 보면 이름처럼 한글로 델리게이트를 직역한 '대리자'라는 이름과 어울린다.

정의 방법

  • 접근제한자 delegate 반환타입 식별자 (매개변수);
  • 대신 호출할 함수의 시그니처 (반환타입과 매개변수 목록)를 확인하는 것이다.
  • 그래서 델리게이트는 int, float 타입처럼 타입의 정의 라고도 할 수 있다.
  • 그리고 델리게이트는 참조형 타입이다.
public class DelegateTest : MonoBehaviour
{
	// 델리게이트 타입의 정의
	public delegate int MyDelegate(int num);
    
    // 선언한 델리게이트 타입으로 변수(객체)를 선언
    public MyDelegate _myDelegate;
    
    public void TestFunction()
    {
    	Debug.Log("Test");
    }
    
    public void Start()
    {
    	// 방법 1
    	// C# 1.0 버전 (초창기) 방법이라 요즘엔 이렇게 쓰지 않는다. 근데 원리는 컴파일러에서 이런 방식으로 변환되어 코드가 작성될 것이다.
    	// 생성자로 인스턴스 생성 후 주소값을 받아옴
        // _myDelegate가 TestFunction()을 참조하게 된다.
    	_myDelegate = new MyDelegate(TestFuction);
        
        // 방법 2
        // C# 2.0 버전부터는 이렇게 사용한다. 함수의 주소값을 바로 참조하는 것처럼 축약해 사용한다.
        _myDelegate = TestFunction();
        
        // TestFuction가 호출된다.
        _myDelegate();
    }
}

위 샘플 코드를 설명해보자면,
_myDelegate 는 MyDelegate 타입의 인스턴스를 참조하고 있다 (=주소값을 가지고 있다)
근데 MyDelegate 인스턴스는 TestFuction를 참조하고 있다

필요성

굳이??

왜 델리게이트를 사용할까?

델리게이트는 타입이므로

  • 변수를 선언하는 것처럼 사용할 수 있다.
  • 타입이므로 함수의 매개변수나 반환 형식으로 사용할 수 있다.

그리고 델리게이트는 그 자체로 콜백을 해준다.

콜백

  • 함수를 먼저 참조하고 나중에 호출한다.

언제 콜백(callback) 을 사용하는가?

콜백에 관해서는 정의를 내리는 것보다 언제 왜 사용하는지를 이해하는 것이 더 효과적이다. (정의는 사람마다 달라서 오히려 이해하기 어려움)

함수는 주소이고, 콜백을 사용하든 일반 함수를 사용하든 사실 결과는 같다.

그래서 콜백을 언제 사용하는지가 중요하다.

마치 OOP의 다형성처럼, 하나의 버전만으로도 여러 버전을 사용할 수 있는 그 느낌을 구현할 때 사용된다.

OOP가 아닌 C에서 이걸 구현할 수 있는 방법 중 하나가 바로 콜백이라고 할 수 있다.

그래서 게임 개발에서 예를 들자면,
갖고있는 아이템이 뭐든간에 아이템의 종류에 따라 각각의 기능1번과 기능2번을 사용하도록 할 수 있다.

예시 코드

class Player
{
	private delegate void BuffDelegate(); // 타입 정의
    private BuffDelegate _buffDelegate; // 객체(변수) 선언
    
    public enum Buff
    {
    	None,
        Buff1,
        Buff2
    }
    
    private Buff _buff;
    public Buff _Buff
    {
    	get { return _buff; }
        set
        {
        	if (_buff == value) return;
            
            _buff = value;
            
            if(_buff == Buff.Buff1)
            	_buffDelegate = Buff1;
            else if(_buff == Buff.Buff2)
            	_buffDelegate = Buff2;
            else if(_buff == Buff.None)
            	_buffDelegate = NoneBuff;
        }
    }
    
    public void Attack()
    {
    	_buffDelegate.Invoke();
    }
    
    void Buff1() { Debug.Log("Buff1"); }
    void Buff2() { Debug.Log("Buff2"); }
    void NoneBuff() {  }

}

델리게이트 체인

사용법

델리게이트 체인을 사용하여 하나의 델리게이트가 여러 함수를 참조할 수 있다.

public class DelegateTest : MonoBehaviour
{
	private delegate void TestDelegate(); // 타입 정의
    
    private TestDelegate testDelegate;
    
    void Chain1() { Debug.Log("Chain1"); }
    void Chain2() { Debug.Log("Chain2"); }
    void Chain3() { Debug.Log("Chain3"); }

	void Start()
    {
    	// C# 2.0 버전 
        // + 연산자 뿐만 아니라, - 연산자로 체인에서 제거하는 것도 가능하다.
        // += 연산자도 사용 가능하다.
    	testDelegate = new TestDelegate(Chain1);
    		+ new TestDelegate(Chain2);
    		+ new TestDelegate(Chain3);
	
    	testDelegate.Invoke();
    }
}

언제 사용할까?

하나의 사건이 트리거될 때 일어나야 할 일이 꼭 하나라는 법이 없다.

게임 개발을 예시로 들면, 몹을 죽였을 때에도 여러 일이 일어난다.

1. 파티클 재생
2. 아이템 드랍
3. 스코어 증가 (UI)

예시를 위해 먼저, 델리게이트 체인을 사용하지 않았을 때의 코드를 보자!

IEnumerator CoDie()
{
	isDead = true;
    anim.SetInteger("Stat", 2); // 사망
    
    DelegateUIManager.instance.AddScore(transform.position);
    DelegateParticleManager.instance.PlayDieParticle(transform.position);
    DelegateItemManager.instance.DropItem(transform.position);
    
    yield return new WaitForSeconds(0.5f);
    Destroy(gameObject);
}

여기서 갑자기 몹을 죽였을 때 처리해야 하는 일이 늘어난다면?

1. 파티클 재생
2. 아이템 드랍
3. 스코어 증가 (UI)
+ 4. 죽는 소리 추가
+ 5. 적 동료 생성

그럼 계속..계속.. 함수가 추가되고 의존성 또한 커진다는 단점이 있다. 하나가 바뀌면 다 바뀌어야 한다는 점... OTL

그래서 이번에는 델리게이트 체인을 사용할 때를 보자!

// DelegateEnemy.cs
IEnumerator CoDie()
{
	isDead = true;
    anim.SetInteger("Stat",2);
    dieDelegate.Invoke(transform.position);
    yield return new WaitForSecons(0.5f);
    Destroy(gameObject);
}

// DelegateManager.cs
public class DelegateManager : MonoBehaviour
{
	...
    
    private void Start()
    {
    	...
        AddDelegate();
    }
    
    public void AddDelegate()
    {
    	enemy.dieDelegate += DropItem;
    }
    
    public void DropItem(Vector3 pos)
    {
    	GameObject go = Instantiate(item);
        go.transform.position = enemy.transform.position;
    }
}

그래서 델리게이트 체인을 사용하면 싱글톤을 마구 사용했을 때 의존성이 생기고 유지보수가 힘들어지는 단점과 달리, 델리게이트와 연결된 코드를 자동으로 호출해주므로 유지보수에 상당히 좋다.

그렇다면 델리게이트가 만능인가?

꼭 그런 것만은 아니다.

Invoke()를 할 때, 실행만 한다. 이 안에 어떤 함수들을 참조하고 있는지까지는 알 수 없다. 라는 단점이 있다! 후후

그렇기 때문에 델리게이트는 코드 분석은 어려우니 콜백 기능이 필요할 때만 사용하는 것이 추천된다!


델리게이트 이벤트

이벤트란

객체의 상태 변화나 사건의 발생을 알리는 용도이다.

델리게이트에도 이벤트라는 키워드가 있다.

이 이벤트의 특징으로는

  1. 일반적인 델리게이트는 외부에서도 호출이 가능한 반면, 이벤트는 외부에서 호출할 수 없다.

아래 사진과 같은 오류가 발생한다.

그래서 호출은 아래 코드처럼 내부에서만 할 수 있다.
이 차이로 인해서 더 안정적인 이벤트 기반 프로그래밍이 가능해진다.

public class TestDele
{
	public delegate void TestEvent();
    public event TestEvent testEvent;
    public void StartEvent()
    {
    	testEvent.Invoke();
    }
}

public class TestDelegate : MonoBehaviour
{
	private void Start()
    {
    	TestDele testDele = new TestDele();
        testDele.testEvent += Test1;
        testDele.testEvent += Test2;
        testDele.testEvent += Test3;
        
        // 이렇게 !
        testDele.StartEvent();
    }
    
    public void Test1() { ... }
    ...
}
  1. 상태 변화에 대한 동작 정의, 이벤트 기반 프로그래밍이 가능해진다.
  1. 외부에서 대입 연산자도 사용할 수 없다. (+,- 은 가능) 이 또한 안정성을 위함이다.
profile
game client programmer

0개의 댓글