주녘공부일지

[C#] 이벤트 (Event) + 표준 이벤트 패턴, 이벤트 접근자&수정자 본문

Programming/Definition, Etc

[C#] 이벤트 (Event) + 표준 이벤트 패턴, 이벤트 접근자&수정자

주녘 2023. 11. 15. 16:04

0. 이벤트 (Event)

방송자 : 대리자가 있는 필드 형식으로, 대리자를 호출해 정보를 방송한다는 의미

구독자 : 대리자가 호출할 대상 메서드를 등록하는 형식으로, '+=', '-=' 연산자를 호출해 해당 방송의 청취를 시작 or 중단함

// 대리자 정의
public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);

public class Stock
{
    string symbol;
    decimal price;

    public Stock(string symbol) { this.symbol = symbol; }

    // 이벤트 대리자
    public event PriceChangedHandler PriceChanged;

    public decimal Price
    {
        get { return price; }
        set
        {
            if (price == value) return; // 변한 값이 없다면 그냥 반환

            decimal oldPrice = price;
            price = value;

            // 호출 목록이 비어있지 않으면 이벤트 발동
            PriceChanged?.Invoke(oldPrice, price);
        }
    }
}

- 위 예제에서 event 키워드가 없어도 결과는 동일하지만, 그렇게 되면 구독자들이 서로 간섭할 수 있게 됨

 -> 간섭이란? PriceChanged를 직접 조작하여 구독자 배정, 해제하는 등

 -> 이벤트의 주된 목적은 구독자들이 서로 간섭하지 못하게 하는 것

1. 표준 이벤트 패턴

    using System;
    
    // 1번 Point ( 코드 하단 설명 참조 )
    public class PriceChangedEventArgs : EventArgs
    {
        public readonly decimal LastPrice;
        public readonly decimal NewPrice;

        public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
        {
            LastPrice = lastPrice;
            NewPrice = newPrice;
        }
    }

    public class Stock
    {
        string symbol;
        decimal price;

        public Stock(string symbol) { this.symbol = symbol; }
        
        // 3번 Point ( 코드 하단 설명 참조 )
        public event EventHandler<PriceChangedEventArgs> PriceChanged;
        
        // 2번 Point ( 코드 하단 설명 참조 )
        protected virtual void OnPriceChanged (PriceChangedEventArgs e)
        {
            PriceChanged?.Invoke (this, e);
        }
        
        public decimal Price
        {
            get { return price; }
            set
            {
                if (price == value)
                    return;

                decimal oldPrice = price;
                price = value;
                OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
            }
        }
    }

    class Test
    {
        static void Main()
        {
            Stock stock = new Stock("THPW");
            stock.Price = 27.10M;

            // PriceChanged 이벤트에 등록
            stock.PriceChanged += Stock_PriceChanged;
            stock.Price = 31.59M;
        }

        static void Stock_PriceChanged(object sender, PriceChangedEventArgs e)
        {
            if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M)
                Console.WriteLine("주의, 주가 10% 상승!");
        }
    }

1번) public class PriceChangedEventArgs : EventArgs

닷넷 프레임워크에 미리 정의되어 있는 System.EventArgs는 정적 Empty 속성 외에는 아무런 멤버도 없음

- EventArgs 파생 클래스는 재사용성을 위해, 담고 있는 정보를 반영하는 이름을 붙임

+ 일반적으로 자신의 자료를 읽기 전용  필드로 노출함

 

2번)  protected virtual void OnPriceChanged (PriceChangedEventArgs e)

- 표준 이벤트 패턴에 따르려면 이벤트를 알리는 보호 가상 메서드가 반드시 필요한데 이 메서드명은 반드시 "On"으로 시작해야 하며. EventArgs 형식의 인수 하나를 받아야 함

 

3번) public event EventHandler<PriceChangedEventArgs> PriceChanged;

이벤트를 위한 대리자로, 이는 지켜야하는 규칙이 3가지 있음

1) 반환 형식은 반드시 void 이여야 함

2) 인수 두 개를 받아야 함

 -> object ( 이벤트 방송자 지정 ), EventArgs 파생 클래스 ( 전달할 추가 정보를 담음 )

3) 이름은 반드시 "EventHandler"로 끝나야 함

 + 닷넷 프레임워크에는 위 조건들을 만족하는 System.EventHandler가 정의되어 있음 (제네릭)

public delegate void EventHandler<TEventArgs>
    (object source, TEventArgs e) where TEventArgs : EventArgs;

2. 이벤트 접근자

이벤트 접근자(accessor)는 이벤트에 대한 '+=', '-=' 연산을 그 이벤트에 맞는 방식으로 구현하기 위한 것으로,  기본적으로 컴파일러가 암묵적으로 이벤트 접근자들을 구현해줌

public class Broadcaster
{
    public event PriceChangedHandler PriceChanged;
}

위와 같은 예제의 선언된 이벤트에 대해 컴파일러는 내부적으로 아래와 같은 형태로 바꾸어 컴파일

// 전용 대리자 필드
private EventHandler priceChanged;

// 공용 이벤트 접근자 함수인 add, remove
public event EventHandler PriceChanged
{
    add    { priceChanged += value; }
    remove { priceChanged -= value; }
}

+ 컴파일러는 add, remove 블록들을 add_PriceChanged, remove_PriceChanged 메서드로 바꾸어서 컴파일

3. 명시적 이벤트 접근자

컴파일러의 기본 구현 대신 명시적 이벤트 접근자를 이용하면 바탕 대리자의 저장과 접근에 좀 더 복잡한 전략을 사용할 수 있음

- ex 1) 이벤트 접근자들이 다른 클래스에 이벤트 방송을 위임하기만 할 경우

- ex 2) 클래스가 너무 많은 수의 이벤트를 노출해 대리자 인스턴스를 딕셔너리에 저장하는 게 더 유리할 경우

 ( 널 대리자 필드 참조들을 수십 개씩 저장하는 것보다 딕셔너리의 저장소가 더 유리할 수 있음 )

 ex 3) 이벤트를 선언하는 인터페이스를 명시적으로 구현

public interface IFoo { event EventHandler Ev; }

class Foo : IFoo
{
    private EventHandler ev;
    
    // 이벤트 접근자 명시적 구현 ( ex 3 )
    event EventHandler IFoo.Ev
    {
        add    { ev += value; }
        remove { ev -= value; }
    }
}

4. 이벤트 수정자

메서드처럼 이벤트에도 여러 수정자를 적용해 가상, 재정의, 추상, 봉인 이벤트로 만들 수 있음 ( 정적 이벤트도 가능 )

public class Foo
{
    public static event EventHandler<EventArgs> StaticEvent;
    public virtual event EventHandler<EventArgs> VirtualEvent;
}

 

참고도서) C# 6.0 완벽가이드