본문 바로가기

C#

C# EventHandler 표준 이벤트 구현

// System에 있는 이벤트 핸들러의 정의
public delegate void EventHandler<TEventArgs>
	(object source, TEventArgs e) where TEventArgs : EventArgs;

이벤트 핸들러: .NET에서 이벤트를 일관된 패턴으로 사용하도록 구현해놓은 표준 객체

.Net에서는 표준 이벤트를 구현하는 예약어이지만 원래는 이벤트(마우스 클릭 등) 발생시 함께 발생시킬 메서드들도 모두 핸들러라고 부르기도 한다. 특정 이벤트를 관찰하고 다루기 때문이다.

  • System.EventArgs 미리 정의된 클래스를 이용하거나 (아무 멤버도 없음. 정적 Empty 속성만 존재.)
  • 발생하는 이벤트에 대한 정보 전달이 필요할 경우 EventArgs를 상속받는 파생 클래스를 작성하여 사용함.

 

EventArgs를 상속받는 파생 클래스를 작성하는 방법

  • System.EventArgs를 상속 받는 파생클래스를 만듦
  • 이벤트 발생시 전달할 정보를 멤버 변수로 만들고 각 변수에 대해 생성자를 둠.
  • 각 변수는 일반적으로 readonly 접근한정자나 속성의 get만을 허용한다.

 

표준 이벤트를 직접 수동으로 작성하는 방법

  1. void를 리턴 타입으로 한다.
  2. 필요하다면 System.EventArgs를 상속 받는 파생클래스를 제네릭으로 받는다.
  3. EventHandler로 이름이 끝나야 한다.
  4. 이벤트를 발생시키는 함수는 protected virtual 함수이고 이름은 On으로 시작해야한다.

 

이전 포스트의 코드를 이벤트 핸들러를 이용한 표준 이벤트 패턴으로 바꾸고 테스트를 추가하면 아래와 같다.

using System;

// 표준 이벤트 NumberChanged의 매개변수 클래스
public class NumberChangedEventArgs : EventArgs
{
    // EventArgs 클래스를 상속 받아 전달할 정보를 멤버 변수로 만든다. 
    // Generic을 이용할 수도 있다.
    // 일반적으로 속성이나 readonly로 만든다. 
    public readonly int OldNum;
    public int NewNum { get; }  
    // tip: 생성자 단축키 ctor → 탭 
    
    public NumberChangedEventArgs(int oldNum, int newNum)
    {
        OldNum = oldNum;
        NewNum = newNum;
    }
}

public class ReactiveNum
{
    private int _value;
    
    public ReactiveNum(int value)
    {
    	_value = value
    }
    
    // System에 미리 정의된 표준 이벤트 형식
    public event EventHandler NumberChanged;
    
    protected virtual void OnNumberChanged(EventArgs e)
    {
    	//if (NumberChanged != null) NumberChanged(this, e); 와 같은 구문이다. 
        NumberChanged?.Invoke(this, e);
        
        /* 다중 스레드에서는 임시변수에 배정 후 점검, 호출해야 스레드 안정성 오류를 피할 수 있음. 
            var temp = NumberChanged; 
            temp?.Invoke (this, e); 
        */
        
    }
    
    public int Value
    {
        get => _value;
        set
        {
            if (_value == value) return;
            
            //EventArgs 파생클래스를 생성해서 정보 전달. 
            OnNumberChanged(new NumberChangedEventArgs(_value, value))
            _value = value;
        }
    }
}

class Test
{
    static void Main()
    {
        ReactiveNum num = new ReactiveNum(1);
        
        // 표준 이벤트 NumberChanged에 구독. 숫자가 변할 때마다 구독한 함수가 실행됨.
        num.NumberChanged += NumIncreased2;
        num.Value = 3; 
        //'2만큼 증가했습니다.'가 출력됨.
    }

    static void NumIncreased2(object sender, CustomEventArgs e)
    {
        if (e.NewNum - e.OldNum == 2) Console.WriteLine("2만큼 증가했습니다.");
    }
}

추가 정보를 전달하지 않는 경우 비제네릭 EventHandler를 사용하면 된다.
위에서도 정보전달이 필요없는 함수라면 다음처럼 간결하게 짤 수 있다.

using System;
public class ReactiveNum
{
    int _value;
    
    public ReactiveNum(int value)
    {
    	_value = value
    }
    
    public event EventHandler NumberChanged;
    
    protected virtual void OnNumberChanged(EventArgs e)
    {
        NumberChanged?.Invoke(this, e);
    }
    
    public int Value
    {
        get => _value;
        set
        {
            if (_value == value) return;
            _value = value;

            //쓸데없는 EventArgs 인스턴스를 생성하지 않으려고 Empty 속성을 사용함. 
            OnNumberChanged(EventArgs.Empty);
        }
    }
}
class Test
{
    static void Main()
    {
        ReactiveNum num = new ReactiveNum(1);
        num.NumberChanged += ConsoleWriteNumChanged;
        
        //1과 다른 숫자를 입력하면 '숫자가 변했습니다.' 가 출력된다. 
        num.Value = int.Parse(Console.ReadLine());
    }
    static void ConsoleWriteNumChanged(object sender, EventArgs e)
    {
        Console.WriteLine("숫자가 변했습니다.");
    }
}

참고문헌: C# 8.0 in a Nutshell, Joseph Albahari. Eric Johannsen. pp160-163.

'C#' 카테고리의 다른 글

C# 제네릭(Generics) 1  (0) 2020.11.30
C# 이벤트(Events) 접근자와 수정자  (0) 2020.11.27
C# 이벤트(Events)  (0) 2020.11.26
C# 람다식(Lambda Expressions)  (0) 2020.11.16
C# 확장 메서드(Extension Methods)  (0) 2020.11.15