Delegate, Event

Ricon·2025년 1월 20일
0

기초 이론

목록 보기
3/6

Delegate


Delegate란

예를 들어 a가 b에게 전화했는데 b가 아닌 c가 전화를 받았다. a는 c에게 b가 돌아오면 전화를 해달라고 메로를 남겼고, 잠시후 b는 c에게 메모를 받고 a에게 전화를 걸었다.

이때, a가 c에게 했던 부탁 (전화를 해달라는 메모)는 영어로 Callback이라고 한다.
이러한 콜백은 프로그래밍에서도 사용되며 콜백을 작성하고 다른 코드에 이 콜백을 맡겨 대신 실행하게 된다.
여기에서 콜백을 맡아줄 코드는 컴파일 시점이 아닌 런타임에 결정된다.

Delegate는 메소드에 대한 참조이다. 대리자에 메소드의 주소를 할당한 후 대리자를 호출하면 이 대리자가 메소드를 호출한다.

한정자 delegate ReturnType DelegateName(Parameter);
delegate int MyDelegate(int a, int b);

Delegate는 Instance가 아닌 Type이다.
다시 말해 MyDelegate는 int, string과 같은 형식이며 '메소드를 참조하는 그 무엇'을 만드려면 MyDelegate의 인스턴스를 따로 만들어야 한다.

Delegate가 참조할 메소드는 Return Type과 Parameter가 Delegate와 같아야한다.

int Plus (int a, int b)
{
	return a + b;
}

int Minus(int a, int b)
{
	return a - b;
}

MyDelegate Callback;

Callback = new MyDelegate(Plus);
Console.WriteLine(Callback(3, 4));

Callback = new MyDelegate(Minus);
Console.WriteLine(Callback(5, 3));

돌고 돌아 메모리 구조

Delegate가 메소드를 참조하는 것을 알았다. 그럼 Delegate, Delegate 변수, 메소드는 각각 어디에 할당될까.

  1. 메소드는 코드 자체이므로 프로그램 실행시 Code Segment에 저장된다
    • 코드 영역은 프로그램 실행시(프로그램 파일이 로드될 때) 프로그램이 시작되기 전에 운영 체제에 의해 메모리에 저장된다
  2. Delegate는 메소드를 참조하는 객체이다. Delegate가 참조하는 메소드와 Delegate Instance는 각각 다른 메모리 영역에 저장된다.
    • Delegate 객체는 Heap에 저장된다 -> Delegate는 객체로 동작하기 때문에 이를 생성하면 Instance 정보는 Heap에 저장된다.
    • Delegate 변수가 지역 변수, 매개 변수일 경우 -> Stack에 Delegate 변수 할당
    • Delegate 변수가 전역 변수, Static 일 경우 -> Data에 Delegate 변수 할당
class Program
{
    delegate void MyDelegate(); 

    static void Main()
    {
        MyDelegate del = new MyDelegate(SayHello); // 델리게이트 생성
        // MyDelegate는 New를 사용하여 객체를 생성하므로 동적 할당임 -> Heap 할당
        // del은 Delegate 변수로 Main의 지역 변수이므로 -> Stack 할당
        del(); // 호출
    }

    static void SayHello() // 메소드이므로 Code 할당
    {
        Console.WriteLine("Hello, World!");
    }
}

Delegate는 왜, 언제 사용하나

'코드' 자체를 매개변수로 넘기고 싶을 때가 있다.

예를 들어 배열을 정렬하는 메소드를 만든다고 가정할때, 오름차 순으로 정렬할지, 내림차 순으로 정렬할지 고민할 필요 없이, 비교 루틴을 매개변수에 넣을 수 있으면 메소드를 사용하는 프로그래머가 원하는 것을 선택할 수 있다

Delegate는 메소드에 대한 참조이므로 비교 메소드를 참조할 Delegate를 Parameter에 받을 수 있도록 정렬 메소드를 작성하면 된다.

