ch13 대리자와 이벤트

2020. 1. 28. 17:30C# 언어/이것이 C# 이다. 책정리

반응형

 

 

챕터13  '대리자와 이벤트' 입니다

메소드를 참조하여 대신 수행해 주는 대리자와

특정 이벤트를 만드는 방법, 콜 백(call back)을 배웁니다.

 

'이것이 C#이다' 교재를 바탕으로 정리했습니다.

 

이전 정리글

더보기

ㅇ 대리자란?

 

간략한 비유로

출장을 나온 상현이는 사장님께 보고를 드립니다. 그런데 전화를 받은 사람은 비서였습니다.

상현이는 비서에게 "사장님이 돌아오면 제게 전화 부탁드려요" 라는 메모를 남기고 전화를 끊었습니다.

잠시 후, 사무실에 돌아온 사장님은 비서의 메모를 받고 상현이에게 전화를 걸어 통화를 했습니다.

 

이 처럼 상현이가 비서에게 했던 부탁을 영어로 "콜 백" 이라고 부르는데

대신 어떤 일을 해줄코드를 두고, 이 코드가 실행할 세부 코드는 컴파일 시점이 아닌 실행 시점에 부여하는 식입니다.

 

C#에서 대리자는 바로 이 콜백을 구현하기 위해 사용합니다.

대리자는 메소드에 대한 참조입니다.

대리자에 메소드의 주소를 할당한 후 대리자를 호출하면 이 대리자가 메소드를 호출해줍니다.

 

전체적인 대리자의 선언과 쓰임세 입니다.

using System;
using static System.Console;

namespace p429
{
    class Program
    {
        // 대리자 선언 (deleagte 키워드 사용)
        delegate int MyDelegate(int a, int b); 

        class Calculator
        {
            public int Plus(int a, int b)
            {
                return a + b;
            }
            public static int Minus(int a, int b)
            {
                return a - b;
            }
        }
        static void Main(string[] args)
        {
            Calculator Calc = new Calculator();
            MyDelegate Callback;
            
            // 대리자의 매개변수로 메소드를 넘겨줌
            Callback = new MyDelegate(Calc.Plus);
            WriteLine(Callback(3, 4));

            Callback = new MyDelegate(Calculator.Minus);
            WriteLine(Callback(4, 3));
        }
    }
}

 

"그냥 메소드를 호출하면 되지, 무엇 때문에 이런 난리굿을 피는 걸까?" 

 

프로그래밍을 하다 보면 "값"이 아닌 "코드" 자체를 매개 변수로 넘기고 싶을 때가 많습니다.

 

간단한 예로

배열을 정렬하는 메소드를 만든다고 생각해보세요.

오름차순으로 정렬하는지, 내림차순으로 정렬하는지, 아니면 특별한 계산식을 거쳐서 나오는지

 

이 메소드가 정렬을 수행할 때 사용하는 비교 루틴을 매개 변수로 넣을 수 있으면

이런 고민은 메소드를 사용하는 프로그래머가 알아서 하라고 하면 되는데 말입니다.

 

바로 이런 때 대리자가 사용됩니다.

대리자는 메소드에 대한 참조 이므로 비교 메소드를 참조할 대리자를 작성해두면 언제든 쓰기만 하면 됩니다.

 

배열을 정렬하는 코드입니다.

using System;
using static System.Console;

namespace p432
{
    // 대리자 선언
    delegate int Compare(int a, int b);
    
    class Program
    {
        // 오름차순 정렬
        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;
            int j = 0;
            int temp = 0;

            for (i = 0; i < DataSet.Length; i++)
            {
                for (j = 0; j < DataSet.Length - 1; j++)
                {
                    if (Comparer(DataSet[j], DataSet[j + 1]) > 0)
                    {
                        temp = DataSet[j + 1];
                        DataSet[j + 1] = DataSet[j];
                        DataSet[j] = temp;
                    }
                }
            }
        }
        
        static void Main(string[] args)
        {
            int[] array = { 7, 2, 7, 2, 11 };

            WriteLine("Sroting ascending...");
            // 매개 변수로 메소드를 넣을 수 있음.
            // 비교되는 오름차순,내림차순을 프로그래머가 정할 수 있다.
            BubbleSort(array, new Compare(AscendCompare));

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

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

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

            WriteLine();
        }
    }
}

 

ㅇ 일반화 대리자

 

대리자 또한 일반화 메소드도 참조할 수 있습니다. 

요렁과 사용법도한 메소드와 동일합니다.

 

일반화 대리자의 코드입니다.

using System;
using static System.Console;

