이벤트 – 이것이 C# 이다.
이벤트는 대리자를 event 한정자로 수식하여 만듭니다.
이벤트를 선언하고 사용하는 절차
1. 대리자를 선언합니다.
– 대리자의 위치는 클래스 밖 또는 안에 선언해도 됩니다.
2. 선언한 대리자의 인스턴스를 event 한정자로 수식하여 선언합니다.
3. 이벤트 핸들러를 작성합니다.
– 이벤트 핸들러는 1. 에서 선언한 대리자와 일치하는 메소드면 됩니다.
4. 클래스의 인스턴스를 생성하고 이 객체의 이벤트에 3. 에서 작성한 이벤트 핸들러를 등록합니다.
5. 이벤트가 발생하면 이벤트 핸들러가 호출됩니다.
1. 대리자를 선언합니다.
– 대리자의 위치는 클래스 밖 또는 안에 선언해도 됩니다.
public delegate void EventHandler(string message); // EventHandler는 대리자의 이름 입니다.
2. 선언한 대리자의 인스턴스를 event 한정자로 수식하여 선언합니다.
// 1. 대리자 선언 public delegate void EventHandler(string message); // EventHandler는 대리자의 이름 입니다. class MyNotifier { // 2. 선언한 대리자의 인스턴스를 event 한정자로 수식하여 선언 public event EventHandler SomethingHappend; public void DoSomething(int number) { int temp = number % 10; if (temp != 0 && temp % 3 == 0) { // 2. number가 3, 6, 9 로 끝나는 값이 될때마다 이벤트가 발생 SomethingHappend(String.Format(" {0} : 짝 ", number)) } } }
3. 이벤트 핸들러를 작성합니다.
– 이벤트 핸들러는 1. 에서 선언한 대리자와 형식이 일치하는 메소드면 됩니다.
class MainApp { static public void MyHandler(string message) { Console.WriteLine(message); } } //... }
4. 클래스의 인스턴스를 생성하고 이 객체의 이벤트에 3. 에서 작성한 이벤트 핸들러를 등록합니다.
5. 이벤트가 발생하면 이벤트 핸들러가 호출됩니다.
class MainApp { static public void MyHandler(string message) { Console.WriteLine(message); } static void Main(string[] args) { // 인스턴스 생성 MyNotifier notifier = new MyNotifier(); // 이벤트 등록 notifier.SomethingHappend += new EventHandler(MyHandler); for (int i = 0; i < 30; i++) { // 이벤트가 발생하면 이벤트 핸들러가 호출됩니다. notifier.DoSomething(i); } } }
using System; namespace EventTest { delegate void EventHandler(string message); class MyNotifier { public event EventHandler SomethingHappened; public void DoSomething(int number) { int temp = number % 10; if ( temp != 0 && temp % 3 == 0) { SomethingHappened(String.Format("{0} : 짝", number)); } } } class MainApp { static public void MyHandler(string message) { Console.WriteLine(message); } static void Main(string[] args) { MyNotifier notifier = new MyNotifier(); notifier.SomethingHappened += new EventHandler(MyHandler); for (int i = 1; i < 30; i++) { notifier.DoSomething(i); } } } }
그렇다면 대리자와 이벤트의 차이점은 무엇일까?
이벤트는 public 한정자로 선언되어도 자신이 선언된 클래스 외부에서는 호출이 불가능합니다.
해당 이벤트를 포함하고 있는 클래스 안에서만 이벤트를 발생시킬 수 있다는 것.
– 대리자의 문제점인 불충분한 캡슐화를 보완하고 객체의 상태변화나 사건의 발생을 알리는 용도로 사용합니다.
반면 대리자는 public 이나 internal로 수식되어 있으면 클래스 외부에서라도 얼마든지 호출이 가능합니다.
– 콜백 용도로 많이 사용
static void Main(string[] args) { MyNotifier notifier = new MyNotifier(); // 불가능한 호출 notifier.SomethingHappened("테스트"); notifier.SomethingHappened += new EventHandler(MyHandler); for (int i = 1; i < 30; i++) { notifier.DoSomething(i); } }
다른 예제
using System; namespace Cooler { public class Thermostat { private float currentTemperature; // OnTemperatureChange 속성은 Action<float> 대리자 형식으로 구독자 목록을 저장한다. public Action<float> OnTemperatureChange { get; set; } // CurrentTemperature 속성은 Thermostat 클래스에서 제공하는 // 현재 온도값을 설정하거나 가져온다. public float CurrentTemperature { get { return currentTemperature; } set { if (value != currentTemperature) { currentTemperature = value; this.OnTemperatureChange(currentTemperature); // 구독자 호출 } } } } // 온도 조절장치는 가열 및 냉각 단위 즉, // 온도 변화를 여러 개의 수신기(구독자)로 전달(게시)한다. // Cooler와 Heater의 개체 정의 // 각 클래스느 장치를 켜야하는 기준 온도 값(Temperature)을 가지고 있다. class Cooler { public float Temperature { get; set; } // 생성자 기준 온도 값 생성 public Cooler(float temperature) { Temperature = temperature; } // Temperature 과 newTemperature을 비교하여 장치의 ON OFF 를 결정 // 구독자 메서드 역할을 하게되며 Thermostat 클래스에서 정의하는 대리자와 // 일치하는 매개변수 및 반환 형식을 가져야 한다. public void OnTemperatureChanged(float newTemperature) { if (newTemperature > Temperature) { Console.WriteLine("Cooler : On"); } else { Console.WriteLine("Cooler : Off"); } } class Heater { public float Temperature; public Heater(float temperature) { Temperature = temperature; } public void OnTemperatureChanged(float newTemperature) { if (newTemperature < Temperature) { Console.WriteLine("Heatrer : On"); } else { Console.WriteLine("Heater : Off"); } } } static void Main(string[] args) { Thermostat thermostat = new Thermostat(); Heater heater = new Heater(60); Cooler cooler = new Cooler(80); string temperature; // 게시자와 구독자 연결 thermostat.OnTemperatureChange += heater.OnTemperatureChanged; thermostat.OnTemperatureChange += cooler.OnTemperatureChanged; Console.Write("Enter temperaure : "); temperature = Console.ReadLine(); thermostat.CurrentTemperature = int.Parse(temperature); } } }
주의 알림을 받을 구독자가 하나도 없다면 OnTemperatureChange는 Null이며 NullReferenceException이 발생한다.
이와 같은 상황을 피하기 위해서 이벤트를 발생시키기 전에 null 여부를 확인해야한다.
public class Thermostat { private float currentTemperature; public Action<float> OnTemperatureChange { get; set; } public float CurrentTemperature { get { return currentTemperature; } set { if (value != currentTemperature) { currentTemperature = value; if (null != OnTemperatureChange) { this.OnTemperatureChange(currentTemperature); } } } } }
또한 대리자의 문제점은 아래와 같은 불충분한 캡슐화이다.
thermostat 클래스의 OnTemperatureChange 대리자를 다른 클래스가 호출하지 못하게 제한하는 것이 바람직하다.
static void Main(string[] args) { Thermostat thermostat = new Thermostat(); Heater heater = new Heater(60); Cooler cooler = new Cooler(80); string temperature; // 게시자와 구독자 연결 thermostat.OnTemperatureChange += heater.OnTemperatureChanged; thermostat.OnTemperatureChange += cooler.OnTemperatureChanged; thermostat.OnTemperatureChange(50); // 온도의 변화가 없음에도 구독자에게 알림 /* 온도 변화 Console.Write("Enter temperaure : "); temperature = Console.ReadLine(); thermostat.CurrentTemperature = int.Parse(temperature); */ }
이벤트의 캡슐화
원래의 클래스에서 변경사항
1. 기존의 OnTemperatureChange 속성 대신에 공용 속성 OnTemperatureChange를 새로 선언.
2. event 키워드를 추가로 적용하면 공용 대리자 필드에 할당 연산자를 외부에서 사용할 수 없다.
– event 키워드가 제공하는 캡슐화로 클래스의 외부에서 이벤트를 발생하거나 실수로 기존 구독자를 제거할 수 없다.
또한 대리자를 포함하고 있는 클래스만 대리자를 호출해 구독자들에게 이벤트를 발행할 수 있다.
3. //… OnTemperatureChange = delegate { }; 이벤트를 선언할 때 빈 대리자를 할당하여 null을 확인하지 않고 이벤트 발생시킬 수 있다.
public class Thermostat { public event EventHandler<TemperatureArgs> OnTemperatureChange = delegate { }; public class TemperatureArgs : EventArgs { public TemperatureArgs(float newTemperature) { NewTemperature = newTemperature; } public float NewTemperature { get; set; } } }
이벤트 구현의 보편적인 방식
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
결과적으로 public Action<float> OnTemperatureChange 대리자 형식의 매개변수 1개가 새로운 매개변수 2개로 대체됬으며
각각 이벤트 게시자와 이벤트 데이터다.
첫번째 매개변수인 sender는 대리자를 호출한 클래스의 인스턴스를 가리키고 있어야 한다.
두번째 매개변수인 TEventArgs e 는 Thermostat.TemperatureArgs 형식이다.
Thermostat.TemperatureArgs 는 추가로 NewTemperature하는 속성을 정의해 구독자에게 보낼 온도를 저장하는 수단으로 이용한다.