개발/C#

C# 기초 - 이벤트와 델리게이트 (Event & Delegate)

huiyu 2021. 8. 24. 17:19

Delegates(델리게이트)

 - Delegate는 대리자라고도 하며, 메서드에 대한 참조를 갖는 형식이다.
 - 함수포인터나 콜백과 동일한 동작으로 delegate를 호출하면 참조하고 있는 메서드가 호출된다.
 - 참조하는 함수의 반환 형식 및 매개변수를 사용하여 선언한다.
  * 선언한 함수 형식이 일치하는 메서드에 대해서만 참조할 수 있다.

아래와 같은 형식으로 선언하여 사용

public delegate [반환형식] [이름] (매개변수)

[예제]

public delegate void ThresholdReachedEventHandler(object sender, ThresholdReachedEventArgs e);

-> 반환형이 void, 매개변수는 object, ThresholdReachesEventArgs를 갖는 delegate 선언.
  이름은 ThresholdReachedEventHandler. 위와 동일한 void형의 동일한 매개변수를 갖는 함수를 등록할 수 있다.

delegate 사용 예제 ->

namespace ConsoleApplication
{
    delegate int FuncDelegate(int a, int b);

    class Program
    {
        static int Plus(int a, int b)
        {
            return a + b;
        }
        
        static int Minus(int a, int b)
        {
            return a - b;
        }

        static void Main(string[] args)
        {
            FuncDelegate plusDelegate = Plus;
            FuncDelegate minusDelegate = Minus;

            Console.WriteLine(plusDelegate(5, 10));
            Console.WriteLine(minusDelegate(20, 10));
        }
    }
}

출력)
15
10

코드설명)
- Plus()와 Minus() 함수를 delegate 'FuncDelegate'에 연결하여, 각각 연결된 함수가 호출되고 있다.
- 선언한 delegate의 반환/매개변수는 참조하는 함수(Plus/Minus)와 일치한다.

또한, 하나의 delegate는 여러개의 함수를 등록하고 호출할 수 있는 '델리게이트 체인(Delegate Chain)'을 지원한다.

using System;

namespace ConsoleApp1
{
    delegate void FuncDelegate(string str);

    class Program
    {
        static void Func1(string str)
        {
            Console.WriteLine("Helo1 : " + str);
        }
        static void Func2(string str)
        {
            Console.WriteLine("Helo2 : " + str);
        }
        static void Func3(string str)
        {
            Console.WriteLine("Helo3 : " + str);
        }
        static void Func4(string str)
        {
            Console.WriteLine("Helo4 : " + str);
        }
        static void Main(string[] args)
        {
            FuncDelegate plusDelegate = null;
            plusDelegate += Func1;
            plusDelegate += Func2;
            plusDelegate += Func3;
            plusDelegate += Func4;

            plusDelegate("Text");
        }
    }
}

출력 )
Hello1 : Text
Hello2 : Text
Hello3 : Text
Hello4 : Text

코드설명)
 - 하나의 delegate 함수 'FuncDelegate'에 여러개의 함수(Func1~4)를 등록하고 있다.
 - delegate 한번의 호출로 등록된 모든 함수가 한번에 호출되고 있는 걸 볼 수 있다.
 - '+=' 연산자를 통해 함수를 등록, '-=' 연산자를 통해 제거할 수 있다.

* C#에선 이러한 delegate의 기능을 이용하여 event를 구현하고 사용할 수 있다.

MS Delegate Guide Link(클릭)

Events(이벤트)

 - 객체에 특정 작업의 실행을 알리는 메시지, 예를 들면 사용자와의 인터랙션(Interaction)과 같은 처리.
   ex) 버튼을 터치했을 경우나 속성값 변경 등의 사건.

 - 이벤트는 일반적으로 delegate model을 기반으로 하며, 이는 관찰자 디자인 패턴(Observer Design Pattern)을 따른다.
  * Observer Design Pattern : 구독자(Subscriber)가 공급자(Provider)를 등록하고 공급자(Provider)로부터 알림을 수신하는 데 사용

