[delegate] 01.델리게이트의 정의와 정체, 체이닝, 사용 목적
델리게이트(Delegate)는 C#에서 중요한 개념 중 하나로, 메서드 참조를 저장하고 호출할 수 있는 형식을 제공하는 기능입니다.
C++의 함수 포인터와 비슷한 개념인데, 좀 더 많은 기능을 제공합니다.
델리게이트를 사용하면 메서드를 매개변수로 전달하거나, 런타임에 호출할 메서드를 동적으로 결정할 수 있습니다.
델리게이트 사용
델리게이트는 delegate
예약어를 사용하여 메서드의 시그니처를 선언하듯 정의합니다.
1
접근제한자 delegate 반환타입 델리게이트_이름([매개변수 목록...])
델리게이트 인스턴스 생성 및 사용
1
public delegate bool ActionBtnDelegate();
만약, 위와 같이 델리게이트를 선언했다면, 클래스 인스턴스 생성하듯 해당 델리게이트 이름으로 인스턴스를 생성할 수 있습니다.
또한, C# 2.0부터는 new
없이도 인스턴스를 쉽게 생성할 수 있는 기능을 제공합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 일반 메서드 정의
private static bool ActionBtn1()
{
Console.WriteLine("ActionBtn1");
return true;
}
private static bool ActionBtn2() {
Console.WriteLine("ActionBtn2");
return true;
}
static void Main(string[] args)
{
// 델리게이트 인스턴스 생성
ActionBtnDelegate actionBtnDelegate1 = new ActionBtnDelegate(ActionBtn1);
// C# 2.0부터는 new 없이도 생성 가능
ActionBtnDelegate actionBtnDelegate2 = ActionBtn2;
}
이제 이 델리게이트를 다양하게 사용 할 수 있는데, 이 델리게이트를 테스트 할 수 있는 테스트 클래스를 만들어 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DelegateTest
{
private ActionBtnDelegate actionBtnDelegates;
public ActionBtnDelegate GetNowOperableDelegate()
{
return actionBtnDelegates;
}
public void SetActionDelegate(ActionBtnDelegate actionBtnDelegate)
{
actionBtnDelegates = actionBtnDelegate;
}
public void Action()
{
if (actionBtnDelegates != null)
{
actionBtnDelegates();
}
else
{
Console.WriteLine("등록된 델리게이트가 없습니다.");
}
}
}
일단, ActionBtnDelegate
가 델리게이트 객체 혹은 타입을 가리키는 것이라고 생각하고 어디어디에 어떤 의도로 메서드들을 만들었는지 보겠습니다.
actionBtnDelegates
필드는 어디서든 저장된 메서드를 호출할 수 있도록 메서드 참조를 저장하는 변수입니다.
GetNowOperableDelegate()
는 현재 등록된 델리게이트를 반환하는 메서드, SetActionDelegate()
는 새로운 델리게이트를 등록하는 메서드입니다.
그리고 Action()
메서드에서 현재 actionBtnDelegates
필드에 저장된 메서드를 호출하는 메서드입니다.
즉, 특정한 메서드를 변수에 저장해서 언제 어디서든 사용할 수 있게 만들었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
internal class Program
{
private static bool ActionBtn1()
{
Console.WriteLine("ActionBtn1");
return true;
}
private static bool ActionBtn2() {
Console.WriteLine("ActionBtn2");
return true;
}
static void Main(string[] args)
{
ActionBtnDelegate actionBtnDelegate1 = new ActionBtnDelegate(ActionBtn1);
ActionBtnDelegate actionBtnDelegate2 = ActionBtn2;
// 테스트 클래스 인스턴스 생성
DelegateTest delegateTest = new DelegateTest();
// 현재 범위 내의 메서드를 델리게이트에 등록
delegateTest.SetActionDelegate(actionBtnDelegate1);
// 등록된 델리게이트를 확인
ActionBtnDelegate actionBtnDelegate = delegateTest.GetNowOperableDelegate();
if (actionBtnDelegate != null)
{
Console.WriteLine($"Now Action : {actionBtnDelegate.ToString()}");
// Now Action : _12_Delegate.ActionBtnDelegate
}
// 테스트 클래스 인스턴스 내에서 액션도 호출해 봄.
delegateTest.Action(); // ActionBtn1
}
}
위 예제와 같이 Main
을 구성하고 테스트를 해보니 Program
클래스 내부의 메서드도 잘 호출하고 있는 모습을 확인할 수 있었습니다.
일급 객체
어떻게 위와 같은 형태의 메서드 저장 및 호출이 가능한 것일까?
그건 델리게이트가 일급 객체이기 때문입니다.
일급 객체란, 간단히 말하면 다음과 같은 속성을 가진 객체를 말합니다.
- 변수에 할당될 수 있습니다.
- 함수의 인자로 전달될 수 있습니다.
- 함수의 반환값으로 사용될 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DelegateTest
{
// 변수(필드)에 할당
public ActionBtnDelegate actionBtnDelegates;
// 반환 값으로 사용
public ActionBtnDelegate GetNowOperableDelegate()
{
return actionBtnDelegates;
}
// 인자로 전달 가능
public void SetActionDelegate(ActionBtnDelegate actionBtnDelegate)
{
actionBtnDelegates = actionBtnDelegate;
}
}
따라서, 위 테스트 클래스를 보면, 변수에 할당하기도 하고, 인자에 사용되기도 하고, 반환 값으로 사용하기도 하는 모습을 볼 수 있습니다.
델리게이트 체이닝
사실, delegate
는 MulticastDelegate
타입의 간편 표기법입니다.
System.MulticastDelegate
타입으로 올라가 보면, 델리게이트와 관련된 기능을 제공하는 추상 클래스라는 것을 알 수 있습니다.
따라서, delegate
는 다양한 기능들을 제공하는데, 그 중 하나가 델리게이트 체이닝입니다.
델리게이트는 여러 메서드를 호출할 수 있는 체인으로 결합될 수 있습니다.
하나의 델리게이트 인스턴스에 여러 메서드를 순차적으로 연결하는 기술을 델리게이트 체이닝이라고 하고, 이렇게 한 번의 호출로 여러 메서드를 동시에 실행할 수 있는 델리게이트를 멀티캐스트 델리게이트라고 합니다.
델리게이트는 연산자 =
로 호출할 델리게이트를 설정하고, +
로 메서드를 추가하거나 -
로 메서드를 제거할 수 있습니다.
1
2
3
4
5
ActionBtnDelegate test = ActionBtn1;
test = test + ActionBtn2; // test += ActionBtn2;
test();
// ActionBtn1
// ActionBtn2
그 외 델리게이트가 지원하는 멤버
그 외에도 BeginInvoke()
나 EndInvoke()
같은 비동기 호출을 지원하는 메서드라던가, 델리게이트의 메서드 메타데이터를 참조하는 Method
필드, 델리게이트가 참조하는 메서드의 대상 객체를 반환하는 Target
필드 등을 지원합니다.
멤버 | 설명 |
---|---|
Invoke() | 델리게이트가 참조하는 메서드를 동기적으로 호출합니다. |
BeginInvoke() | 델리게이트가 참조하는 메서드를 비동기적으로 호출합니다. 호출이 즉시 반환됩니다. |
EndInvoke() | BeginInvoke 로 시작한 비동기 호출의 결과를 수집합니다. |
GetInvocationList() | 멀티캐스트 델리게이트에 연결된 개별 델리게이트들의 배열을 반환합니다. |
Method | 델리게이트가 참조하는 메서드의 메타데이터를 제공합니다. |
Target | 델리게이트가 참조하는 메서드가 호출될 대상 객체를 반환합니다. |
델리게이트를 어디에 쓰는가?
만약, 게임을 만들 때, 어떤 동작(Action)의 버튼을 누르면 다양한 동작이 가능하도록 만든다고 생각해 봅시다.
게임의 Action
버튼을 눌렀을 때, 적의 등 뒤에서는 암살을 수행하고, 문 앞에서는 문을 여는 행동을 하고, 땅에 떨어진 아이템 주변에서는 아이템을 줍는 등의 동작을 할 수 있습니다.
즉, Action
버튼을 눌렀을 때 어떤 동작을 할지 미리 정할 수 없기 때문에 런타임에 실행할 메서드를 동적으로 결정할 수 있어야 합니다.
그럴 때 델리게이트를 쓰면 이벤트를 편리하게 관리 할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public delegate bool ActionBtnDelegate();
internal class Player
{
public ActionBtnDelegate actionBtnDelegates;
public void OnAction()
{
actionBtnDelegates?.Invoke();
}
}
internal class Item
{
public bool PickUp()
{
Console.WriteLine("아이템을 줍습니다.");
return true;
}
}
internal class Door
{
public bool Open()
{
Console.WriteLine("문을 엽니다.");
return true;
}
}
internal class Enemy
{
public bool StealthKill()
{
Console.WriteLine("암살을 합니다.");
return true;
}
}
static void Main(string[] args)
{
Player player = new Player();
Item item = new Item();
player.actionBtnDelegates = item.PickUp;
player.OnAction(); // 아이템을 줍습니다.
Door door = new Door();
player.actionBtnDelegates = door.Open;
player.OnAction(); // 문을 엽니다.
Enemy enemy = new Enemy();
player.actionBtnDelegates = enemy.StealthKill;
player.OnAction(); // 암살을 합니다.
}
물론, 런타임에 실행할 메서드를 동적으로 결정한다는 말을 들었을 때, 인터페이스와 전략패턴 등… 다양한 방법이 생각 나셨을 수 있습니다.
물론, 프로그래밍에는 정답이 없기 때문에 다양한 방법으로 기능을 구현할 수 있습니다.
그러나 델리게이트는 특히 콜백 메서드를 구현하기 매우 좋으며, 이벤트 핸들링, 비동기 처리 등의 기능 등에 사용할 수 있습니다.
이를 통해 코드의 재사용성을 높이고 유지 보수를 용이하게 할 수 있습니다.
참고
[delegate] 02. event와 EventArgs (EventHandler)
[delegate] 03. 익명 델리게이트와 Func, Action