2025. 2. 18. 23:37ㆍ개발/C#
이 글은 박상현님의 '이것이 C#이다 개정판'을 참고하여 공부한 내용입니다.
delegate
C#의 대리자는 C의 함수 포인터와 비슷한 기능을 하는 타입입니다. 대리자는 호출될 메서드와 호출할 개체를 지정하여 안정적인 메서드의 참조를 지원합니다. 즉 메서드를 변수로 바꿔 다른 메서드에 전달하여 실행할 수 있다는 뜻입니다.
delegate void MyDelegate(string message);
void PrintMessage(string msg)
{
Console.WriteLine(msg);
}
MyDelegate del = PrintMessage;
del("Hello, World!"); // print: Hello, World!
우선 대리자에 할당할 메서드와 대리자(delegate)를 선언합니다. 이때 대리자의반환타입과 매개변수는 호출할 메서드와 동일해야 합니다. 그리고 대리자에 대입연산자를 통해 메서드를 할당합니다. 이후 대리자의 매개변수를 전달하여 메서드를 호출합니다.
대리자는 익명 메서드와 람다식을 사용하여 더욱 간편하게 사용할 수 있습니다.
delegate void MyDelegate(string message);
MyDelegate del = delegate (string msg)
{
Console.WriteLine("anonymous method: " + msg);
};
del("Hello"); // print: anonymous method: Hello
대리자에 익명 메서드를 할당하여 실행한 예시입니다. 익명 메서드란 말 그대로 이름이 없는 메서드로, 일회성으로 특정 메서드 내에서만 호출하거나, 선언문 생략하여 코드를 간결하게 만들때 사용합니다. 익명 메서드는 delegate 키워드를 사용하여 할당합니다.
MyDelegate del = msg => Console.WriteLine("lambda: " + msg);
del("Hello!"); // print: lambda: Hello!
람다식은 익명 메서드보다 더 간결한 코드를 표현할 수 있는 표현식으로, 미국 수학자 알로조 처리의 람다식에서 영향을 받았습니다. 메서드의 이름이 아닌 코드 본문을 노출시켜 이해를 돕고, 코드를 더욱 간결히 작성할 수 있게 해줍니다. 위 예시에서 msg는 람다식에 전달될 매개변수이며, =>는 람다 연산자, Console.WriteLine("lambda: " + msg)은 람다식 본문 입니다. 익명 메서드와 다르게 delegate 키워드를 사용하지 않습니다.
또한 대리자는 복합 대입 연산자를 통해 여러개의 메서드를 할당받아 호출할 수 있습니다.
delegate void MyDelegate(string message);
void Method1(string msg) => Console.WriteLine("Method1: " + msg);
void Method2(string msg) => Console.WriteLine("Method2: " + msg);
MyDelegate del = Method1;
del += Method2;
del("Hello!");
del -= Method1;
del("World!");
Method1: Hello!
Method2: Hello!
Method2: World!
람다식을 사용하여 간단히 2개의 메서드를 선언하고, 하나의 대리자를 통해 호출한 예시입니다. 처음 대리자를 선언과 동시에 Method1을 할당합니다. 그리고 다음 += 연산자를 통해 Method2를 추가로 할당합니다. 그후 대리자를 통해 두개의 메서드 모두 호출하여 결과를 출력합니다. 마지막으로 -= 연산자를 통해 대리자에서 Method1을 해재한 후 실행하여 Method2만 호출되는 것을 확인합니다.
Func, Action, Predicate
대리자를 사용하다보면 불편한 점이 하나 눈에 뜁니다. 바로 대리자를 사용할 메서드에 따라 계속 선언해줘야 한다는 점입니다. .NET에서는 이런 점을 개선하기 위해 많은 일반화 형식 매개변수를 사용한 대리자를 미리 선언해뒀습니다. 이런 대리자들을 빌트인 대리자라 하며 Func, Action, Predicate가 있습니다.
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
...
public delegate TResult Func<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, out TResult>(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);
Func 대리자는 반환형이 있는 대리자로 마지막 형식 매개변수가 반환타입입니다. 0 ~ 16개의 매개변수를 받을 수 있게 선언되어 있습니다.
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 5)); // print: 8
대리자를 선언하지 않고 위 예시와 같이 사용할 수 있으며, 위 예시에서는 a, b 2개의 int 타입을 매개변수로 받고 마지막 int 타입이 반환형입니다.
public delegate void Action<>();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
...
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 대리자는 반환형이 없는 대리자입니다. 동일하게 0 ~ 16개의 매개변수를 받을 수 있게 선언되어 있습니다.
Action<string> print = msg => Console.WriteLine(msg);
print("Hello Action!"); // print: Hello Action!
매개변수 한개를 사용하는 Action 대리자를 사용하여 출력을 하는 예시입니다.
public delegate bool Predicate<in T>(T obj);
Predicate는 매개변수로 들어을 객체 검사하여 bool 값을 반환하는 대리자입니다. Predicate는 매개변수 한개만을 받을 수 있게 선언되어 있습니다.
Predicate<int> isEven = num => num % 2 == 0;
Console.WriteLine(isEven(4)); // print: True
정수가 짝수인지 홀수인지 검사하는 람다식을 Predicate 대리자를 사용하여 확인하는 예시입니다.