C#고급 주제

Action과 Func 델리게이트

Action<T>, Func<T, TResult>, Predicate<T>의 시그니처 규칙, 람다·메서드 그룹 대입, 콜백·전략 패턴 구현에서의 활용을 정리합니다.

마지막 수정 2026년 3월 25일

기본 패턴

csharp
// 반환값 없음
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만 쓸 수도 있습니다.

csharp
Action              // () → void
Action<T>           // (T) → void
Action<T1, T2>      // (T1, T2) → void
// ... Action<T1, ..., T16>까지 지원

람다 대입메서드 그룹 대입 두 가지 방식 모두 사용할 수 있습니다.

csharp
// 람다 대입
Action<string> log = msg => Console.WriteLine(msg);

// 메서드 그룹 대입 — 시그니처가 일치하면 바로 할당 가능
Action<string> log2 = Console.WriteLine;

Func<T, TResult> — 반환값 있는 델리게이트

Func 은 반환값이 있는 델리게이트입니다. 타입 목록의 마지막 타입이 반환 타입이고, 나머지는 입력 파라미터입니다.

csharp
Func<TResult>           // () → TResult
Func<T, TResult>        // (T) → TResult
Func<T1, T2, TResult>   // (T1, T2) → TResult
// 예) Func<int, int, bool> = (int, int) → bool
csharp
Func<int, int, bool> isGreater = (a, b) => a > b;
bool ok = isGreater(5, 3);  // true

// 메서드 그룹 대입
Func<string, int> parse = int.Parse;
int n = parse("42");  // 42

Predicate<T> — bool 반환 특화형

Predicate<T>Func<T, bool>의 특화형으로, List<T>.FindAll, List<T>.RemoveAll 등 컬렉션 API에서 요구하는 타입입니다. 기능적으로는 Func<T, bool>과 동일하지만 의도를 명확히 드러낼 때 사용합니다.

csharp
Predicate<string> isLong = s => s.Length > 5;
var names = new List<string> { "Kim", "Alexander", "Jo" };
var longNames = names.FindAll(isLong);  // ["Alexander"]

콜백·전략 패턴

메서드 파라미터로 Action/Func를 받으면 전략 패턴을 간결하게 구현할 수 있습니다. 호출자가 동작을 주입하고 내부 로직은 그 동작을 실행합니다.

csharp
// 전략 주입 — 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 문서 주석 등으로 명확한 계약을 표현해야 할 때
csharp
// 가독성을 위한 named delegate
delegate decimal PricingStrategy(decimal basePrice, int quantity);

PricingStrategy bulk = (price, qty) => qty > 100 ? price * 0.8m : price;

빠른 정리

타입반환형파라미터 수주요 사용처
Actionvoid0 ~ 16콜백, 부수 효과, 이벤트 핸들러
Func<..., TResult>TResult0 ~ 16값 변환, 조건 판단, 팩토리
Predicate<T>bool1 (T)List<T> 필터링 API
사용자 정의 delegate자유자유이름 있는 계약, event 패턴

주의할 점

Action/Func는 멀티캐스트를 지원하지만 이벤트처럼 사용하면 예외 처리가 어렵습니다. 호출 목록 중 하나가 예외를 던지면 이후 핸들러는 실행되지 않습니다. 구독/알림 패턴이 필요하다면 event 키워드를 사용하고 예외를 명시적으로 처리하세요.

클로저로 외부 변수를 캡처할 때 루프 변수 캡처 함정에 주의하세요. for 루프에서 i를 직접 캡처하면 람다가 실행되는 시점에 이미 루프가 끝나 최종값만 참조합니다.

csharp
// ❌ 루프 변수 캡처 함정
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