기본 패턴
// 반환값 없음
Action<string> log = msg => Console.WriteLine($"[LOG] {msg}");
log("시작");
// 반환값 있음 — 마지막 타입이 반환형
Func<int, int, int> add = (a, b) => a + b;
int result = add(3, 4); // 7
// Predicate<T> — bool 반환 특화
Predicate<int> isEven = n => n % 2 == 0;
var evens = new List<int> { 1, 2, 3, 4 }.FindAll(isEven);
// 콜백 파라미터
void Process(IEnumerable<int> items, Action<int> onEach)
{
foreach (var item in items) onEach(item);
}설명
Action<T> — 반환값 없는 델리게이트
Action 은 반환값이 없는(void) 델리게이트입니다. 타입 파라미터를 최대 16개까지 받을 수 있으며, 파라미터 없이 Action만 쓸 수도 있습니다.
Action // () → void
Action<T> // (T) → void
Action<T1, T2> // (T1, T2) → void
// ... Action<T1, ..., T16>까지 지원람다 대입과 메서드 그룹 대입 두 가지 방식 모두 사용할 수 있습니다.
// 람다 대입
Action<string> log = msg => Console.WriteLine(msg);
// 메서드 그룹 대입 — 시그니처가 일치하면 바로 할당 가능
Action<string> log2 = Console.WriteLine;Func<T, TResult> — 반환값 있는 델리게이트
Func 은 반환값이 있는 델리게이트입니다. 타입 목록의 마지막 타입이 반환 타입이고, 나머지는 입력 파라미터입니다.
Func<TResult> // () → TResult
Func<T, TResult> // (T) → TResult
Func<T1, T2, TResult> // (T1, T2) → TResult
// 예) Func<int, int, bool> = (int, int) → boolFunc<int, int, bool> isGreater = (a, b) => a > b;
bool ok = isGreater(5, 3); // true
// 메서드 그룹 대입
Func<string, int> parse = int.Parse;
int n = parse("42"); // 42Predicate<T> — bool 반환 특화형
Predicate<T> 는 Func<T, bool>의 특화형으로, List<T>.FindAll, List<T>.RemoveAll 등 컬렉션 API에서 요구하는 타입입니다. 기능적으로는 Func<T, bool>과 동일하지만 의도를 명확히 드러낼 때 사용합니다.
Predicate<string> isLong = s => s.Length > 5;
var names = new List<string> { "Kim", "Alexander", "Jo" };
var longNames = names.FindAll(isLong); // ["Alexander"]콜백·전략 패턴
메서드 파라미터로 Action/Func를 받으면 전략 패턴을 간결하게 구현할 수 있습니다. 호출자가 동작을 주입하고 내부 로직은 그 동작을 실행합니다.
// 전략 주입 — Func으로 변환 로직을 파라미터로 받음
IEnumerable<TOut> Map<TIn, TOut>(
IEnumerable<TIn> source,
Func<TIn, TOut> transform)
{
foreach (var item in source)
yield return transform(item);
}
var doubled = Map(new[] { 1, 2, 3 }, x => x * 2);
// [2, 4, 6]커스텀 delegate vs Action/Func
대부분의 경우 Action/Func로 충분합니다. 다음과 같은 경우에는 이름 있는(named) delegate를 선언하는 것이 낫습니다.
event키워드와 함께 쓸 때 (event Func<int>보다event EventHandler가 관례에 맞음)- 같은 시그니처의 delegate가 서로 다른 의미를 가질 때 (코드 가독성 향상)
- XML 문서 주석 등으로 명확한 계약을 표현해야 할 때
// 가독성을 위한 named delegate
delegate decimal PricingStrategy(decimal basePrice, int quantity);
PricingStrategy bulk = (price, qty) => qty > 100 ? price * 0.8m : price;빠른 정리
| 타입 | 반환형 | 파라미터 수 | 주요 사용처 |
|---|---|---|---|
Action | void | 0 ~ 16 | 콜백, 부수 효과, 이벤트 핸들러 |
Func<..., TResult> | TResult | 0 ~ 16 | 값 변환, 조건 판단, 팩토리 |
Predicate<T> | bool | 1 (T) | List<T> 필터링 API |
| 사용자 정의 delegate | 자유 | 자유 | 이름 있는 계약, event 패턴 |
주의할 점
Action/Func는 멀티캐스트를 지원하지만 이벤트처럼 사용하면 예외 처리가 어렵습니다. 호출 목록 중 하나가 예외를 던지면 이후 핸들러는 실행되지 않습니다. 구독/알림 패턴이 필요하다면 event 키워드를 사용하고 예외를 명시적으로 처리하세요.
클로저로 외부 변수를 캡처할 때 루프 변수 캡처 함정에 주의하세요. for 루프에서 i를 직접 캡처하면 람다가 실행되는 시점에 이미 루프가 끝나 최종값만 참조합니다.
// ❌ 루프 변수 캡처 함정
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
actions.Add(() => Console.WriteLine(i)); // i를 직접 캡처
actions.ForEach(a => a()); // 3, 3, 3 출력
// ✅ 지역 변수로 복사
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy)); // copy를 캡처
}
actions.ForEach(a => a()); // 0, 1, 2 출력참고 링크
2 sources