
우리가 고용한 심부름꾼에게는 한 가지 아쉬운 점이 있었습니다.
"정수(
int)를 계산하는Calculate대리자를 만들었는데,
이번엔 실수(double)를 계산해야 하네? 하는 일은 똑같은데..."
우리의 심부름꾼이 오직 '정수 심부름'만 할 수 있는 전문가였던 탓입니다.
만약 어떤 종류의 심부름이든 척척 해내는 '만능 심부름꾼'이 있다면 얼마나 좋을까요?
먼저, 제네릭이 없을 때의 불편함을 다시 한번 코드로 확인해 봅시다.
[타입별로 대리자를 선언해야 하는 번거로움]
// 정수 계산용 대리자
public delegate int CalculateInt(int a, int b);
// 실수 계산용 대리자
public delegate double CalculateDouble(double a, double b);
// ... string, float 등 타입마다 계속 만들어야 함 ...
똑같은 '두 개의 입력을 받아 하나를 반환하는' 구조임에도 불구하고,
타입이 다르다는 이유로 코드가 중복되고 있습니다.
이 문제를 해결하기 위해, 제네릭 메서드나 제네릭 클래스처럼
대리자에도 타입 매개변수(<T>)를 사용할 수 있습니다.
[제네릭 대리자 사용]
// "T라는 타입 두 개를 받아, T 타입 하나를 반환한다"는 만능 명세서!
public delegate T Calculate<T>(T a, T b);
Calculate<T>선언으로 모든 숫자 타입의 계산을 처리할 수 있습니다!
코드 중복이 사라지고 훨씬 깔끔해졌습니다!
사실, 위와 같은 제네릭 대리자를 직접 선언할 일은 거의 없습니다. 왜냐하면
.NET 프레임워크가 가장 흔하게 사용되는 형태의 제네릭 대리자들을 미리 만들어서
기본으로 제공하기 때문입니다. 우리는 이 '기성품'들을 가져다 쓰기만 하면 됩니다!
<T, TResult>Func대리자는 반환 값이 있는 모든 종류의 메서드를 참조할 수 있습니다.
심부름을 시키면 반드시 결과물(Return Value)을 들고 돌아옵니다.
[특징]
Func<T, TResult>: T타입의 입력 1개를 받아 TResult타입의 결과를 반환.
Func<T1, T2, TResult>: T1, T2타입의 입력 2개를 받아 TResult타입의 결과를 반환.
Func<T1, T2, ... TResult>: 최대 16개의 입력 매개변수까지 지원합니다.
가장 마지막 타입 매개변수(TResult)는 반환 타입입니다.
[코드]
class Program
{
static void Main()
{
Func<int, int, int> intCalcFunc = AddInt;
int result = intCalcFunc(10, 20);
Console.WriteLine($"정수 계산 결과: {result}");
Func<double, double, double> doubleCalcFunc = AddDouble;
double result2 = doubleCalcFunc(3.14, 1.59);
Console.WriteLine($"실수 계산 결과: {result2}");
}
static int AddInt(int a, int b)
{
return a + b;
}
static double AddDouble(double a, double b)
{
return a + b;
}
}
[실행 결과]
정수 계산 결과: 30
실수 계산 결과: 4.73
아까 만든 Calculate<T>는 Func로 완벽하게 대체할 수 있습니다.
<T>Action대리자는 Func와 정반대로, 반환 값이 없는(void) 모든 메서드를 참조합니다.
지시받은 임무를 수행하기만 할 뿐, 아무것도 들고 돌아오지 않습니다.
[특징]
Action: 입력 매개변수가 없음.
Action<T>: T타입의 입력 1개를 받음.
Action<T1, T2>: T1, T2타입의 입력 2개를 받음.
Action<T1, T2, ...>: 최대 16개의 입력 매개변수를 지원합니다.
[코드]
class Program
{
static void Main()
{
Action<string> logger = LogToConsole;
logger("프로그램이 시작되었습니다.");
}
static void LogToConsole(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
[실행 결과]
[LOG] 프로그램이 시작되었습니다.
<T>Predicate는 하나의 입력을 받아 bool값을 반환합니다.
주로 어떤 조건이 참(true)인지 거짓(false)인지 검사하는 용도로 사용됩니다.
[코드]
class Program
{
static void Main()
{
Predicate<int> numberChecker = IsEven;
Console.WriteLine($"10은 짝수인가? {numberChecker(10)}");
Console.WriteLine($"7은 짝수인가? {numberChecker(7)}");
}
static bool IsEven(int number)
{
return number % 2 == 0;
}
}
[실행 결과]
10은 짝수인가? True
7은 짝수인가? False
이 '유연함'이 왜 강력한지, 조금 더 발전된 예제로 보여드릴게요.
숫자 목록에서 '짝수'만 골라내고, 다른 목록에서는 '홀수'만 골라내고 싶다고 해봅시다.
[코드]
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 원본 데이터
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 1. "짝수만 골라줘" 라는 '규칙(IsEven)'을 전달
List<int> evenNumbers = FilterNumbers(numbers, IsEven);
Console.WriteLine("짝수 목록:");
PrintNumbers(evenNumbers);
// 2. "홀수만 골라줘" 라는 '규칙(IsOdd)'을 전달
List<int> oddNumbers = FilterNumbers(numbers, IsOdd);
Console.WriteLine("\n홀수 목록:");
PrintNumbers(oddNumbers);
// 3. "5보다 큰 수만 골라줘" 라는 '규칙(IsGreaterThanFive)'을 전달
List<int> greaterThanFive = FilterNumbers(numbers, IsGreaterThanFive);
Console.WriteLine("\n5보다 큰 수 목록:");
PrintNumbers(greaterThanFive);
}
// 핵심! 필터링하는 '규칙'을 대리자(filterCondition)로 받습니다.
static List<int> FilterNumbers(List<int> numbers, Predicate<int> filterCondition)
{
List<int> result = new List<int>();
foreach (int number in numbers)
{
// 전달받은 '규칙'으로 숫자를 검사합니다.
// 이 규칙이 IsEven일수도, IsOdd일수도 있습니다.
if (filterCondition(number))
{
result.Add(number);
}
}
return result;
}
// --- 필터링 '규칙'으로 사용될 메서드들 ---
static bool IsEven(int number)
{
return number % 2 == 0;
}
static bool IsOdd(int number)
{
return number % 2 != 0;
}
static bool IsGreaterThanFive(int number)
{
return number > 5;
}
// --- 결과를 출력하는 도우미 메서드 ---
static void PrintNumbers(List<int> numbers)
{
foreach (int number in numbers)
{
Console.Write(number + " ");
}
Console.WriteLine();
}
}
[실행 결과]
짝수 목록:
2 4 6 8 10
홀수 목록:
1 3 5 7 9
5보다 큰 수 목록:
6 7 8 9 10
대부분의 상황에서 delegate키워드를 직접 사용할 필요는 없습니다.
특별한 경우가 아니라면, Func, Action, Predicate를 사용하세요!
| 구분 | Func<...> | Action<...> | Predicate<T> |
|---|---|---|---|
| 반환 값 | O (있음) | X (없음, void) | O (bool 고정) |
| 목적 | 값을 계산하고 결과를 반환 | 특정 동작을 수행 | 조건이 참/거짓인지 판별 |
| 비유 | 결과를 가져오는 심부름꾼 | 임무만 수행하는 심부름꾼 | "Yes/No" 전문가 |