대리자는 메서드를 담는 변수다
목록에서 조건에 맞는 것만 세는 메서드를 만든다고 해 봅시다.
int CountEven(List<int> nums)
{
int count = 0;
foreach (var n in nums) if (n % 2 == 0) count++;
return count;
}
양수만 세려면 CountPositive 를 만들고, 100 이상만 세려면 또 만듭니다.
반복하는 부분은 완전히 같고 if 안의 조건만 다릅니다.
제네릭은 여기서 도움이 안 됩니다. 제네릭은 타입을 나중에 정하는 것인데, 여기서 나중에 정하고 싶은 것은 동작입니다.
무엇을 할지를 변수에 담아 넘길 수 있으면 됩니다.
대리자는 이렇게 생겼다
delegate bool Check(int n);
메서드의 생김새를 적은 것입니다. int 하나를 받아 bool 을 돌려주는 메서드,
그런 것을 담을 수 있는 타입이 Check 입니다.
bool IsEven(int n) { return n % 2 == 0; }
bool IsPositive(int n) { return n > 0; }
Check c = IsEven; // 괄호 없이 이름만
Console.WriteLine(c(4)); // True
c = IsPositive;
Console.WriteLine(c(4)); // True
IsEven 뒤에 괄호가 없습니다. 부르는 것이 아니라 담는 것이라 그렇습니다.
괄호를 붙이면 실행되고 그 결과가 담깁니다. 완전히 다른 일입니다.
담아 두고 나중에 c(4) 로 부릅니다. 그때 실제로 실행됩니다.
맞아야 하는 것은 받는 것과 돌려주는 것뿐입니다. 이름은 상관없습니다.
Check c = IsPositive; // bool(int) 라 됩니다
Check c = Console.WriteLine; // 생김새가 안 맞아서 컴파일 에러
이제 조건을 넘길 수 있다
int Count(List<int> nums, Check test)
{
int count = 0;
foreach (var n in nums) if (test(n)) count++;
return count;
}
Count(nums, IsEven);
Count(nums, IsPositive);
반복하는 코드는 한 벌이고, 조건만 갈아 끼웁니다.
어떻게 반복할지는 Count 가 알고, 무엇을 고를지는 부르는 쪽이 정합니다.
앞에서 다형성을 볼 때 비슷한 이야기를 했습니다. 거기서는 클래스를 만들어 갈아 끼웠고, 여기서는 메서드 하나를 갈아 끼웁니다. 동작 하나만 다르면 클래스를 만들 것 없이 이쪽이 가볍습니다.
Action 과 Func 는 미리 만들어 둔 것이다
delegate 를 매번 선언하는 것이 번거롭습니다.
생김새가 같으면 이름만 다른 타입을 계속 만들게 됩니다.
그래서 미리 만들어 둔 것이 있습니다.
Func<int, bool> test = IsEven; // int 를 받아 bool 을 돌려준다
Action<string> print = Console.WriteLine; // string 을 받고 안 돌려준다
| 돌려주는 것 | 꺾쇠 안 | |
|---|---|---|
Action |
없음 (void) |
받는 타입들 |
Func |
있음 | 받는 타입들 + 맨 뒤가 돌려줄 타입 |
Action // 받지도 돌려주지도 않음
Action<int> // int 를 받고 안 돌려줌
Action<int, string> // int 와 string 을 받음
Func<int> // 안 받고 int 를 돌려줌
Func<int, bool> // int 를 받고 bool 을 돌려줌
Func<int, int, int> // int 둘을 받고 int 를 돌려줌
Func 의 맨 마지막 칸이 돌려줄 타입입니다. 이것만 기억하면 됩니다.
앞의 코드를 다시 쓰면 이렇게 됩니다.
int Count(List<int> nums, Func<int, bool> test)
delegate 선언이 사라졌습니다. 실무에서 delegate 를 직접 선언할 일은 드뭅니다.
여러 개를 담을 수 있다
대리자에는 메서드를 여러 개 담을 수 있습니다.
Action<string> log = Console.WriteLine;
log += SaveToFile;
log += SendToServer;
log("서버 시작"); // 셋이 순서대로 실행됩니다
+= 로 더하고 -= 로 뺍니다. 부르면 담긴 것이 등록된 순서대로 전부 실행됩니다.
여기서 주의할 것이 있습니다. Func 처럼 값을 돌려주는 대리자에 여러 개를 담으면
마지막 것의 결과만 나옵니다. 나머지는 버려집니다.
그래서 여러 개를 담는 것은 보통 Action 입니다.
null 이면 터진다
대리자는 참조 타입입니다. 아무것도 안 담겨 있으면 null 입니다.
Action<string> log = null;
log("안녕"); // NullReferenceException
담긴 것을 전부 빼도 null 이 됩니다. 0개짜리 대리자는 없습니다.
if (log != null) log("안녕");
log?.Invoke("안녕"); // 짧게 쓰는 방법
?. 는 앞이 null 이면 그냥 넘어가라는 뜻입니다. null 관련 문법은 뒤에서 다룹니다.
Invoke 는 대리자를 부르는 메서드입니다. log("안녕") 과 같은 일인데,
?. 를 붙이려면 이 형태로 써야 합니다.
어디에 쓰나
언제 실행될지는 모르지만 무엇을 할지는 지금 정해 두고 싶을 때 씁니다.
- 버튼을 눌렀을 때 할 일
- 정렬할 때 무엇을 기준으로 비교할지
- 목록에서 무엇을 고를지
- 작업이 끝나면 알려 줄 곳
전부 “부르는 쪽은 언제인지 알고, 넘기는 쪽은 무엇인지 안다” 는 구조입니다. 둘을 떼어 놓는 것이 대리자가 하는 일입니다.
정리
- 대리자는 메서드를 담는 변수입니다. 값이 아니라 무엇을 할지를 담습니다.
- 담을 때는 괄호 없이 이름만 적습니다. 괄호를 붙이면 실행됩니다.
- 받는 것과 돌려주는 것이 맞으면 담을 수 있습니다. 이름은 상관없습니다.
Action은 안 돌려주고Func는 돌려줍니다.Func의 꺾쇠 맨 뒤가 돌려줄 타입입니다.+=로 여러 개를 담을 수 있고 순서대로 실행됩니다. 값은 마지막 것만 남습니다.- 아무것도 안 담겨 있으면
null입니다. 부르기 전에 확인합니다. - 부르는 쪽과 실제 동작을 떼어 놓을 때 씁니다.