Skip to main content

[This is C#] Chapter 13. 대리자와 이벤트

이것이 C#이다 책을 읽고 공부한 노트입니다.




대리자 #

  • 대리자(Delegate)
    • 메소드에 대한 참조이다.
// 델리게이트 형식(Type)
delegate int MyDelegate(int a, int b);

class Calc
{
    public int Add(int x, int y)
    {
        return x + y;
    }

    public int Minus(int x, int y)
    {
        return x - y;
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        Calc calc = new Calc();

        // 델리게이트 인스턴스 생성 및 등록
        MyDelegate callback = new MyDelegate(calc.Add);
        // 델리게이트 호출
        Console.WriteLine($"{callback(3, 4)}"); // 7

        callback = new MyDelegate(calc.Minus);
        Console.WriteLine($"{callback(5, 4)}"); // 1
    }
}



대리자는 왜, 언제 사용할까? #

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[] data, Compare comparer)
    {
        int temp = 0;

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

    static void Main(string[] args)
    {
        int[] arr = {8, 7, 3, 5, 1};
        BubbleSort(arr, new Compare(AscendCompare));

        for (int i = 0; i < arr.Length; i++)
            Console.Write($"{arr[i]} "); // 1 3 5 7 8
    }
}



일반화 대리자 #

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

class MainApp
{
    // System.Int32와 같은 모든 수치 형식과 System.String은 모두 IComparable을 상속해서 CompareTo()를 구현하고 있다. 
    // CompareTo()는 매개변수가 자신보다 크면 -1, 같으면 0, 작으면 1을 반환한다. 
    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[] data, Compare<T> comparer)
    {
        T temp;

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

    static void Main(string[] args)
    {
        int[] arr = {8, 7, 3, 5, 1};
        BubbleSort<int>(arr, new Compare<int>(AscendCompare));

        for (int i = 0; i < arr.Length; i++)
            Console.Write($"{arr[i]} "); // 1 3 5 7 8
    }
}



대리자 체인 #

  • 대리자 하나가 여러 개의 메소드를 동시에 참조할 수 있다.
delegate void Notify(string mesg);

class MainApp
{
    static void Call119(string location)
    {
        Console.WriteLine("Call119");
    }

    static void ShotOut(string location)
    {
        Console.WriteLine("ShotOut");
    }

    static void Escape(string location)
    {
        Console.WriteLine("Escape");
    }

    static void Main(string[] args)
    {
        // 델리게이트 체인 (추가 방법1)
        Notify notify1 = new Notify(Call119);
        notify1 += ShotOut;
        notify1 += Escape;

        // (추가 방법2)
        Notify notify2 = new Notify(Call119) + new Notify(ShotOut) + new Notify(Escape);

        // (추가 방법3)
        Notify notify3 = (Notify) Delegate.Combine(new Notify(Call119), new Notify(ShotOut), new Notify(Escape));


        // (삭제 방법1)
        notify1 -= Call119; // ShotOut Escape

        // (삭제 방법2) notify3에서 notify2에 있는 것을 뺀다. 
        notify3 = (Notify) Delegate.Remove(notify3, notify1); // Call119
    }
}



익명 메소드 #

  • 익명 메소드(Anonymous Method)
    • 이름이 없는 메소드이다.
    • 메소드가 델리게이트에 의해 참조 된 후로 사용할 일이 다시 없다고 한다면 익명 메소드로 선언하면 된다.
delegate int Calculate(int a, int b);

class MainApp
{
    static void Main(string[] args)
    {
        // 익명 메소드
        Calculate calc = delegate(int a, int b)
        {
            return a + b;
        };

        calc(1, 3); // 4
    }
}



이벤트 #

  • 이벤트

    • 델리게이트에 event 키워드를 붙여서 사용한다.
  • 이벤트와 델리게이트의 차이점

    • 이벤트는 public 한정자로 선언돼 있어도 자신이 선언된 클래스 외부에서는 호출이 불가능하다.
// 델리게이트 형식
delegate void Notify(string mesg);

class MyClass
{
    // 델리게이트 인스턴스 생성 + event 한정자 
    public event Notify OnSomethingHappened;

    public void DoSometing(int number)
    {
        // 이벤트 호출
        if (number % 2 == 0) 
            OnSomethingHappened(String.Format("{0} : 짝", number));
    }
}

class MainApp
{
    static public void PrintMesg(string mesg)
    {
        Console.WriteLine(mesg);
    }

    static void Main(string[] args)
    {
        MyClass myClass = new MyClass();

        // 이벤트 등록
        myClass.OnSomethingHappened += new Notify(PrintMesg);

        for (int i = 1; i < 30; i++)
        {
            myClass.DoSometing(i);
        }
        
        // event 한정자를 붙이면 클래스 밖에서 호출이 안 된다. 
        myClass.OnSomethingHappened("클래스 밖에서 호출해보기"); // 컴파일 에러!
    }
}



