델리게이트와 이벤트
메서드를 값처럼 넘기고, 일이 일어났을 때 알립니다.
메서드를 값처럼 다루기
숫자는 int 변수에, 글자는
string 변수에 담습니다. 그러면
메서드는 어디에 담을까요. 담을 수 있다면 매개 변수로 넘기거나
속성에 두거나 목록에 모을 수 있을 텐데요.
델리게이트(delegate)가 그것입니다. 메서드를 담는 형식이며, 담을 메서드가 무엇을 받고 무엇을 돌려주는지를 미리 정해 둡니다. 모양이 맞는 메서드만 담기므로 잘못 담으면 컴파일 오류입니다.
// int 둘을 받아 int 를 돌려주는 메서드를 담는 형식 delegate int Calc(int a, int b);
이것이 있으면 "무엇을 할지" 를 부르는 쪽이 정하게 할 수 있습니다.
3.3의
Where(p => p.Price <= 3000) 가 바로 그 방식이었습니다 —
Where 는 거르는 절차만 알고, 무엇이 조건인지는
넘겨받습니다.
그리고 델리게이트에 event 를 붙이면
일이 일어났을 때 알리는 자리가 됩니다. 알림을 보내는 쪽은 누가
받는지 모르고, 받는 쪽은 언제 일어나는지 모릅니다. 둘이 서로를 몰라도 되게
떼어 놓는 것이 이 문법의 값어치입니다.
메서드를 담고 바꿔 담기
아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.
// 델리게이트 변수에 메서드 이름을 담습니다. 괄호를 적지 않습니다 — // 부르는 것이 아니라 메서드 자체를 담는 것이기 때문입니다. Calc op = Add; Console.WriteLine(op(3, 4)); // 7 op = Multiply; // 담긴 것을 바꿉니다 Console.WriteLine(op(3, 4)); // 12 // 형식을 직접 만들지 않아도 되는 것들이 준비되어 있습니다 Func<int, int, int> f = Add; // 돌려주는 것이 있을 때 Action<string> log = Console.WriteLine; // 없을 때 Console.WriteLine(f(10, 20)); log("메서드를 값처럼 넘겼습니다"); static int Add(int a, int b) => a + b; static int Multiply(int a, int b) => a * b; delegate int Calc(int a, int b);
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
op = Add; 에 괄호가 없다는 점을 보세요.
Add(3, 4) 는 부른 결과인 7 이고,
Add 는 메서드 자체입니다. 담아 두었다가
나중에 op(3, 4) 로 부릅니다.
형식을 직접 만들지 않아도 됩니다
delegate 로 형식을 선언하는 것은 C# 1.0의 방법이고, 지금은
모양별로 미리 준비된 제네릭 델리게이트를 그대로 사용합니다.
3.1의 형식 매개 변수로 받을 것과 돌려줄
것을 적습니다.
| 형식 | 모양 | 예시 |
|---|---|---|
| Action | 받지 않고 돌려주지 않음 | Action save = Flush; |
| Action<T> | 받되 돌려주지 않음 | Action<string> log = Console.WriteLine; |
| Func<TResult> | 받지 않고 돌려줌 | Func<int> now = GetCount; |
| Func<T, TResult> | 받고 돌려줌 | Func<int, bool> even = n => n % 2 == 0; |
| Predicate<T> | 받아서 참·거짓 | Predicate<int> even = n => n % 2 == 0; |
Func 는 마지막 형식 매개 변수가 돌려주는 것입니다 —
Func<int, int, int> 는 int 둘을
받아 int 를 돌려줍니다. 매개 변수는 16개까지 받습니다.
Predicate<T> 는
Func<T, bool> 과 하는 일이 같아 새 코드에서는 잘
사용하지 않습니다.
분기로 적을 때와 무엇이 다른가
static int Count(int[] items, string kind) { var n = 0; foreach (var it in items) { if (kind == "짝수" && it % 2 == 0) n++; else if (kind == "양수" && it > 0) n++; } return n; } // 조건이 늘 때마다 이 메서드를 고칩니다
static int Count(int[] items, Func<int, bool> match) { var n = 0; foreach (var it in items) if (match(it)) n++; return n; } Count(items, n => n % 2 == 0); Count(items, n => n > 0); // 조건이 늘어도 이 메서드는 그대로입니다
오른쪽이 3.3의
Count·Where 가 실제로 만들어진
방식입니다. 세는 절차와 무엇을 셀지가 갈라져 있어서, 조건이 늘어도
메서드는 그대로입니다.
여럿을 묶을 수 있습니다
델리게이트는 += 로 여러 메서드를 묶어
담을 수 있습니다. 부르면 담긴 순서대로 전부 실행됩니다.
Action notify = () => Console.WriteLine("로그를 남깁니다"); notify += () => Console.WriteLine("메일을 보냅니다"); notify += () => Console.WriteLine("화면을 새로 그립니다"); notify(); // 로그를 남깁니다 // 메일을 보냅니다 // 화면을 새로 그립니다 notify -= …; // 빼는 것도 됩니다
돌려주는 것이 있으면 마지막 하나만 남습니다. 전부 실행되기는 하지만 앞의 결과는 버려집니다.
Func<int> many = () => 1; many += () => 2; many += () => 3; Console.WriteLine(many()); // 3 — 앞의 1 과 2 는 버려집니다
그래서 여럿을 묶을 자리에는 Action 을
사용합니다. 결과를 모아야 한다면 목록을 넘겨 담게 하거나
GetInvocationList 로 하나씩 부르는 방법이 있지만, 그때는
델리게이트가 맞는 도구인지 다시 생각해 보는 편이 낫습니다.
event — 밖에서 손대지 못하게
알림을 보내는 자리를 델리게이트 필드로 그냥 두면 밖에서 무엇이든
할 수 있습니다. 통째로 null 을 담아 다른 사람이 등록해 둔
것을 지우거나, 아무 때나 직접 불러 일어나지도 않은 일을 알릴 수 있습니다.
class Clock { public event Action<int>? Ticked; // event 를 붙였습니다 public void Run() { for (var i = 1; i <= 3; i++) Ticked?.Invoke(i); // 안에서는 부를 수 있습니다 } } // 밖에서는 clock.Ticked += n => Console.WriteLine($"{n}초"); // 됩니다 clock.Ticked = null; // CS0070 clock.Ticked?.Invoke(1); // CS0070 // += 또는 -= 의 왼쪽에만 사용할 수 있습니다
event 를 붙이면 밖에서는
+= 와 -= 만 됩니다.
등록과 해제는 누구나, 그러나 지우는 것과 알리는 것은 그 형식 안에서만
입니다. 붙이는 낱말 하나로 이 규칙이 생깁니다.
?.Invoke(i) 의 물음표를 빠뜨리지 마세요. 아무도 등록하지
않았으면 null 이라, 그냥 부르면 예외가 발생합니다.
?. 는 null 이면 아무 일도
하지 않고 넘어갑니다(C# 6.0).
이름 짓는 관례가 있습니다. 일어난 뒤에 알리는 것은 과거형
(Ticked·Saved·Closed),
일어나기 전에 알려 막을 기회를 주는 것은 진행형
(Closing·Saving)입니다.
닷넷 라이브러리는 EventHandler<TEventArgs> 라는
델리게이트를 사용하는데, 보내는 쪽(object? sender)과 함께
넘기는 관례입니다.
이 문법, 몇 버전부터 사용할 수 있나요
- 1.0delegate · event · 여럿 묶기
- 3.0람다식으로 그 자리에서 넘기기 · Func · Action
- 6.0?.Invoke — null 이면 넘어가기
직접 해보기
배열과 조건을 받아 맞는 것이 몇 개인지 돌려주는
CountBy 를 만들어보세요. 짝수 개수와 50 이상인 것의
개수를 같은 메서드로 세어 봅니다.
계좌 클래스를 만들고 잔액이 바뀔 때 알리는 이벤트를 두세요. 받는 쪽 둘을 등록해 하나는 기록을 남기고, 하나는 잔액이 0 아래로 내려갈 때만 경고하게 해봅니다.
- 델리게이트는 메서드를 담는 형식입니다. 담을 때는 괄호를 적지 않습니다.
- 형식을 직접 선언하기보다
Action·Func를 사용합니다.Func는 마지막 형식 매개 변수가 돌려주는 것입니다. +=로 여럿을 묶으면 순서대로 전부 실행되지만, 돌려주는 값은 마지막 것만 남습니다.event를 붙이면 밖에서+=·-=만 됩니다. 지우는 것과 알리는 것은 그 형식 안에서만입니다.- 알릴 때는
?.Invoke입니다. 등록한 쪽이 없으면null이기 때문입니다.
C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.