Func와 Action은 .NET이 미리 선언해 둔 제네릭 델리게이트 타입이므로, 델리게이트 타입을 먼저 선언하지 않고도 함수를 저장하고 넘길 수 있습니다. Func는 값을 반환하고 Action은 아무것도 반환하지 않습니다.
출력:
4
46.00
False
Hello!
Mia scored 87
기억할 규칙: Func에서는 마지막 타입 인수가 반환 타입입니다. Func<int, int, bool>은 int 두 개를 받아 bool을 반환합니다. Func<int>는 아무것도 받지 않고 int를 반환합니다. Action의 타입 인수는 모두 매개변수입니다.
델리게이트 무리
| 타입 | 매개변수 | 반환 |
|---|---|---|
Action | 없음 | void |
Action<T> | T | void |
Action<T1, T2> ... 최대 16개 | T1, T2, ... | void |
Func<TResult> | 없음 | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... 최대 16개 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int(음수, 0, 양수) |
Converter<TIn, TOut> | TIn | TOut |
Predicate, Comparison, Converter는 더 오래된 타입으로 List<T>와 Array 메서드에 나옵니다: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ는 전부 Func를 쓰며, 그래서 list.Where(...)와 list.FindAll(...)은 모양은 같지만 타입이 다른 람다를 받습니다:
출력:
17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y
메서드에 함수 넘기기
Func나 Action 매개변수는 알고리즘에서 달라지는 부분을 호출하는 쪽이 채우게 해 줍니다. 흔한 형태 두 가지는 "이것을 실행하고 측정하기"와 "성공할 때까지 이것을 시도하기"입니다:
출력:
attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2
Retry는 제네릭이므로 Func<int, T>가 무엇이든 반환할 수 있습니다. 컴파일러는 람다로부터 T를 string으로 추론했습니다. Twice에 넘긴 람다는 지역 변수 clicks를 바꾸는데, 람다가 값의 복사본이 아니라 변수 자체를 캡처하기 때문에 동작합니다.
메서드 그룹: 이름으로 메서드 넘기기
Func나 Action을 채우는 데 람다가 꼭 필요하지는 않습니다. 시그니처가 맞는 기존 메서드의 이름("메서드 그룹")이 바로 변환됩니다:
출력:
parsed: 43
sat is weekend: True
total quantity: 20
Select(int.Parse)는 호출이 한 겹 적다는 점만 빼면 Select(s => int.Parse(s))와 같습니다. 컴파일러는 대상 타입에 맞는 오버로드를 고릅니다. int.Parse에는 오버로드가 여러 개 있고, Func<string, int>에 맞는 것은 Parse(string)뿐입니다. 컴파일러가 하나로 정하지 못하면(여러 오버로드가 똑같이 맞거나, 메서드 그룹으로부터 제네릭 메서드의 타입 인수를 추론할 수 없으면) 호출이 컴파일되지 않으며, 호출을 명시하는 람다로 해결됩니다.
Func로 쓰인 메서드 그룹도 람다와 똑같이 델리게이트가 호출될 때 나중에 실행됩니다. 차이는 쓰는 방식뿐입니다.
함수 반환하기
메서드는 함수를 만들어 반환할 수 있습니다. 반환된 Func는 자신을 만들 때 쓴 값을 지니고 다닙니다:
출력:
140
180
162
Twice(staff)는 직원 할인을 두 번 적용하는 새 함수를 만듭니다. 200, 그다음 180, 그다음 162입니다.
함수 딕셔너리
함수를 딕셔너리에 저장하면 긴 switch가 조회 표가 됩니다. 명령 처리기, 계산기, 메뉴 동작에 흔한 형태이며, 실행 중에 새 항목을 추가할 수 있습니다:
출력:
12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE
연산자를 추가하는 것은 딕셔너리 항목 하나를 더하는 것이며 루프는 바뀌지 않습니다. 두 번째 딕셔너리에 넘긴 StringComparer.OrdinalIgnoreCase는 명령 이름이 대소문자를 구분하지 않게 합니다.
async 함수: Action이 아니라 Func<Task>
async 코드에서는 델리게이트 타입이 호출하는 쪽이 작업을 기다릴 수 있는지를 결정합니다:
Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save(); // the caller can await it and see its exceptions
Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);
Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad(); // async void: fire and forget, exceptions escape
Action에 대입한 async 람다는 async void 메서드가 됩니다. 아무것도 이를 await할 수 없고, 그 안의 예외는 호출하는 쪽에 절대 도달하지 않습니다. 예외는 스레드 풀(데스크톱 앱이라면 UI 스레드)에서 다시 던져지며, 보통 프로세스를 죽입니다. async 작업을 받는 메서드를 작성할 때는 Func<Task>(또는 Func<Task<T>>)를 받으세요.
델리게이트를 직접 선언해야 할 때
Func와 Action이 대부분의 요구를 해결합니다. 다음 경우에는 이름 붙은 델리게이트 타입을 선언하세요:
- 시그니처에
ref,out,in매개변수가 있을 때.Func는 이를 표현할 수 없으므로TryParse스타일 함수에는delegate bool TryParser<T>(string text, out T value);가 필요합니다. - 이름이 공개 API에서의 역할을 문서화할 때. 메서드 시그니처의
delegate bool Validator(string input)은Func<string, bool>보다 많은 것을 말해 주고, 매개변수 이름이 IntelliSense에 나타납니다. params매개변수가 필요할 때. 제네릭 델리게이트는 이것도 가질 수 없습니다.
사용자 정의 델리게이트 타입이 여전히 등장하는 또 다른 곳은 이벤트이지만, 대부분은 기본 제공 EventHandler<TEventArgs>로 해결됩니다. 델리게이트 타입을 선언하고 조합하는 방법은 델리게이트 페이지에서 다룹니다.
자주 묻는 질문
C#에서 Func와 Action의 차이는 무엇인가요?
Func는 값을 반환하고 Action은 반환하지 않습니다. Func<int, string, bool>에서 마지막 타입 인수(bool)가 반환 타입이고 나머지는 매개변수입니다. Action<int, string>은 int와 string을 받고 void를 반환합니다. 둘 다 매개변수 0개부터 16개까지의 버전이 있습니다.
C#에서 함수를 매개변수로 넘기려면 어떻게 하나요?
매개변수를 알맞은 모양의 Func나 Action으로 선언하고 메서드 안에서 호출합니다: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. 호출하는 쪽은 람다(Apply(80m, p => p * 0.9m))나 메서드 이름(Apply(80m, HalfPrice))을 넘깁니다.
C#의 Predicate<T>는 무엇인가요?
T를 받아 bool을 반환하는 델리게이트 타입으로, Func<T, bool>과 모양이 같습니다. Func보다 먼저 있었고 List<T>.Find, FindAll, RemoveAll, Exists, Array.Find에서 쓰입니다. 두 타입은 서로 변환되지 않지만 람다는 어느 쪽으로든 변환됩니다.
async 코드에서 Func는 어떻게 쓰나요?
결과가 없는 async 함수에는 Func<Task>를, 결과가 있으면 Func<T, Task<TResult>>를 써서 호출하는 쪽이 await할 수 있게 하세요. async 람다를 Action에 대입하는 것은 피하세요. 그러면 async void가 되어 await할 수 없고, 예외를 호출하는 쪽에서 잡을 수 없습니다.
Func에 out이나 ref 매개변수를 쓸 수 있나요?
아니요. Func와 Action의 제네릭 매개변수는 일반 값 매개변수이므로 Func<string, out int, bool>이라고 쓸 방법이 없습니다. TryParse 같은 시그니처에는 델리게이트를 직접 선언하세요: delegate bool TryParser<T>(string text, out T value);.