Delegate
델리게이트는 특정 매개변수와 반환 타입을 가진 메서드에 대한 "참조"를 나타내는 타입이다. 델리게이트를 인스턴스화하면, 호완가능한 시그니처와 반환 타입을 가진 모든 메소드와 연결할 수 있다. 또한 델리게이트의 인스턴스를 통해 메소드를 호출할 수 있다.
개발을 할 때 "메서드 자체를 파라미터로 전달할 수 있을까?" 라는 의문을 해결하는데는 델리게이트가 적격이다. 메서드에 대한 참조를 델리게이트로 래핑한 뒤, 메서드를 다른 메서드의 파라미터로 전달하는데 사용할 수 있다.
델리게이트 타입의 선언
그럼 델리게이트 타입은 어떻게 선언할 수 있을까?
public delegate void CustomCallback(string s);
`접근한정자` `delegate` `반환 형식` `delegate타입 이름` `(delegate에 등록할 매개 변수)` 형식을 맞춰 작성하면 선언 가능하다. 또한 델리게이트에 등록할 메서드가 델리게이트의 원형 타입과 다르다면 등록할 수 없으니 조심해야 한다.
델리게이트 인스턴스 생성
public class MyClass
{
public delegate void MyDelegate(string s);
public void PrintMessage1(string msg)
{
Console.WriteLine($"msg: {msg}");
}
public void PrintMessage2(string msg)
{
Console.WriteLine($"msg: {msg}");
}
public void Perform()
{
MyDelegate m1 = new MyDelegate(PrintMessage1); // m3선언과 동일한 방식
MyDelegate m2 = new MyDelegate(PrintMessage2); // m4선언과 동일한 방식
MyDelegate m3 = PrintMessage1;
MyDelegate m4 = PrintMessage2;
}
}
델리게이트 호출 및 사용 예시
델리게이트는 함수에 대한 참조를 가지고 있다가, 호출 할 때 사용할 파라미터를 입력하면 등록되어있는 함수에 파라미터가 전달되어 사용된다.
public class MyClass
{
public delegate void MyDelegate(string s);
public void PrintMessage1(string msg)
{
Console.WriteLine($"msg: {msg}");
}
public void PrintMessage2(string msg)
{
Console.WriteLine($"msg: {msg}");
}
public void Perform()
{
MyDelegate m1 = new MyDelegate(PrintMessage1);
MyDelegate m2 = new MyDelegate(PrintMessage2);
MyDelegate m3 = PrintMessage1;
MyDelegate m4 = PrintMessage2;
m1("Test1"); // m1.Invoke("Test1");과 동일한 방식
m2("Test2");
m3("Test3");
m4("Test4");
}
}
internal class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
mc.Perform();
}
}
델리게이트 체인
그러면 하나의 델리게이트에서는 단 하나의 함수의 참조만 들고있을 수 있을까? 그렇지는 않다. `+` 연산자를 사용하여 하나의 델리게이트 인스턴스에 여러 개체를 할당할 수 있다.
public class MyClass
{
public delegate void MyDelegate(string s);
void Hello(string s)
{
Console.WriteLine($" Hello, {s}!");
}
void Goodbye(string s)
{
Console.WriteLine($" Goodbye, {s}!");
}
public void Perform()
{
MyDelegate hiDel, byeDel, multiDel, multiMinusHiDel;
hiDel = Hello;
byeDel = Goodbye;
multiDel = hiDel + byeDel;
multiMinusHiDel = multiDel - hiDel;
Console.WriteLine("hiDel Delegate 실행: ");
hiDel("A");
Console.WriteLine("byeDel Delegate 실행: ");
byeDel("B");
Console.WriteLine("multiDel Delegate 실행: ");
multiDel("C");
Console.WriteLine("multiMinusDel Delegate 실행: ");
multiMinusHiDel("D");
}
}
internal class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
mc.Perform();
}
}
Multicast Delegate
위의 예시는 델리게이트 인스턴스 + 델리게이트 인스턴스를 통해 multiDel을 만들었지만, 하나의 델리게이트 인스턴스에 여러 개의 메서드를 등록할 수 있다. 단순히 `+=` 연산자를 사용해 함수 추가 연산을 수행하면 된다.
public class MyClass
{
public delegate void MyDelegate(string s);
void Hello(string s)
{
Console.WriteLine($" Hello, {s}!");
}
void Goodbye(string s)
{
Console.WriteLine($" Goodbye, {s}!");
}
public void Perform()
{
MyDelegate hiByeDel;
hiByeDel = Hello;
hiByeDel += Goodbye;
hiByeDel("Combined");
}
}
// 출력 결과
Hello, Combined!
Goodbye, Combined!
이러한 구조는 동일한 파라미터를 가진 여러 메서드를 호출하는 구조이다. 이럴 때는 일괄 처리나 브로드 캐스팅의 목적으로 사용되는데, 여러 리스너(델리게이트 구독자)가 동일한 정보를 처리해야 할 때 사용된다. 이런 하나의 델리게이트 인스턴스에 여러 함수가 연결되어 있는 경우의 예시는 다음 처럼 생각할 수 있다.
1. 플레이어가 몬스터를 처치했을 때 이벤트 처리
몬스터를 처치했을 때 경험치가 올라가고, 처치 사운드가 들리는 등의 함수을 한 번에 실행시킬 수 있을 것이다.
2. 특정 컨텐스트를 포함하여 객체 전달 후 if 분기로 작업 분리
이 구조는 아마 간단하게 사용할거면 상관은 없겠지만, 조건의 개수가 많아질수록 하드 코딩 비율이 늘어날 것이므로 아마 이런 식으로는 사용하지 않는 것이 구조에 더 좋을 것이라 생각한다.
public class MyClass
{
public class MessageContext
{
public string Message { get; set; }
public bool IsFarewell { get; set; }
}
public delegate void MyDelegate(MessageContext context);
void Hello(MessageContext context)
{
if (!context.IsFarewell)
{
Console.WriteLine($"Hello, {context.Message}");
}
}
void Goodbye(MessageContext context)
{
if (context.IsFarewell)
{
Console.WriteLine($"Goodbye, {context.Message}");
}
}
public void Perform()
{
MyDelegate hiByeDel = Hello;
hiByeDel += Goodbye;
MessageContext context = new MessageContext { Message = "Here!", IsFarewell = true };
hiByeDel(context);
}
}
3. 하나의 이벤트가 발생했을 때 이를 여러 로그 시스템에 기록
public class Logger
{
public delegate void LogHandler(string message);
private LogHandler _logHandlers;
public void RegisterLogHandler(LogHandler logHandler)
{
_logHandlers += logHandler;
}
public void Log(string message)
{
_logHandlers?.Invoke(message);
}
}
// Example handlers
void LogToFile(string message) => Console.WriteLine($"File Log: {message}");
void LogToConsole(string message) => Console.WriteLine($"Console Log: {message}");
void LogToDatabase(string message) => Console.WriteLine($"Database Log: {message}");
// Usage
var logger = new Logger();
logger.RegisterLogHandler(LogToFile);
logger.RegisterLogHandler(LogToConsole);
logger.RegisterLogHandler(LogToDatabase);
logger.Log("플레이어가 적을 처치!");
EventHandler
C#을 다루다 보면 EventHandler를 많이 접하게 되는데, 이는 단순히 델리게이트로 선언된 타입일 뿐이다. 그 타입의 이름이 EventHandler일 뿐이며, 해당 EventHandler 델리게이트에 등록될 함수는 Object와 EventArgs를 받을 수 있도록 형식 매개변수가 정의가 되어있으면 된다.
public delegate void EventHandler(object? sender, EventArgs e);
델리게이트의 내부 구조
델리게이트의 내부 구조에 대한 설명은 다음 링크에서 정말 잘해놓았다.
인상 깊었던 점은, 델리게이트가 싱글 캐스트(_methodPtr)와 멀티 캐스트 상황에 따라 내부적으로 다른 필드를 사용한다는 것. 또한 특정 인스턴스의 메서드의 참조를 가지고 있는 상황이라면 내부적으론 해당 메서드의 인스턴스도 같이 갖게 됨으로 델리게이트 밖에서 별도의 인스턴스 객체를 파라미터로 전달할 필요가 없는 점.
그리고 static 메서드에 대한 참조를 가지게 되는 경우, _mtehodPtr이 아닌 _methodPtrAux에 레퍼런스 값이 저장된다는 점.
또한 멀티 캐스트 부분에서는, _invocationList가 2이상인 경우 'delegate' 요소를 하나씩 가져와 차례로 모두 호출한다. 그래서 _invocationList의 각 요소에 접근하면 또 다른 delegate 객체를 볼수 있다.
그리고 이 글에서는 delegate에 대해 좀 더 새로운 시각을 제공해준다.
C# 컴파일러는 선언된 delegate의 정의를 읽어 System.MulticastDelegate 클래스로부터 파생된 Delegate 클래스를 생성한다고 한다. 그래서 해당 글에서는 delegate는 메서드 메타 정보를 내부에 가지고 있는 특별한 종류의 Wrapper 클래스라고 설명한다. 이렇게 정의됨으로 인해 클래스 객체를 생성하는 것과 비슷한 방식으로 new를 써서 델리게이트의 인스턴스를 생성할 수 있게 된다고 한다. (또한 직접 System.MulticastDelegate를 파생하여 새로운 클래스를 만들 순 없다고 한다.)
그래서 사실 델리게이트를 다른 메서드에 전달하는 것은 메서드 정보를 갖는 Wrapper 클래스의 객체를 파라미터로 전달하는 것이라 볼 수 있다,
Event
기존의 delegate는 잘못 사용될 소지가 있다. 외부에서 delegate 필드를 실행시킬 수도 있으며 `=`할당 연산자를 사용해 기존 delegate를 덮어씌워 버리는 문제가 생길 수도 있다.
이벤트는 특별한 종류의 multicast delegate인데, 이벤트가 선언된 클래스(publisher class 또는 파생 클래스)와 구조체 안에서만 실행될 수 있다. 만약 다른 클래스나 구조체에서 이벤트를 구독한다면 publisher 클래스가 이벤트를 발생시킬 때 해당 이벤트 핸들러 메서드가 호출된다.
이벤트 선언
이벤트를 선언할 때는 `event` 키워드를 사용해서 이벤트를 선언한다. 그리고 선언할 때 접근제한자는 무엇이든 사용가능하다.
public delegate void MyDelegate(MessageContext context);
public event MyDelegate MyDelEvt;
이벤트 사용자는 이벤트에 이벤트 핸들러 추가하거나 삭제할 수 있다.
객체가 이벤트를 트리거하는 동안 이벤트는 제공된 모든 이벤르를 호출한다. 이벤트 핸들러들은 delegate 인스턴스이며 이는 이벤트에 추가되고 이벤트가 발생할 때 실행된다.
이벤트의 사용
우선 아래와 방식으로 문법적 문제없이 사용은 가능하지만, event키워드 자체가 외부에서 호출을 할 수 없도록 만들었기에 InvokeEvt를 통해 억지로 호출을 가능하게 만드는 것은 좋지 않은 구조라고 생각한다. 이벤트의 트리거가 외부에 의해 제어되는 구조적 문제를 가지고 있다고 보이는 구조이다.
public class MyEvnet
{
public class MessageContext
{
public string Message { get; set; }
public bool IsFarewell { get; set; }
}
public delegate void MyDelegate(MessageContext context);
public event MyDelegate MyDelEvt;
public void InvokeEvt(MessageContext context)
{
MyDelEvt.Invoke(context);
}
}
public class MyFunctions
{
public void Hello(MessageContext context)
{
if (!context.IsFarewell)
{
Console.WriteLine($"Hello, {context.Message}");
}
}
public void Goodbye(MessageContext context)
{
if (context.IsFarewell)
{
Console.WriteLine($"Goodbye, {context.Message}");
}
}
}
internal class Program
{
static void Main(string[] args)
{
MyEvnet evt = new MyEvnet();
MyFunctions myFunctions = new MyFunctions();
evt.MyDelEvt += myFunctions.Hello;
evt.MyDelEvt += myFunctions.Goodbye;
MessageContext context = new MessageContext
{
Message = "Here!",
IsFarewell = true
};
evt.InvokeEvt(context);
}
}
이벤트 트리거 분리
그러므로 외부에서는 데이터만 전달하는 방식을 사용하면서. 이벤트 트리거가 발생할 조건과 이벤트를 실행하는 주체는 이벤트를 소유한 클래스 내부에서 처리하는 것이 좋을 것이다.
public class MyEvnet
{
public class MessageContext
{
public string Message { get; set; }
public bool IsFarewell { get; set; }
}
public delegate void MyDelegate(MessageContext context);
public event MyDelegate MyDelEvt;
// 이벤트를 외부에서 호출할 수 없도록 이벤트 발생을 제어
protected void OnMyDelEvt(MessageContext context)
{
MyDelEvt?.Invoke(context);
}
public void ProcessMessage(string message, bool isFareWell)
{
var context = new MessageContext
{
Message = message,
IsFarewell = isFareWell
};
// 필요 시 여기에서 추가 로직 (e.g., 검증, 상태 변경 등)을 수행
if (!string.IsNullOrEmpty(context.Message))
{
OnMyDelEvt(context);
}
}
}
public class MyFunctions
{
public void Hello(MessageContext context)
{
if (!context.IsFarewell)
{
Console.WriteLine($"Hello, {context.Message}");
}
}
public void Goodbye(MessageContext context)
{
if (context.IsFarewell)
{
Console.WriteLine($"Goodbye, {context.Message}");
}
}
}
internal class Program
{
static void Main(string[] args)
{
MyEvnet evt = new MyEvnet();
MyFunctions myFunctions = new MyFunctions();
evt.MyDelEvt += myFunctions.Hello;
evt.MyDelEvt += myFunctions.Goodbye;
// 외부에서는 단순히 메시지와 상태를 전달
evt.ProcessMessage("Here!", true);
evt.ProcessMessage("Everyone", false);
}
}
물론 이 구조도 ProcessMessage가 MyEvent에 있는 것은 과할 수 있다. 그래서 별도의 클래스를 만들어 MyEvent의 객체의 역할과 분리한 뒤, 둘을 중간에서 관리해줄 클래스 같은 것을 만드는 것도 방법이다.
Action, Func
Action과 Func는 동일하게 delegate 타입으로 선언되어있고, delegate의 타입 이름이 Action 또는 Func일 뿐이다. 그리고 Func는 추가적으로 반환값을 내보낼 수 있게 설계되어 있다.
public delegate void Action(); // Action 선언체
public delegate void Action<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);
Action은 최대 타입을 16개까지 받을 수 있다.
사용자가 사용할 때는 그냥 다음과 같이 선언하면 바로 사용할 수 있다.
Action action; // 기본 Action 변수 선언
Action<string> actionHasString; // String을 형식 매개변수로 가지는 함수를 actionHasString에 담을 수 있다.
참고 자료
- https://www.csharpstudy.com/csharp/CSharp-delegate2.aspx
- https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/delegates/how-to-combine-delegates-multicast-delegates
- https://www.csharpstudy.com/CSharp/CSharp-delegate.aspx
- https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/delegates/
- https://www.csharpstudy.com/CSharp/CSharp-delegate-concept.aspx
- https://www.csharpstudy.com/DevNote/Article/14
- https://www.csharpstudy.com/CSharp/CSharp-delegate3.aspx
'C#' 카테고리의 다른 글
[C#] Public 필드와 프로퍼티 (0) | 2024.12.20 |
---|---|
[C#] 제너릭 메소드 (0) | 2024.10.06 |
[C#] CS0273에러 accessor must be more restrictive than the property or indexer (1) | 2024.09.25 |
[C#] 데이터 타입 (0) | 2023.01.05 |
[C#] 변수 (0) | 2023.01.04 |