이벤트는 일이 생기면 알려 달라는 약속이다
플레이어가 죽었을 때 할 일을 생각해 봅시다. 화면에 문구를 띄우고, 소리를 내고, 기록을 저장하고, UI 를 갱신합니다.
class Player
{
public void TakeDamage(int amount)
{
hp -= amount;
if (hp <= 0)
{
ui.ShowGameOver();
sound.PlayDeath();
logger.Save("죽음");
quest.OnPlayerDied();
}
}
}
돌아갑니다. 그런데 Player 가 UI 와 소리와 기록과 퀘스트를 전부 알고 있습니다.
- 죽었을 때 할 일이 하나 늘면
Player를 고쳐야 합니다. Player만 따로 떼어 쓸 수가 없습니다. 넷이 다 있어야 컴파일됩니다.- UI 쪽 코드가 바뀌면
Player도 같이 흔들립니다.
플레이어가 하는 일은 체력을 깎는 것인데, 남의 사정을 너무 많이 압니다.
“죽었다” 고 알리기만 하고, 그걸 듣고 무엇을 할지는 듣는 쪽이 정하게 하면 됩니다.
대리자를 하나 열어 두면 된다
앞에서 대리자에 += 로 여러 개를 담을 수 있다고 했습니다. 그걸 쓰면 됩니다.
class Player
{
public event Action Died;
public void TakeDamage(int amount)
{
hp -= amount;
if (hp <= 0)
{
Died?.Invoke(); // 알리기만 합니다
}
}
}
Player 는 이제 UI 도 소리도 모릅니다. 죽었다는 사실만 알립니다.
듣고 싶은 쪽이 등록합니다.
player.Died += ui.ShowGameOver;
player.Died += sound.PlayDeath;
player.Died += () => logger.Save("죽음");
관심 없으면 등록을 안 하면 됩니다. 그만 듣고 싶으면 -= 로 뺍니다.
알리는 쪽은 누가 듣는지 모르고, 듣는 쪽은 언제 일어날지 모릅니다.
아는 것은 Died 라는 창구 하나뿐입니다.
event 는 대리자에 자물쇠를 채운 것이다
Action Died 라고만 해도 되는데 앞에 event 를 붙였습니다. 왜일까요.
그냥 대리자면 밖에서 이런 것이 됩니다.
player.Died = ui.ShowGameOver; // 남이 등록한 것을 통째로 날림
player.Died(); // 밖에서 마음대로 알림을 발생시킴
+= 대신 = 를 쓰면 앞서 등록한 것들이 전부 사라집니다.
그리고 죽지도 않았는데 밖에서 Died() 를 부를 수 있습니다.
event 를 붙이면 밖에서는 += 와 -= 만 됩니다.
| 클래스 안 | 클래스 밖 | |
|---|---|---|
+= -= |
된다 | 된다 |
= 로 통째로 넣기 |
된다 | 안 된다 |
| 부르기 | 된다 | 안 된다 |
앞에서 캡슐화를 볼 때 창구를 좁히면 실수할 길이 막힌다고 했습니다.
event 는 대리자에 그걸 적용한 것입니다.
무엇이 일어났는지 같이 넘기기
알림에 정보를 실어 보내려면 Action<T> 를 씁니다.
public event Action<int> HpChanged;
public void TakeDamage(int amount)
{
hp -= amount;
HpChanged?.Invoke(hp);
}
player.HpChanged += (hp) => healthBar.SetValue(hp);
.NET 관습은 EventHandler 를 쓰는 것인데, 입문 단계에서는 Action 으로 충분합니다.
중요한 것은 무엇을 알릴지 정해서 넘긴다는 것입니다.
반드시 null 검사를 한다
아무도 등록하지 않았으면 null 입니다.
Died(); // 아무도 안 들으면 NullReferenceException
Died?.Invoke(); // 이렇게 씁니다
이벤트는 아무도 안 들을 수 있다는 것이 정상입니다.
그래서 ?.Invoke() 가 관용구처럼 굳어졌습니다.
등록을 빼는 것을 잊으면
+= 로 등록하면 알리는 쪽이 듣는 쪽을 가리키게 됩니다.
player.Died += ui.ShowGameOver;
player 안의 Died 가 ui 를 붙잡고 있습니다.
ui 를 없앴다고 생각해도, player 가 살아 있는 한 ui 는 GC 가 못 치웁니다.
앞에서 아무도 안 가리키는 것만 치워진다고 했습니다. 여기서는 이벤트가 가리키고 있습니다.
그래서 화면을 닫거나 객체를 버릴 때 -= 로 빼야 합니다.
player.Died -= ui.ShowGameOver;
등록한 곳에는 빼는 곳이 짝으로 있어야 합니다. 게임에서 씬을 여러 번 오갈 때 메모리가 계속 늘어난다면 대개 이것입니다.
한 가지 더 있습니다. 람다로 등록하면 뺄 수가 없습니다.
player.Died += () => Console.WriteLine("죽음");
player.Died -= () => Console.WriteLine("죽음"); // 안 빠집니다
두 람다는 코드가 같아도 다른 것입니다. 빼야 할 것이면 변수에 담아 두고 등록합니다.
Action handler = () => Console.WriteLine("죽음");
player.Died += handler;
player.Died -= handler; // 이건 빠집니다
언제 쓰나
한 쪽이 다른 쪽을 알면 곤란할 때 씁니다.
- 버튼 클릭, 입력, 충돌 같은 것
- 데이터가 바뀌었을 때 화면을 갱신
- 작업이 끝났을 때 알리기
공통점은 알리는 쪽이 더 아래에 있다는 것입니다.
Player 는 UI 를 몰라야 하고 UI 는 Player 를 알아도 됩니다.
아래에서 위로 알려야 할 때, 그 방향을 뒤집지 않고 알리는 방법이 이벤트입니다.
정리
- 이벤트는 일이 생기면 알려 주는 약속입니다. 알리는 쪽은 누가 듣는지 모릅니다.
- 듣고 싶은 쪽이
+=로 등록하고-=로 뺍니다. event를 붙이면 밖에서는 등록과 해제만 됩니다. 통째로 덮어쓰거나 부를 수 없습니다.- 아무도 안 들을 수 있으므로
?.Invoke()로 부릅니다. - 등록하면 알리는 쪽이 듣는 쪽을 붙잡습니다. 빼지 않으면 GC 가 못 치웁니다.
- 람다로 등록한 것은 뺄 수 없습니다. 뺄 것이면 변수에 담아 둡니다.
- 아래에 있는 것이 위에 알려야 할 때 씁니다.