- 이벤트를 발생시키는 개체(Object)를 'event sender'라고 하며, event sender는 어떤 개체(Object)나 함수(Method)를 수신하거나 처리할 지 모른다. 일반적으로 이벤트는 event sender의 멤버이다.
 (ex, Click 이벤트는 Button의 클래스 멤버, PropertyChanged 이벤트는 INotifyPropertyChanged의 클래스 멤버)

[예제]

class Counter
{
    public event EventHandler ThresholdReached;

    protected virtual void OnThresholdReached(EventArgs e)
    {
        EventHandler handler = ThresholdReached;
        handler?.Invoke(this, e);
    }
    // provide remaining implementation for the class
}

 - 일반적으로 이벤트를 발생 시키기 위해 'protected' 및 'virtual' 이라고 표시된 메서드를 추가,  함수에서 EventHandler를 호출하고 있다.
   -> On(EventName) 형태, 위 예제에선 OnThresholdReached 함수
 - 메서드에서 EventArgs 형식이나 파생형식의 이벤트 데이터를 지정하는 매개변수 사용하여 선언, 이벤트 발생시 필요한 데이터를 주고받을 수 있다.(데이터가 필요없는 경우 EventArgs를 사용, EventArgs는 모든 이벤트 클래스의 기본 형식이다.)
 * 여기서 EventHandler는 Delegate이다. Delegate임으로 여기에 연결된 함수들은 호출(Invoke)와 동시에 호출되게 된다.

 - MS Event 개요 Link(클릭)

EventHandler/EventHandler<TEventArgs> 대리자

EventHandler

public delegate void EventHandler(object? sender, EventArgs e);

EventHandler<TEventArgs>

public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);

 .Net에서 제공하고 있는 Event 처리 관련 delegate, 이 두가지를 이용하여 대부분의 이벤트 시나리오를 처리할 수 있다. EventHandler의 경우엔 데이터가 없는 이벤트 처리, EventHandler<TEventArgs>는 데이터를 포함하는 이벤트에 대해서 처리할 수 있다.
 이 두 대리자의 경우엔 반환값이 없으며, 두개의 매개변수(이벤트가 발생한 개체, 이벤트 데이터 개체)를 갖는 형태로 선언하여 사용한다.

예제 1 - EventHandler, 데이터가 없는 이벤트

using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Counter c = new Counter(new Random().Next(10));
            c.ThresholdReached += c_ThresholdReached;

            Console.WriteLine("press 'a' key to increase total");
            while (Console.ReadKey(true).KeyChar == 'a')
            {
                Console.WriteLine("adding one");
                c.Add(1);
            }
        }

        static void c_ThresholdReached(object sender, EventArgs e)
        {
            Console.WriteLine("The threshold was reached.");
            Environment.Exit(0);
        }
    }

    class Counter
    {
        private int threshold;
        private int total;

        public Counter(int passedThreshold)
        {
            threshold = passedThreshold;
        }

        public void Add(int x)
        {
            total += x;
            if (total >= threshold)
            {
                ThresholdReached?.Invoke(this, EventArgs.Empty);
            }
        }

        public event EventHandler ThresholdReached;
    }
}

 - 예제는 숫자를 더하는 Counter 클래스에 ThreshHold 값을 설정한 후, while을 통해 값을 계속 더해준다. 이후 ThreashHold값에 더하던 값이 도달했을 경우 ThreshholdReached라는 Event를 호출하며 사용자가 원하는 동작을 구현한 콜백함수(c_ThresholdReached)가 호출되며 앱을 종료한다. 

ThresholdReached?.Invoke(this, EventArgs.Empty);

 - Invoke()를 통해 delegate함수인 EventHandler를 호출하며, 연결된 콜백을 호출한다. 이 때 첫번째 인자는 이벤트가 발생한 개체(본인, this)이며, 두번째는 전달할 데이터가 없으므로 EventArgs.Empty를 전달한다.

예제2 - EventHandler<TEventArgs>, 데이터가 있는 경우

