Menu

C# 델리게이트: 선언, 호출, 멀티캐스트, 콜백

C#의 델리게이트가 무엇이고 어떻게 쓰는지: 델리게이트 타입 선언, 메서드나 람다나 익명 메서드로 만들기, 호출하기, 콜백으로서의 델리게이트, +=와 -=를 쓰는 멀티캐스트 델리게이트, ?.Invoke로 null 검사하기, 그리고 델리게이트와 Func, Action, 이벤트의 관계를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

델리게이트는 메서드 시그니처를 나타내는 타입입니다. 델리게이트 타입의 변수는 그 시그니처를 가진 메서드에 대한 참조를 담고, 변수를 호출하면 그 메서드가 호출됩니다. 이것이 C#이 함수를 값으로 다루는 방법입니다. 저장하고, 다른 메서드에 넘기고, 나중에 호출할 수 있습니다.

출력:

40
70
68.00

delegate decimal PriceRule(decimal price);는 class처럼 새 타입을 선언합니다. decimal 하나를 받아 decimal을 반환하는 모든 메서드가 여기에 맞습니다. 변수 rule은 차례로 세 개의 다른 메서드를 가리켰고, 매번 그때 담고 있던 메서드가 실행되었습니다.

선언, 생성, 호출

델리게이트 타입 선언은 앞에 delegate 키워드를 붙인 메서드 시그니처처럼 생겼습니다. 보통 네임스페이스 수준에서 클래스 옆에 두거나, 클래스 안에 중첩해 둡니다.

델리게이트 인스턴스를 만드는 방법은 네 가지입니다:

PriceRule a = HalfPrice;                             // method group (most common)
PriceRule b = new PriceRule(HalfPrice);              // explicit constructor, same thing
PriceRule c = p => p - 5m;                            // lambda expression
PriceRule d = delegate (decimal p) { return p; };    // anonymous method (C# 2 syntax)

호출하는 방법은 두 가지이며 똑같습니다: rule(80m)과 rule.Invoke(80m). 컴파일러가 대입 시점에 시그니처를 검사하므로, 맞지 않는 메서드는 실행 중의 놀라움이 아니라 컴파일 오류가 됩니다.

델리게이트는 인스턴스 메서드도 가리킬 수 있습니다. 이때는 메서드와 그 메서드를 호출할 객체를 함께 기억합니다:

출력:

Ana got: Your order shipped
target: Mailbox, method: Receive

Target은 델리게이트가 메서드를 호출할 객체이고(정적 메서드면 null), Method는 메서드를 설명합니다. 대상 객체를 담고 있다는 것은 델리게이트가 그 객체를 살려 둔다는 뜻이기도 하며, 이는 이벤트에서 중요합니다.

콜백으로서의 델리게이트

델리게이트의 주된 역할은 메서드가 호출하는 쪽이 고른 코드를 호출하게 하는 것입니다. 메서드는 무언가가 언제 일어나는지 정하고, 호출하는 쪽은 무엇이 일어나는지 정합니다:

출력:

