[Unity/C#] 대리자 (delegate, Func, Action)와 event

Donghee·2024년 3월 18일
0

대리자란?

Microsoft의 C# 가이드에 따르면, 대리자는 '특정 매개 변수 목록 및 반환 형식이 있는 메서드에 대한 참조를 나타내는 형식'이다. 말을 좀 풀어서 설명하자면, '매개 변수와 반환 형식이 정해져 있으면, 그 메서드들을 참조할 수 있게 해주는 형식'인 것이다. 이를 통해 메서드를 매개변수 형태로 전달할 수도 있고, 특정 조건이 되면 대리자에 참조되어있는 함수들을 한번에 실행할 수 있는 이벤트로 활용할 수도 있다. 다른 말로는 콜백(callback)이라고도 한다.
C와 C++에 있었던 함수 포인터의 위험성과 불편함을 C#에서 발전시킨 것이 대리자 개념이다. 정리해보면 다음과 같은 특징을 가진다.

  • 함수의 포인터가 아닌 대리자 개념을 통해 메서드를 호출한다.
  • 동일한 형(매개변수/리턴 타입)을 가진 메서드들을 대리자가 묶어서 관리하고, 한번에 호출한다.
  • 대리자 타입을 통해 함수의 매개 변수로도 전달이 가능하다.

C# 대리자의 종류

delegate

delegate는 메서드의 매개변수와 반환 타입을 지닌 대리자 형식이다. 이는 명시적으로 정의를 해줘야한다. 사용할 때는 먼저 델리게이트 형식을 만들고, 그 형식을 객체로 선언한 다음, 메서드와 그 객체를 연결시키는 식으로 해야 한다.

public class DelegateTest : MonoBehaviour
{
   private delegate void MyDelegate(int a, int b);
   private MyDelegate newDelegate;

   private void PlusNumber(int a , int b)
   {
       print(a + b);
   }

   private void MinusNumber(int a , int b)
   {
       print(a - b);
   }

   private void Start()
   {
       newDelegate += new MyDelegate(PlusNumber);
       newDelegate += MinusNumber;
       newDelegate += (int a, int b) =>
       {
           print(a * b);
       };
   }

   private void Update()
   {
       if (Input.GetMouseButtonDown(0))
       {
           newDelegate.Invoke(Random.Range(1, 10), Random.Range(1, 10));
       }
   }
}

위의 코드에서는 리턴타입이 void이고, int타입의 매개변수를 두개 받는 MyDelegate 대리자를 선언해주었다. 그것의 객체로 myDelegate라고 선언해주었고, 그에 맞는 메서드들을 += 연산자를 통해 이 객체에 추가해주었다. 이후 마우스를 누를 때마다 Invoke를 통해 호출한다.
델리게이트 객체에 메서드를 추가하는 방법은
1. new delegate 타입(메서드이름)으로 추가
2. 메서드 이름으로 추가
3. 람다식으로 대리자를 선언 후 바로 메서드 객체로 만들어 추가
와 같은 방식이 존재한다.

위 스크립트를 오브젝트에 추가해준 후 마우스를 한번 클릭한 모습이다. 랜덤으로 (4, 2) 매개변수가 결정되어 PlusNumber, MinusNumber, 곱하는 메서드가 차례대로 실행된 모습이다.

Func

Func는 반환 타입이 void가 아닌 메서드를 담는 델리게이트 형식이다. 즉, 반환 타입이 void가 아닌 델리게이트를 칭하는 타입이라고 생각하면 된다. 이미 타입이므로 delegate와 다르게 따로 형식을 지정할 필요가 없고, 제네릭으로 반환 타입과 매개 변수들을 입력해주면 된다.

Func<매개변수1 타입, 매개변수2 타입, ... , 리턴 타입> MyFunc;

만약 매개변수가 없다면 매개변수 타입을 제외하고 리턴 타입만 하나 지정해주면 된다.

Func<리턴 타입> MyFunc;

다음의 예제를 보자.

public class FuncTest : MonoBehaviour
{
    private Func<int> MyFunc;
    private Func<int, int, int, string> MyFunc2;

    private int RandomNumber()
    {
        return UnityEngine.Random.Range(1, 10);
    }

    private string NumberToString(int a, int b, int c)
    {
        return "A: " +  a + " B: "  + b + " C: " + c;
    }

    private void Start()
    {
        MyFunc += RandomNumber;
        MyFunc2 += NumberToString;
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            print(MyFunc.Invoke());
            print(MyFunc2.Invoke(1, 2, 3));
        }
    }
}

매개변수가 없고 리턴 타입이 int형인 메서드를 담을 수 있는 MyFunc, 매개변수가 int형 세개 있고 리턴 타입이 string인 메서드를 담을 수 있는 MyFunc2 델리게이트 객체를 선언해준다.