using System;

namespace UsingCallback
{
    delegate int Compare(int a, int b);

    class MainApp
    {
        static int AscendCompare(int a, int b)
        {
            if (a > b)
                return 1;
            else if (a == b)
                return 0;
            else
                return -1;
        }

        static int DescendCompare(int a, int b)
        {
            if (a < b)
                return 1;
            else if (a == b)
                return 0;
            else
                return -1;
        }

        static void BubbleSort(int[] dataSet, Compare comparer)
        {
            int i = 0, j = 0, temp = 0;

            for (i = 0; i < dataSet.Length - 1; i++)
            {
                for (j = 0; j < dataSet.Length - (i + 1); j++)
                {
                    if (comparer(dataSet[j], dataSet[j + 1]) > 0)
					// Compareer가 어떤 메소드를 참조하고 있는가에 따라 정렬 결과가 달라진다.
                    {
                        temp = dataSet[j + 1];
                        dataSet[j + 1] = dataSet[j];
                        dataSet[j] = temp;
                    }
                }
            }
        }

        static void Main(string[] args)
        {
            int[] array = { 3, 7, 4, 2, 10 };

            Console.WriteLine("Ascending...");
            BubbleSort(array, new Compare(AscendCompare));
            for(int i = 0; i < array.Length; i++)
                Console.Write($"{array[i]} ");

            Console.WriteLine();
            int[] array2 = { 7, 2, 8, 10, 11 };
            Console.WriteLine("Descending...");
            BubbleSort(array2, new Compare(DescendCompare));

            for (int i = 0; i < array2.Length; i++)
                Console.Write($"{array2[i]} ");
            Console.WriteLine();
        }
    }
}

Generic Delegate

Delegate는 일반 메소드 뿐만 아니라 Generic 메소드도 참조할 수있다.

delegate int Compare<T>(T a, T b);

//Parameter 예
static void BubbleSort<T>(T[] DataSet, Compare<T> Comparer)

static int AscendCompare<T>(T a, T b) where T : IComparable<T>
{
	return a.CompareTo(b);
}

System.Int32(int), System.Double(double)을 비롯한 모든 수치 형식과 System.String(string)은 모두 IComparable을 상속해서 CompareTo()메소드를 구현하고 있으며, 해당 메소드는 매개변수가 자신보다 크면 -1, 같으면 0, 작으면 1을 반환한다.

using System;

namespace GenericDelegate
{
    delegate int Compare<T>(T a, T b);

    class MainApp
    {
        static int AscendCompare<T>(T a, T b) where T : IComparable<T>
        {
            return a.CompareTo(b);
        }

        static int DescendCompare<T>(T a, T b) where T : IComparable<T>
        {
            return a.CompareTo(b) * -1;
        }

        static void BubbleSort<T>(T[] dataSet, Compare<T> comparer)
        {
            for (int i = 0; i < dataSet.Length - 1; i++)
            {
                for (int j = 0; j < dataSet.Length - (i + 1); j++)
                {
                    if (comparer(dataSet[j], dataSet[j + 1]) > 0)
                    {
                        T temp = dataSet[j + 1];
                        dataSet[j + 1] = dataSet[j];
                        dataSet[j] = temp;
                    }
                }
            }
        }

        static void Main(string[] args)
        {
            int[] array = { 3, 7, 4, 2, 10 };
            Console.WriteLine("Ascending...");

            BubbleSort<int>(array, new Compare<int>(AscendCompare));

            foreach (var val in array)
                Console.Write($"{val} ");

            string[] array2 = { "abc", "def", "ghi", "jkl", "mno" };
            Console.WriteLine("\nDescending...");

            BubbleSort<string>(array2, new Compare<string>(DescendCompare));
            foreach (var val in array2)
                Console.Write($"{val} ");
        }
    }
}

Delegate Chain