namespace p436
{
    delegate int Compare<T>(T a, T b);
    class Program
    {
        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)
        {
            int i = 0;
            int j = 0;
            T temp;

            for (i = 0; i < DataSet.Length - 1; i++)
            {
                for (j = 0; j < DataSet.Length - 1; j++)
                {
                    if (Comparer(DataSet[j], DataSet[j + 1]) > 0)
                    {
                        temp = DataSet[j + 1];
                        DataSet[j + 1] = DataSet[j];
                        DataSet[j] = temp;
                    }

                }
            }
        }

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

            WriteLine("Sorting ascending...");
            BubbleSort<int>(array, new Compare<int>(AscendCompare));

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

            string[] array2 = { "abc", "def", "ghi", "jkl", "mno" };

            WriteLine("\nSorting descending...");
            BubbleSort<string>(array2, new Compare<string>(DescendCompare));

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

            WriteLine();
        }
    }
}

일반화를 사용하여 int형과 string형 두개의 형식에 사용가능합니다.

 

 

대리자를 참조하는 메소드를 구현하는 부분중에

        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;
        }

: IComparable<T>를 상속하는 이유를 설명드리겠습니다.

사실은 System.Int32(int), System.Double(double)을 비롯한 모든 수치 형식과

System.String(string)은 모두 IComparable을 상속해서 CompareTo() 메소드를 구현하고 있습니다.

 

CompareTo() 메소드는 매개 변수가 자신보다 크면 -1, 같으면 0, 작으면 1을 반환합니다.

 

그래서 AscendCompare() 메소드가 a.CompareTo(b)를 호출하면

우리가 원하는 대로 오름차순 정렬에 필요한 비교 결과를 얻을 수 있습니다.


 

ㅇ 대리자 체인

 

앞서 말한것 처럼 대리자는 메소드의 참조입니다. 이 대리자는 재미있는 속성이 있습니다.

그건 바로 대리자 하나가 여러 개의 메소드를 동시에 참조할 수 있는 것입니다.

 

대리자 체인은 여러 개의 콜백을 동시에 호출해야 할 때 유용합니다.

 

대리자 체인을 만드는 것은 += 연산자와

Delegate.Combine() 메소드를 이용할 수 도있습니다.

 

특정 대리자를 끊어내야 할 때도 -= 연산자와

Delegate.Remove() 메소드를 이용할 수 있습니다.

 

어떤 것을 사용할 지는 취향에 맞게 하면 됩니다.

using System;
using static System.Console;

namespace p440
{
    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)
        {
            WriteLine($"{name}.SomethingHappend : {message}");
        }
    }

    class Program
    {
        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.");

            WriteLine();

            // -= 연산자를 이용한 체인 끊기
            notifier.EventOccured -= listener2.SomethingHappend;
            notifier.EventOccured("Download comlete.");

            WriteLine();

            // +, = 연산자를 이용한 체인 만들기
            notifier.EventOccured = new Notify(listener2.SomethingHappend)
                                  + new Notify(listener3.SomethingHappend);
            notifier.EventOccured("Nuclear launch detected.");

            WriteLine();

            // Delegate.Combine() 메소드를 이용한 체인 만들기
            Notify notify1 = new Notify(listener1.SomethingHappend);
            Notify notify2 = new Notify(listener2.SomethingHappend);

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

            WriteLine();

            // Deleagte.Remove() 메소드를 이용한 체인 끊기
            notifier.EventOccured =
              (Notify)Delegate.Remove(notifier.EventOccured, notify2);
            notifier.EventOccured("RPG!");
        }
    }
}

 

ㅇ 익명 메소드

 

익명 메소드는 한정자, 반환할 값, 매개 변수가 없는 변수입니다.

이름이 없는 메소드이지요.

 

익명 메소드는 delegate 키워드를 이용하여 선언합니다.

당연한 이야기지만, 익명 메소드는 자신을 참조할 대리자의 형식과 동일한 형식으로 선언되어야 합니다.

 

익명 메소드를 이용한 대리자 코드입니다.

using System;
using static System.Console;

namespace p436
{
    /*delegate int Compare<T>(T a, T b);*/
    delegate int Compare(int a, int b);
    class Program
    {
        /*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(int[] DataSet, Compare Comparer)
        {
            int i = 0;
            int j = 0;
            int temp;

            for (i = 0; i < DataSet.Length - 1; i++)
            {
                for (j = 0; j < DataSet.Length - 1; j++)
                {
                    if (Comparer(DataSet[j], DataSet[j + 1]) > 0)
                    {
                        temp = DataSet[j + 1];
                        DataSet[j + 1] = DataSet[j];
                        DataSet[j] = temp;
                    }

                }
            }
        }

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

            WriteLine("Sorting ascending...");
            /*BubbleSort<int>(array, new Compare<int>(AscendCompare));*/
            
            // 익명 메소드를 이용한 부분 delegate 키워드를 사용했다.
            // 전에 있었던 배열 정리 코드와 비교해 보는 것도 좋다.
            BubbleSort(array, delegate (int a, int b)
            {
                if (a > b)
                    return 1;
                else if (a == b)
                    return 0;
                else
                    return -1;
            });

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