using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Counter c = new Counter(new Random().Next(10));
            c.ThresholdReached += c_ThresholdReached;

            Console.WriteLine("press 'a' key to increase total");
            while (Console.ReadKey(true).KeyChar == 'a')
            {
                Console.WriteLine("adding one");
                c.Add(1);
            }
        }

        static void c_ThresholdReached(object sender, ThresholdReachedEventArgs e)
        {
            Console.WriteLine("The threshold of {0} was reached at {1}.", e.Threshold,  e.TimeReached);
            Environment.Exit(0);
        }
    }

    class Counter
    {
        private int threshold;
        private int total;

        public Counter(int passedThreshold)
        {
            threshold = passedThreshold;
        }

        public void Add(int x)
        {
            total += x;
            if (total >= threshold)
            {
                ThresholdReachedEventArgs args = new ThresholdReachedEventArgs();
                args.Threshold = threshold;
                args.TimeReached = DateTime.Now;
                OnThresholdReached(args);
            }
        }

        protected virtual void OnThresholdReached(ThresholdReachedEventArgs e)
        {
            EventHandler<ThresholdReachedEventArgs> handler = ThresholdReached;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        public event EventHandler<ThresholdReachedEventArgs> ThresholdReached;
    }

    public class ThresholdReachedEventArgs : EventArgs
    {
        public int Threshold { get; set; }
        public DateTime TimeReached { get; set; }
    }
}

* 사용자 지정된 이벤트 데이터 클래스를 만들려면 EventArgs로부터 파생된 클래스를 만든 후, 이벤트 관련 데이터를 전달하는 데 필요한 멤버를 제공합니다. 일반적으로 .NET과 동일한 명명 패턴을 사용하여 이벤트 데이터 클래스 이름을 EventArgs로 끝내야 합니다.

- 위 예제에선 EventArgs를 상속받은 ThresholdReachedEventArgs를 구현하고, 전달할 데이터 'Threshold, TimeReached'를 선언하고 있다.

public event EventHandler<ThresholdReachedEventArgs> ThresholdReached;

- 선언한 ThresholdReachedEventArgs를 사용하여 EventHandler를 선언하였으며, 전달이 필요한 데이터 값은 Add()함수에서 채워져 전달하고 있다.
- 이전 예제와 마찬가지로 Threshold값에 도달했을 경우 event를 호출하며, threashold값과 time값을 등록한 콜백에 같이 알려주는 예제.

예제3 - 직접 EventHandler를 구현하여 사용하는 경우

using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Counter c = new Counter(new Random().Next(10));
            c.ThresholdReached += c_ThresholdReached;

            Console.WriteLine("press 'a' key to increase total");
            while (Console.ReadKey(true).KeyChar == 'a')
            {
                Console.WriteLine("adding one");
                c.Add(1);
            }
        }

        static void c_ThresholdReached(Object sender, ThresholdReachedEventArgs e)
        {
            Console.WriteLine("The threshold of {0} was reached at {1}.", e.Threshold, e.TimeReached);
            Environment.Exit(0);
        }
    }

    class Counter
    {
        private int threshold;
        private int total;

        public Counter(int passedThreshold)
        {
            threshold = passedThreshold;
        }

        public void Add(int x)
        {
            total += x;
            if (total >= threshold)
            {
                ThresholdReachedEventArgs args = new ThresholdReachedEventArgs();
                args.Threshold = threshold;
                args.TimeReached = DateTime.Now;
                OnThresholdReached(args);
            }
        }

        protected virtual void OnThresholdReached(ThresholdReachedEventArgs e)
        {
            ThresholdReachedEventHandler handler = ThresholdReached;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        public event ThresholdReachedEventHandler ThresholdReached;
    }

    public class ThresholdReachedEventArgs : EventArgs
    {
        public int Threshold { get; set; }
        public DateTime TimeReached { get; set; }
    }

    public delegate void ThresholdReachedEventHandler(Object sender, ThresholdReachedEventArgs e);
}

- delegate를 이용하여 ThresholdReachedEventHandler를 직접 선언하여 사용한 예제, 일반적으론 EventHandler, EventHandler<TEventArgs> 형태로 모든 이벤트 시나리오에서 처리가능하기 때문에 직접 선언하여 사용할 필요는 많이 없다. 특별히 이 두가지를 사용할 수 없는 경우에만 직접 선언하여 사용한다.