Delegate는 메소드를 참조하는 것이다. 그리고 Delegate는 여러 메소드를 동시에 참조할 수 있다.
Delegate와 Delegate의 형식에 맞춘 메소드가 여러개 있을때, 선언한 메소드들을 += 연산자를 통해 결합할 수 있다.

delegate void ThereIsAFire(string location);

void Call119(string location)
{
	Console.WriteLine($"불이 났어요 {location}"); 
}
void ShotOut(string location)
{
	Console.WriteLine($"살려주세요 {location}"); 
}
void Escape(string location)
{
	Console.WriteLine($"도망가세요 {location}"); 
}

//+= 메소드 결합
ThereIsAFire Fire = new ThereIsAFire(Call119);
Fire += new ThereIsAFire(ShotOut);
Fire += new ThereIsAFire(Escape);

//Combine 메소드 결합
ThereIsAFire Fire = (ThereIsAFire) Delegate.Combine(
							new ThereIsAFire(Call119),
                            new ThereIsAFire(ShoutOut),
                            new ThereIsAFire(Escape));

이렇게 결합한 Delegate는 한번만 호출하면 자신이 참조하고 있는 Call119, ShotOut, Escape를 순차적으로 모두 호출한다

Fire("우리집");

또한 메소드의 결합을 끊을 때는 -= 연산자나 Delegate.Remove를 사용한다

using System;

namespace DelegateChains
{
    delegate void Notify(string message);

    class Notifier
    {
        public Notify EventOccured;
    }

    class EventListener
    {
        private string name;
        public EventListener(string name)
        {
            this.name = name;
        }

        public void SomethingHappend(string message)
        {
            Console.WriteLine($"{name}.SomethingHappend: {message}");
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            Notifier notifier = new Notifier();
            EventListener listener1 = new EventListener("Listener1");
            EventListener listener2 = new EventListener("Listener2");
            EventListener listener3 = new EventListener("Listener3");

			// +=
            notifier.EventOccured += listener1.SomethingHappend;
            notifier.EventOccured += listener2.SomethingHappend;
            notifier.EventOccured += listener3.SomethingHappend;
            notifier.EventOccured("You've got mail");

            Console.WriteLine();
			
            // -=
            notifier.EventOccured -= listener2.SomethingHappend;
            notifier.EventOccured("Download Complete");

            Console.WriteLine();

			// +, =
            notifier.EventOccured = new Notify(listener2.SomethingHappend)
                                  + new Notify(listener3.SomethingHappend);
            notifier.EventOccured("Nuclear launch detected");

            Console.WriteLine();
			
            Notify notify1 = new Notify(listener1.SomethingHappend);
            Notify notify2 = new Notify(listener2.SomethingHappend);

			// Delegate.Combine
            notifier.EventOccured = (Notify)Delegate.Combine(notify1, notify2);
            notifier.EventOccured("Fire!");

            Console.WriteLine();
			
            // Delegate.Remove
            notifier.EventOccured = (Notify)Delegate.Remove(notifier.EventOccured, notify2);
            notifier.EventOccured("RPG!!!");
        }
    }
}

익명 메소드

익명 메소드는 delegate 키워드를 사용하여 이름이 없는 메소드를 만들어 내는 것이다.

DelegateInstance = delegate (Parameter)
					{
                    	//Code
                    };

익명 메소드는 보통 delegate가 참조할 메소드를 넘겨줘야 하는데, 이 메소드가 한번만 사용될 때 익명 메소드로 작성한다.

using System;

namespace AnonymousMethos
{
    delegate int Compare(int a, int b);
    class MainApp
    {
        static void BubbleSort(int[] dataSet, Compare comparer)
        {
            for (int i = 0; i < dataSet.Length - 1; i++)
            {
                for (int j = 0; j < dataSet.Length - (i + 1); j++)
                {
                    if (comparer(dataSet[j], dataSet[j + 1]) > 0)
                    {
                        int temp = dataSet[j + 1];
                        dataSet[j + 1] = dataSet[j];
                        dataSet[j] = temp;
                    }
                }
            }
        }