[##        ] 25%
[#####     ] 50%
[#######   ] 75%
[##########] 100%
import finished

ImportRows는 콘솔이나 막대에 대해 아무것도 모릅니다. 같은 메서드가 호출하는 쪽이 무엇을 넘기느냐에 따라 진행 막대, 로그 한 줄, UI 갱신을 움직일 수 있습니다. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action)과 모든 LINQ 연산자가 이 패턴을 따릅니다.

멀티캐스트 델리게이트: +=와 -=

델리게이트는 메서드를 둘 이상 담을 수 있습니다. +=는 호출 목록에 메서드를 덧붙이고, -=는 제거하며, 델리게이트를 호출하면 모든 메서드가 순서대로 호출됩니다:

출력:

email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002

멀티캐스트 델리게이트의 세 가지 세부 사항이 실무에서 중요합니다:

  • 반환값: 델리게이트 타입이 값을 반환하면 멀티캐스트 델리게이트를 호출했을 때 마지막 메서드의 결과만 반환됩니다. 모든 결과를 모으려면 GetInvocationList()를 돌며 하나씩 직접 호출하세요.
  • 예외: 한 메서드가 예외를 던지면 나머지 메서드는 실행되지 않고 예외가 호출하는 쪽으로 전달됩니다.
  • 불변성: 델리게이트는 불변입니다. +=는 목록이 더 긴 새 델리게이트를 만들어 변수에 다시 대입하며, 그래서 복사본에서 -=로 제거해도 원본에는 영향이 없습니다.

-=로 람다를 제거하는 것은 같은 델리게이트 인스턴스를 넘길 때만 동작합니다. 같은 람다를 두 번 쓰면 서로 다른 두 델리게이트가 만들어지므로 handlers -= id => Log(id);는 아무것도 제거하지 않습니다. 구독을 해제할 계획이라면 람다를 변수에 저장하세요.

null 델리게이트와 ?.Invoke

아무것도 대입되지 않은 델리게이트 변수는 null이고, -=로 마지막 메서드를 제거해도 null이 됩니다. null을 호출하면 NullReferenceException이 발생합니다:

출력:

no handler, no crash
saved draft.txt
NullReferenceException

?.Invoke는 선택적 콜백을 호출하는 표준 방법입니다. ?. 연산자는 괄호 바로 앞에 둘 수 없으므로(onSaved?("x")는 올바른 문법이 아닙니다), 이 패턴은 델리게이트의 Invoke 메서드를 이름으로 호출하며, 이는 줄여 쓴 호출과 같은 일을 합니다.

델리게이트, Func, Action, 이벤트

시그니처마다 델리게이트 타입을 선언하는 것은 번거로우므로 .NET은 제네릭 타입을 제공합니다:

기본 제공 타입동등한 직접 선언
Actiondelegate void Action();
Action<T>delegate void Action<T>(T arg);
Func<TResult>delegate TResult Func<TResult>();
Func<T, TResult>delegate TResult Func<T, TResult>(T arg);
Predicate<T>delegate bool Predicate<T>(T obj);

첫 예제의 PriceRule은 Func<decimal, decimal>과 모양이 같지만, 둘은 서로 다른 타입이며 서로 변환되지 않습니다. 시그니처가 같은 기본 제공 타입 둘끼리도 마찬가지입니다:

Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'

Predicate<int> ok = new Predicate<int>(isPositive);   // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x);              // or with a lambda

람다는 호환되는 모든 델리게이트 타입으로 변환되므로, API는 보통 람다를 받고 이 문제는 생기지 않습니다. 제네릭 타입 무리와 직접 선언이 여전히 가치 있는 경우는 Func와 Action 페이지에서 다룹니다.

이벤트는 델리게이트 위에 바로 만들어져 있습니다. event 필드는 외부 코드가 +=와 -=만 할 수 있고 호출하거나 덮어쓸 수는 없는 멀티캐스트 델리게이트입니다. 위 멀티캐스트 예제의 구독자 목록이 바로 이벤트가 이런 추가 보호와 함께 대신 관리해 주는 것입니다. 이벤트는 별도 페이지에서 다룹니다.

자주 묻는 질문

C#에서 델리게이트란 무엇인가요?

델리게이트는 메서드에 대한 타입 안전한 참조입니다. delegate decimal PriceRule(decimal price);처럼 시그니처로 델리게이트 타입을 선언하면, 그 시그니처를 가진 모든 메서드(정적, 인스턴스, 람다)를 그 타입의 변수에 저장하고 나중에 그 변수를 통해 호출할 수 있습니다. 델리게이트는 C#이 함수를 값으로 넘기는 방법입니다.

C#에서 델리게이트와 Func의 차이는 무엇인가요?

Func<T, TResult>와 Action<T>는 .NET이 매개변수와 반환 타입에 대한 제네릭으로 미리 선언해 둔 델리게이트 타입입니다. 직접 delegate를 선언하면 타입에 역할을 문서화하는 이름과 매개변수 이름이 생기고, ref, out, in 매개변수를 지원합니다. 대부분의 코드에는 Func와 Action으로 충분합니다.

C#의 멀티캐스트 델리게이트란 무엇인가요?

여러 메서드를 담는 델리게이트입니다. +=는 호출 목록에 메서드를 추가하고 -=는 제거하며, 델리게이트를 호출하면 모두 순서대로 호출됩니다. 델리게이트가 값을 반환하면 호출하는 쪽은 마지막 메서드의 결과만 받고, 한 메서드에서 예외가 나면 나머지는 실행되지 않습니다.

null일 수 있는 델리게이트는 어떻게 안전하게 호출하나요?

null 조건부 연산자를 쓰세요: onProgress?.Invoke(50);. 메서드가 하나도 대입되지 않은 델리게이트 변수는 null이고, 이를 바로 호출하면 NullReferenceException이 발생합니다. ?.Invoke는 변수를 한 번만 읽으므로, 다른 스레드가 같은 순간에 마지막 처리기를 제거해도 안전합니다.

언제 Func나 Action 대신 직접 델리게이트 타입을 선언해야 하나요?

시그니처에 ref, out, in 매개변수가 필요할 때(Func와 Action은 이를 표현할 수 없습니다), 이름 붙은 타입이 공개 API를 더 명확하게 만들 때(delegate bool Validator(string input)), 또는 IntelliSense에 매개변수 이름이 보이게 하고 싶을 때입니다. 그 밖에는 모든 .NET 개발자가 알아보는 Func와 Action을 우선 쓰세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기