*예제 코드 링크 : MS 이벤트 발생 및 사용

 

How to: Raise and Consume Events

Raise & consume events in .NET. See examples that use the EventHandler delegate, the EventHandler delegate, & a custom delegate.

docs.microsoft.com

* EventHandler : https://docs.microsoft.com/en-us/dotnet/api/system.eventhandler?view=net-5.0 

 

EventHandler Delegate (System)

Represents the method that will handle an event that has no event data.

docs.microsoft.com

*EventHandler<TEventArgs> : https://docs.microsoft.com/en-us/dotnet/api/system.eventhandler-1?view=net-5.0 

 

EventHandler Delegate (System)

Represents the method that will handle an event when the event provides data.

docs.microsoft.com

 

Framework Design Guideline - Event

-  이벤트는 일반적으로 사용하는 콜백(사용자 코드를 호출할 수 있도록 구현) 구분. 다른 콜백 메커니즘으로 대리자를 사용하는 멤버, 가상멤버, 인터페이스 기반 플러그인이 있으나 대부분 개발자가 event를 사용할 때 보다 편안함을 느낀다고 한다.

- 이벤트에는 상태 변경 이전에 발생한 이벤트(사전이벤트) 와 상태 변경 후 발생한 이벤트(사후 이벤트) 두 그룹이 존재
  (ex) 사전 이벤트 - Form.Closing, 사후 이벤트 - Form.Closed

- 이벤트에는 'fire' 또는 'trigger'가 아닌 'raise'라는 용어 사용

- 이벤트 처리기로 사용할 새 대리자(delegate)를 수동으로 만들기보단 System.EventHandler<TEventArgs>를 사용하기

- CONSIDER using a subclass of EventArgs as the event argument, unless you are absolutely sure the event will never need to carry any data to the event handling method, in which case you can use the EventArgs type directly.
 -> data 전달이 확실하게 필요가 없을 경우에만 EventArgs를 사용할 것! EventArgs를 사용하여 API를 제공할 경우 호환성을 손상하지 않고 이벤트와 함께 데이터를 추가할 수 없다. EventArgs를 상속받은 subclass를 사용한다면, 처음엔 비어있을 수 있지만 나중에 속성을 추가할 수 있다!

- 각 이벤트를 발생시키려면 보호된 가상 메서드를 사용하자. (DO use a protected virtual method to raise each event.)
  (구조체, sealed class, static event 제외)
 => On_형태의 이름으로 재정의하는 함수 제공, 여기서 이벤트 호출.

- 이벤트를 발생시키는 protected method에는 하나의 매개 변수를 사용, 매개 변수는 'e'로 이름을 지정하고 이벤트 인수 클래스로 형식화 해야한다. (The parameter should be named e and should be typed as the event argument class.)

- 비정적(non-static) 이벤트를 발생시킬 때 sender를 null로 보내지 말 것.
- 정적(static) 이벤트의 경우엔 sender를 null로 전달.

- 이벤트 발생 시 이벤트 데이터 매개 변수로 null을 전달하지 말 것. 데이터가 없을 경우 EventArgs.Empty를 전달. 개발자는 이 매개 변수로 판단.

- 최종 사용자가 취소할 수 있는 이벤트를 발생시키는 것이 좋다.(사전 이벤트의 경우에만), 이벤트를 취소하게 하려면 CancelEventArgs 또는 서브클래스를 이벤트 인수로 사용(CancelEventArgs)

Custom Event Handler Design

 - 이벤트 처리기엔 void 반환 형식을 사용, 이벤트 처리기는 여러 개체에서 여러 이벤트 처리 메서드를 호출할 수 있다. 여러 메서드에서 여러 반환이 있을 수 있다.

 - 첫번째 매개 변수 형식은 object로 하며, 'sender'라고 한다.

 -  System.EventArgs 또는 이를 상속받아 구현하고 이를 두번째 매개변수로 하고 이름을 e로한다.

 - 이벤트 처리기엔 세 개 이상의 매개변수를 사용하지 않는다.

C# Design Guideline - Event(링크)

728x90
반응형