            int[] array2 = { 2, 3, 1, 6, 4 };

            WriteLine("\nSorting descending...");
            /*BubbleSort<string>(array2, new Compare<string>(DescendCompare));*/
            BubbleSort(array2, delegate (int a, int b)
            {
                if (a < b)
                    return 1;
                else if (a == b)
                    return 0;
                else
                    return -1;
            });

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

            WriteLine();
        }
    }
}

 

이벤트 : 객체에 일어난 사건 알리기

 

이벤트는 알람 시계처럼 특정 시간이 되었을 때 이를 알려주거나,

사용자가 버튼을 클릭했을 때 이를 알려주는 객체를 뜻합니다.

이런 객체를 만들 때 사용하는 것이 바로 이벤트입니다.

 

이벤트의 동작 원리는 대리자와 거의 비슷합니다. 

대리자를 event 한정자로 수식해서 만들기 때문입니다.

 

이벤트를 선언하고 사용하는 절차입니다.

  1. 대리자를 선언합니다. 이 대리자는 클래스 밖에 선언해도 되고 안에 선언해도 됩니다.
  2. 클래스 내에 1.에서 선언한 대리자의 인스턴스를 event 한정자로 수식해서 선언합니다.
  3. 이벤트 핸들러를 작성합니다. 이벤트 핸들러는 1. 에서 선언한 대리자와 일치하는 메소드면 됩니다.
  4. 클래스의 인스턴스를 생성하고 이 객체의 이벤트에 3. 에서 작성한 이벤트 핸들러를 등록합니다.
  5. 이벤트가 발생하면 이벤트 핸들러가 호출됩니다.
step 1.

대리자를 선언합니다. 이 대리자는 클래스 밖에 선언해도 되고 안에 선언해도 됩니다.

delegate void EventHandler(string message);

 

step 2.

클래스 내에 step 1. 에서 선언한 대리자의 인스턴스를 event 한정자로 수식해서 선언합니다.

class MyNotifier
    {
        // EventHandler는 step 1. 절차에서 선언한 대리자 입니다.
        public event EventHandler SomethingHappend;
        public void DoSomethig(int number)
        {
            int temp = number % 10;

            if (temp != 0 && temp % 3 == 0)
            {
                // number가 3,6,9로 끝나는 값이 될 때 이벤트 발생
                SomethingHappend(String.Format("{0} : 짝", number));
            }
        }
    }

 

step 3.

이벤트 핸들러를 작성합니다. 이벤트 핸들러는 step 1.에서 선언한 대리자와 일치하는 메소드면 됩니다.

class Program
{
        // SomethingHappend 이벤트에서 사용할 이벤트 핸들러는
        // EventHandler 대리자의 형식과 동일한 메소드여야 합니다.
        static public void MyHandler(string message)
        {
            WriteLine(message);
        }
        
        // ''''
}

 

step 4.

클래스의 인스턴스를 생성하고 이 객체의 이벤트에 step 3. 에서 작성한 이벤트 핸들러를 등록합니다.

class Program
    {
        static public void MyHandler(string message)
        {
            WriteLine(message);
        }
        static void Main(string[] args)
        {
            MyNotifier notifier = new MyNotifier();
            
            // SomethingHappend 이벤트에 MyHandler() 메소드를 이벤트 핸들러로 등록합니다.
            notifier.SomethingHappend += new EventHandler(MyHandler);

            for (int i = 0; i < 30; i++)
            {
                notifier.DoSomethig(i);
            }
        }
    }

 

step 5.

이벤트가 발생하면 이벤트 핸들러가 호출됩니다.

 

class Program
    {
        static public void MyHandler(string message)
        {
            WriteLine(message);
        }
        static void Main(string[] args)
        {
            MyNotifier notifier = new MyNotifier();          
            notifier.SomethingHappend += new EventHandler(MyHandler);

            // 이벤트 발생 부분
            for (int i = 0; i < 30; i++)
            {
                notifier.DoSomethig(i);
            }
        }
    }

 

그럼 이벤트와 대리자는 어떤 차이가 있는 걸까요?

 

이벤트가 대리자와 가장 크게 다른 점은 

이벤트는 외부에서 직접 사용할 수 없다는 데 있습니다.

이벤트는 public 한정자로 선언되어 있어도 자신이 선언되어 있는 클래스 외부에서는 호출이 불가능합니다.

 

반면에 대리자는 public이나 internal로 수식되어 있으면 클래스 외부에서라도 얼마든지 호출이 가능합니다.

 

따라서 대리자는 대리자대로 콜백 용도로 사용하고,

이벤트는 이벤트대로 객체의 상태 변화나 사건의 발생을 알리는 용도로 구분해서 사용해야 합니다.


반응형