Invoke로 실행시키면 위와 같은 결과가 나온다.

Action

Action은 반환 타입이 void인, 즉 반환 타입이 없는 메서드를 담는 델리게이트 형식이다. Func와 마찬가지로 따로 형식을 지정해줄 필요 없이, 제네릭으로 매개 변수를 지정해주며 된다.

Action<매개 변수1, 매개 변수2 ...> myAction;

만약 반환 타입가 void면서 매개 변수도 없다면 제네릭으로 지정하지 않고 Action 키워드로만 델리게이트 객체를 선언해주면 된다.

Action myAction;

다음 예제를 보며 알아보자.

public class ActionTest : MonoBehaviour
{
    private Action<int> MyAction;
    private Action MyAction2;

    private void PowerNum(int num)
    {
        print(num * num);
    }

    private void PrintRandom()
    {
        print(UnityEngine.Random.Range(1, 10));
    }

    private void Start()
    {
        MyAction += PowerNum;
        MyAction2 += PrintRandom;
        MyAction2 += () => { print("Action!"); };
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            MyAction.Invoke(2);
            MyAction2.Invoke();
        }
    }
}

매개 변수가 int형 하나 존재하는 메서드를 담는 델리게이트 MyAction과, 매개 변수가 존재하지 않는 메서드를 담는 델리게이트 MyAction2를 선언했다. Action 또한 람다식을 활용해 바로 델리게이트 인스턴스를 만들어 추가하는 것이 가능하다. 실행 결과는 다음과 같다.

Func와 Action은 사용자가 정의하는 델리게이트 형식이 아니고 미리 정해져있는 형식이기 때문에, 가독성이나 다른 프로그래머가 코드를 볼 때 한눈에 이해하기 편하다는 장점이 있다.

이벤트(event)

C# 델리게이트 객체는 접근 한정자를 public으로 지정할 시, 델리게이트 객체를 대입 연산자(=)를 써서 멋대로 바꾸거나, Invoke 멤버 함수를 통해 멋대로 호출이 가능하다는 단점이 있다.
이를 보완하기 위해 C#에서는 이벤트라는 델리게이트 한정 키워드를 제공한다.

public delegate void MyDelegate(int a, int b);
public event MyDelegate EventDelegate;

다음과 같이 선언하며, EventDelegate는 이벤트 델리게이트 객체로서 특수한 성질을 지니게 된다.
1. 클래스 외부에서 이벤트 델리게이트에게 대입 연산자(=)는 사용 불가하며, 오직 추가하거나 기존 메서드 인스턴스를 제거하는 복합 연산자(+=, -=)만이 사용 가능하다.
2. 클래스 외부에서 Invoke로 델리게이트 객체의 메서드들을 호출하는 것은 불가능하다.
아래 예제를 보자.

public class EventTest : MonoBehaviour
{
    public delegate void MyDelegate(int a, int b);
    public event MyDelegate EventDelegate;

    private void PlusNumber(int a, int b)
    {
        print(a + b);
    }

    private void MinusNumber(int a, int b)
    {
        print(a - b);
    }

    private void Start()
    {
        EventDelegate = PlusNumber;
        EventDelegate += MinusNumber;
        EventDelegate.Invoke(2, 3);
    }
}

다른 델리게이트들과는 델리게이트 객체를 선언할 때 event 키워드가 붙는다는 차이점밖에 없다. 속해있는 클래스에서는 대입 연산자(=), Invoke 호출 모두 문제가 없다.
외부의 클래스에서 이 델리게이트 객체를 조작해보자.

public class EventTest2 : MonoBehaviour
{
    private EventTest publisher;

    private void MultiplyNumber(int a, int b)
    {
        print(a * b);
    }

    private void Start()
    {
        publisher = FindObjectOfType<EventTest>();
        publisher.EventDelegate = MultiplyNumber;
        publisher.EventDelegate.Invoke(2,3);
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            publisher.EventDelegate.Invoke(2, 3);
        }
    }
}

평범한 델리게이트 객체를 대하듯이 대입 연산자(=)를 사용하고, Invoke를 통해 호출을 시켜보았다. 이벤트 델리게이트이므로 다음과 같은 오류가 발생한다.

오류 메세지를 대강 해석해보면, 'EventTest에서 쓰는 것을 제외하면 EventDelegate에는 +=나 -= 연산자만 올 수 있다.' 라는 말이 된다.
즉, event 키워드는 클래스 외부에서는 메서드 구독(+=)이나 해제(-=)만 가능하게 하고, 실행은 내부에서만 하기를 원할 때 사용하면 좋다. 물론 Func과 Action도 델리게이트 형식이므로, 이들 앞에 한정 키워드로 지정해줄 수 있다.


레퍼런스

profile
마포고개발짱

0개의 댓글