미리 정의된 대리자들 #

  • 델리게이트는 형식을 만들고, 인스턴스를 또 생성하여서 사용해야한다.
  • 모든 타입의 형태로 미리 정의된 델리게이트를 간단하게 쓸 수 있는 기능이 바로 ActionFunc 이다. 이것들은 별도로 미리 정의하지 않아도 한 줄로 표현 가능하다.
// delegate의 경우 미리 정의해야 한다. 
delegate void MyDelegate(string mesg);

public class MyClass
{
    static void Main(string[] args)
    {
        // 그리고 인스턴스를 만들어서 사용한다. 
        // C# 2.0에서는 new 없이 간단하게 인스턴스를 만들 수 있다. 
        MyDelegate myDelegate = ShowMessage;

        myDelegate("Hello");
    }

    static void ShowMessage(string mesg)
    {
        Console.WriteLine(mesg);
    }
}
public class MyClass
{
    static void Main(string[] args)
    {
        // Action의 경우 바로 쓸 수 있다. 
        Action<string> myAction = ShowMessage;

        myAction("Hello");
    }

    static void ShowMessage(string mesg)
    {
        Console.WriteLine(mesg);
    }
}



Action 대리자 #

  • 반환값이 없는 대리자이다.
  • 매개변수를 16개까지 넣을 수 있다.
  • Action, Action<T>, Action<T1, T2>



Func 대리자 #

  • 반환값이 1개 있는 대리자이다.
  • 매개변수를 16개까지 넣을 수 있다.
  • Func<TResult>, Func<T, TResult>, Func<T1, T2, TResult>
public class MyClass
{
    static void Main(string[] args)
    {
        Func<int, int, int> myAction = Add;

        Console.WriteLine(myAction(1, 2));
    }

    static int Add(int a, int b)
    {
        return a + b;
    }
}



EventHandler 대리자 #

  • 형식이 지정된 대리자이다.
    • 반환값은 없다.
    • 첫 번째 매개변수는 object 이며 이벤트를 발생시키는 인스턴스를 참조한다.
    • 두 번째 매개변수는 EventArgs 에서 파생된 것이며 이벤트 데이터를 보유한다.

  • EventHandler
    • 이벤트 데이터가 없는 이벤트를 처리할 때
public class Publisher
{
    // 이벤트 데이터가 없는 이벤트를 처리할 때 
    public event EventHandler MyEventHandler; 
 
    public void DoSometing()
    {
        MyEventHandler(this, EventArgs.Empty);
    }
}

public class Subscriber
{
    public void HanldeEvent(object sender, EventArgs e)
    {
        Console.WriteLine("Something happened to " + sender.ToString());
        Console.ReadLine();
    }
}

public class MainApp
{
    static void Main(string[] args)
    {
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();
        publisher.MyEventHandler += subscriber.HanldeEvent;
 
        publisher.DoSometing();
    }
}

  • EventHandler<EventArgs>
    • 이벤트 데이터가 있는 이벤트를 처리할 때
// 이벤트 데이터
public class MyEventArgs : EventArgs
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Publisher
{
    // 이벤트 데이터가 있는 이벤트를 처리할 때 
    public event EventHandler<MyEventArgs> MyEventHandler;
 
    public void DoSometing()
    {
        MyEventArgs myArgs = new MyEventArgs();
        myArgs.Id = 1;
        myArgs.Name = "Kim";
        
        MyEventHandler(this, myArgs);
    }
}

public class Subscriber
{
    public void HanldeEvent(object sender, MyEventArgs e)
    {
        Console.WriteLine("Something happened to " + sender.ToString());
        Console.WriteLine("Name: " + e.Name + ", Id: " + e.Id);
        Console.ReadLine();
    }
}

public class MainApp
{
    static void Main(string[] args)
    {
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();
        publisher.MyEventHandler += subscriber.HanldeEvent;
 
        publisher.DoSometing();
    }
}



대리자를 호출하는 Invoke() 메서드 #

  • 지금까지 우리는 단순히 대리자를 일반 메서드처럼 호출했지만, 본래는 Invoke() 메서드를 호출하는 것이다.
Action myAction = MyMethod;

myAction.Invoke();   // Invoke() 메서드 사용
myAction();          // 생략 가능



이벤트 구독할 때 가비지를 발생시키지 않는 방법 #

private event Action cashedAction;
private event Action myAction;

void Awake()
{
    // 미리 모두 캐시해 놓고 나중에 추가한다. 
    cashedAction = MyMethod;  // new Action(MyMethod)이므로 가비지발생
}

void Start()
{
    myAction += cashedAction; // 여기서는 가비지가 발생하지 않게된다. 
}

void MyMethod()
{
    //...
}



중복으로 이벤트를 구독 할 위험을 줄여주는 방법 #

  • event 키워드가 있는 경우 add{}remove{} 접근자를 정의할 수 있다.
  • add{} 에서 이벤트를 구독할 때, 한번 뺀 후 추가함으로써 중복으로 구독할 위험을 줄여준다.
private event Action myAction
{
    add
    {
        myAction -= value; // 중복으로 넣을 위험을 줄여준다. 
        myAction += value;
    }
    remove
    {
        myAction -= value;
    }
}