Quick Reference
반환값 없는 동기 콜백은 Action, 값을 돌려주는 동기 콜백은 Func<..., TResult>, 완료·예외·취소를 호출자가 기다려야 하는 비동기 콜백은 Func<..., CancellationToken, Task>로 선언합니다. delegate 호출은 null·예외·여러 구독자 실행 정책까지 계약에 포함해야 합니다.
// 반환값 없음
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)
{
ArgumentNullException.ThrowIfNull(onEach);
foreach (var item in items) onEach(item);
}
// 완료와 취소를 호출자가 관찰해야 하는 비동기 콜백
Func<CancellationToken, Task> refreshAsync = ct => Task.Delay(100, ct);
await refreshAsync(CancellationToken.None);문법
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]비동기 콜백은 Task를 계약으로 남긴다
async lambda를 Action에 대입하면 반환 Task가 사라져 async void가 됩니다. 이벤트 핸들러처럼 void가 요구되는 경우를 제외하고, 완료·예외·취소를 관찰해야 하는 콜백은 Func<..., Task>로 받고 await합니다.
async Task ProcessAsync(
IEnumerable<int> items,
Func<int, CancellationToken, Task> onEachAsync,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(onEachAsync);
foreach (int item in items)
{
ct.ThrowIfCancellationRequested();
await onEachAsync(item, ct);
}
}
using var cts = new CancellationTokenSource();
await ProcessAsync(
new[] { 1, 2, 3 },
async (item, token) => await Task.Delay(item, token),
cts.Token);커스텀 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;delegate 형태 선택
| 타입 | 반환형 | 파라미터 수 | 주요 사용처 |
|---|---|---|---|
Action | void | 0 ~ 16 | 콜백, 부수 효과, 이벤트 핸들러 |
Func<..., TResult> | TResult | 0 ~ 16 | 값 변환, 조건 판단, 팩토리 |
Predicate<T> | bool | 1 (T) | List<T> 필터링 API |
| 사용자 정의 delegate | 자유 | 자유 | 이름 있는 계약, event 패턴 |
주의할 점
Action/Func는 멀티캐스트를 지원하지만 이벤트처럼 사용하면 예외 처리가 어렵습니다. 호출 목록 중 하나가 예외를 던지면 이후 핸들러는 실행되지 않습니다. 구독/알림 패턴이 필요하다면 event 키워드를 사용하고 예외를 명시적으로 처리하세요.
여러 Func를 결합해 호출하면 모든 호출은 순서대로 실행되지만 호출 결과는 마지막 delegate의 반환값만 남습니다. 여러 결과나 비동기 작업을 모아야 하면 invocation list와 Task.WhenAll을 명시적으로 다루는 API가 더 분명합니다.
클로저로 외부 변수를 캡처할 때 루프 변수 캡처 함정에 주의하세요. 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 출력foreach 반복 변수는 C# 5부터 반복마다 새로 만들어지므로 일반적인 foreach 캡처는 안전합니다. 반면 for의 제어 변수나 반복 중 바뀌는 외부 상태는 위처럼 지역 복사본을 캡처해야 합니다.
참고 링크
3 sources