        static void Main(string[] args)
        {
            int[] array = { 3, 7, 4, 2, 10 };

            Console.WriteLine("Ascending...");
            BubbleSort(array, delegate (int a, int b)
            {
                return a.CompareTo(b);
            });

            foreach(var item in array)
            {
                Console.Write($"{item} ");
            }

            int[] array2 = { 7, 2, 8, 10, 11 };
            Console.WriteLine("\nDescending...");
            BubbleSort(array2, delegate (int a, int b)
            {
                return a.CompareTo(b) * -1;
            });

            foreach(var item in array2)
            {
                Console.Write($"{item} ");
            }

            Console.WriteLine();
        }
    }
}

Event

Event란

프로그래밍에서 어떤 일이 생겼을때 이를 알려주는 객체가 필요한 경우가 있다.
이런 객체를 만들때 Event를 사용하며, 이벤트의 동작 원리는 Delegate와 거의 비슷하다.
Event는 Delegate를 event 한정자로 수식해서 만든다

event를 선언하고 사용하는 순서는 다음과 같다.

  1. Delegate를 선언한다. 이 Delegate는 클래스 밖, 안 아무데나 선언해도 된다.
  2. 클래스 내에 1에서 선언한 Delegate의 인스턴스를 event 한정자로 선언한다
  3. eventHandler를 작성한다.
  4. 클래스의 인스턴스를 생성하고 이 객체의 이벤트에 3에서 작성한 eventHandler를 등록한다
  5. event가 발생하면 eventHandler가 호출된다
1
delegate void EventHandler(string message);

2.
class MyNotifier
{
	public event EventHandler SomethingHappend; // EventHandler는 1에서 선언한 Delegate
    public void DoSomethine(int num)
    {
    	int tmp = num % 10;
        if(tmp != 0 && tmp % 3 == 0)
        	SomethingHappend("짞"); // num이 3, 6, 9일때 이벤트 발생
    }
}

3. 
class MainApp
{
	// SomthingHappend 이벤트에서 사용할 이벤트 랜들러는 delegate와 동일한 형식 이여야함
	static public void MyHandler(string message) 
    {
    	Console.WriteLine(message);
    }
    4. 
    static void Main(string[] args)
    {
    	MyNotifier notifier = new MyNotifier();
        notifier.SomethingHappened += new EventHandler(MyHandler); //이벤트 핸들러 등록
        
        5.
        for(int i = 1; i < 30; i++)
        	notifier.DoSomthing(i);
    }
}

Delegate와 Event

Event와 Delegate의 가장 큰 차이점은 이벤트는 외부에서 직접 사용할 수 없다는 것이다.
이벤트는 public 으로 선언되어 있어도 자신이 선언된 클래스 외부에서는 호출이 불가능하다.
delegate는 public이나 internal로 수식되어 있으면 클래스 외부에서라도 호출이 가능하다.

delegate와 달리 이벤트가 호출될 수 없다는 사실은 이벤트 기반 프로그래밍이 가능하게 한다.
예를 들어, 상태 변화에 대한 사건을 알리는 클래스를 작성했다고 가정하다.
이벤트를 객체 외부에서 임의로 호출할 수 있게 된다면 클래스의 객체가 감시하는 실제 상태와 상관없이 객체 외부에서 허위로 상태 변화 이벤트를 일으킬 수 있게 된다.
이것은 실제로 객체의 상태를 바꾸는 것보다 더 나쁘다 -> 객체의 상태를 허위로 나타낼 수 있어서

결론

Delegate는 Delegate대로 콜백 용도로 사용하고, Event는 Event대로 객체의 상태 변화나 사건의 발생을 알리는 용도로 구분해서 사용해야 한다.

0개의 댓글