IT_Programming/C#

위임(Delegate) 예제

JJun ™ 2007. 12. 23. 10:15

여러개의 메서드를 동시에 호출한 것처럼 처리하기위해 위임(Delegate)형을 사용한다.
위임형은 delegate 키워드를 이용하여 선언한다.
public delegate void 위임이름(인수);

간단한 형태만알아보기 위해서.... 소스일부생략

public delegate void MyDelegate();
class MyClass{public void MyMethod(int x, int y)}
class MClass

{
       static void Main()

       {
            MyClass mc = new MyClass();
            // MyClass클래스의 인스턴스 mc를 생성합니다.
            MyDelegate md = new MyDelegate(mc.MyMethod);
            /* mc.MyMethod메서드를 MyDelegate의 생성자에 인자 값으로 입력해서

                MyDelegate형 md를 생성합니다. */
            md(3,4);
           // 위임형 md에 3,4를 입력해서 호출합니다.
        }

}

다중 위임 및 제거를 위해서는 +=연산자와 -=연산자를 이용한다.
md += new MyDelegate(mc.MyMethod);
md -= new MyDelegate(mc.MyMethod);

ex1)
public delegate void Mydelegate(string message);

    class Program
    {
        //static Mydelegate md = null;
        static event Mydelegate md = null;
        static void Main(string[] args)
        {
            Program p = new Program();
            string message = "한국";
            //p.OnDisplayMessage(message);
            //p.OnDisplayMessage(message);
            //Mydelegate md = null;

                md += new Mydelegate(p.OnDisplayMessage);
                md += new Mydelegate(p.OnDisplayMessage);
                md += new Mydelegate(p.NewOnDisplayMessage);
                //md(message);
                p.CallDelegate(md, message);
                md -= new Mydelegate(p.OnDisplayMessage);
                md -= new Mydelegate(p.OnDisplayMessage);
                if (md != null)
                {
                    md(message);
                }

        }
        private void CallDelegate(Mydelegate md, string msg)
        {
            md(msg);
        }
        private void onDisplayMessage(string message)
        {
            Console.WriteLine(message);
        }
        private void NewOnDisplayMessage(string message)
        {
            Console.WriteLine(message+"메세지");
        }
    }

 

 


 

 

 

[Delegate를 사용하는 이유]

 

 출처: http://blog.naver.com/niceyss/130023715587 

 

Delegate에 대해 알아 보고자 합니다.

 

많은 분들이 궁금해 하시는 부분중의 하나가 바로 “왜 Delegate를 써야 하는가?”하는 부분입니다.

그럼 이제부터 그 이유를 차근차근 알아보도록 하겠습니다. 

Delegate는 C의 함수 포인터와 비슷한 역할을 합니다.

즉, 자기 자신이 실제로 하는 일은 없고, 단지 자기가 가리키고 있는 메서드를 호출하는

역할을 하는거죠. 아래의 코드는 한번 볼까요? 

int I = int.Parse(“3746”); 

일반적으로 대부분의 메서드들이 인자값을 받아 어떤 일처리를 하게 됩니다.

위의 코드에서 Parse()메서드는 string형의 인자를 받아 int형으로 바꾸어 주는 역할을

하겠죠?

 

그런데, 대부분의 메서드들이 인자를 받는 이유는 뭘까요?

인자(parameter)없이 그냥 간단하게 메서드가 알아서 처리하면 안될까요?

메서드들이 인자를 받는 이유는 처리해야 할 값을 runtime시에만 알 수 있기 때문이죠.

 

예를 들어 숫자를 정렬하는 메서드를 만들었다고 가정합니다.

그런데, 사용자가 숫자 몇에서 몇까지를 정렬하기를 원하는지 알 수가 없습니다.

따라서, 나중에 그 값을 입력 받도록 처리해야겠죠. 바로 이럴 때 인자를 쓰게 됩니다.

이해 되시죠? 

Delegate도 이와 같은 개념으로 생각하시면 됩니다. 단지 다른 점이 있다면,

value형인 인자(parameter) 대신에 메서드를 인자로 받는다는 것이죠.

그런데, 메서드를 인자로 받아야 할 상황들이 있을까요?

 

잠시 후, 예제를 보시면 그런 상황들이 있다는 것을 아실 겁니다. 

Delegate는 Thread, Events 등에 많이 사용됩니다. Thread와 Events는 나중에 따로 강의될 예정이기 때문에, 여기서는 다루지 않겠습니다. 하지만, Thread와 Events에 Delegate가 쓰인다는 것만 일단 기억해 두세요.



자…그럼 delegate의 사용형식을 알아볼까요?

Delegate 선언 문법은 다음과 같습니다. 

delegate ②void ③VoidOperation(④uint X); 

①은 delegate임을 선언하구요. ②는 리턴형이구요.

③은 delegate의 사용자 정의 이름이 되겠죠.

④는 입력받는 인자의 형 및 변수입니다.

 

사용형식은 일반 메서드를 선언하는 것과 아주 흡사합니다.

다른 점이 있다면 delegate라는 키워드를 먼저 쓰는 것과 구현부({ … })가 없다는 것이죠.

구현부가 없는 이유는 이미 설명 드린 것과 같이 delegate 자신은 아무것도 하지않고,

자신이 참조하고 있는 메서드만 호출해 주기 때문입니다. 

Delegate를 선언하는 것은 새로운 클래스를 생성하는 것과 같습니다. 형식은 데이터 타입처럼 쓰이지만, 실제로 System.Delegate 클래스에서 파생되는 새로운 객체를 생성합니다.



간단한 사용예제를 한번 보겠습니다. 

예제 1) 
private delegate string GetAString(); 

static void Main(string[] args) 
{ 
     int X = 30; 
     GetAString MyMethod = new GetAString(X.ToString); 
     Console.WriteLine(“문자열은 {0} 입니다.”, MyMethod()); 

}



Delegate를 사용할 때 주의할 점은 delegate가 참조하는 메서드의 인자 및 리턴형이

선언된 delegate와 같아야 한다는 것입니다.

 

예제 1에서도 볼 수 있듯이, ToString()메서드는 인자가 없고, string형을 리턴합니다.

따라서 선언된 delegate도 인자가 없이 string형을 리턴하도록 선언되어 있습니다.

하지만, delegate는 참조하는 메서드가 어떤 타입이든간에 상관하지 않습니다.

 

static 이든 메서드 객체이든 간에 말이죠.

즉, 참조하는 메서드의 인자형, 개수, 리턴 타입만 같으면 문제가 없다는 말입니다. 

Delegate는 배열로도 선언되어 사용될 수 있습니다. 아래의 예제를 보죠. 

예제 2) 
class MathOperations 
{ 
     public static double MultiplyByTwo(double value) 
     { 
          return value*2; 
     } 

     public static double Square(double value) 
     { 
          return value*value; 
     } 
} 

 

delegate double DoubleOp(double x); 

class MainEntryPoint 
{ 
     static void Main(string[] args) 
     { 
          ①DoubleOp [] operations = 
          { 
               new DoubleOp(MathOperations.MultiplyByTwo), 
               new DoubleOp(MathOperations.Square) 
          }; 

          for (int i=0; i<operations.Length; i++) 
          { 
               Console.WriteLine(“operations[{0}] 호출:”, i); 
               ②CallDelegate(operations[i], 2.0); 
               CallDelegate(operations[i], 7.75); 
               Console.WriteLine(); 
          } 
     } 

     static void CallDelegate(③DoubleOp action, double value) 
     { 
          double result = ④action(value); 
          Console.WriteLine(“입력된 값은 {0}이고, 결과는 {1}입니다.”, value, result); 
     } 
}



①에서 delegate를 배열로 선언한 것을 볼 수 있는데, delegate도 일반 클래스들처럼

생성된 객체라는 사실을 안다면, 배열을 선언하는 것이 전혀 생소하지 않을 것입니다.

 

②에서는 CallDelegate라는 static 메서드를 호출하고 있는데, 이 메서드가 받는 인자를 보면,

③의 DoubleOp delegate형입니다. 즉, DoubleOp delegate와 double형의 값을 받는 메서드입니다. ④이 실행되면 action delegate가 참조하는 메서드를 호출하게 됩니다.

즉, ①에서 저장된 메서드들이겠죠. 

②를 보시면 CallDelegate 메서드를 호출할 때, operations[i]와 숫자를 인자로 넘기는 것을

볼 수 있습니다. 여기서 operations[i]는 delegate가 참조하는 메서드를 가리킵니다.

CallDelegate라는 메서드를 호출할 필요없이 곧바로 delegate가 참조하는 메서드를 실행하려면

다음과 같이하면 됩니다. 

operations[i](2.0); 

지금까지는 delegate를 사용하는 방법, 즉 문법에 대해서 공부했습니다.

이제 여러분이 기다리시던 “어떤 상황에서 delegate를 사용해야 하나?”에 대해서

생각해보겠습니다. ^^ 

“Delegate를 어떻게 사용하는지는 알겠는데, 꼭 사용해야 하는건 아니지 않나요?”

 

네, 그렇게 생각하실 수도 있겠죠. 우리가 본 예제에서도 delegate가 꼭 필요한 것은 아니었습니다. 하지만, delegate가 꼭 필요한 경우가 있습니다.

 

예를 들어 보죠. 여러분이 다음과 같은 버블정렬 알고리즘을 만들었습니다.

예제 3) 
for (int i=0; i<sortArray.Length; i++) 
{ 
     for (int j=0; j<i; j++) 
     { 
          ①if (j > i) // 문제의 소지가 있는 부분 
          { 
               int temp = sortArray[i]; // i 와 j 바꾸기 
               sortArray[i] = sortArray[j]; 
               sortArray[j] = temp; 
          } 
     } 
}



네.. 정말 멋진 알고리즘이져? 상당히 간단하죠? 비효율적이긴 하지만요.

아무튼, 그런건 여기서 별로 중요한게 아니구여. 우리가 여기서 생각해 보아야 할 부분은

①입니다.

 

두 개의 데이터를 비교하는 부분이죠. ①에 무슨 문제가 있을까요? 

네, 사실 코드 자체에는 문제가 없습니다.

 

제가 말씀 드리고자 한 것은 이 코드를 사용자가 사용하려고 했을 때 발생하는 문제입니다.

사용자가 정렬하고자 하는 데이터가 숫자라면, 사실 별 문제가 없겠죠.

 

하지만, 사용자가 정의한 클래스나 구조체 등을 정렬하려고 한다면 문제가 발생하겠죠.

사용자가 어떤식으로 클래스나 구조체 등을 정의해 놓았는지 알 수가 없기 때문이죠.

 

사용자에게 물어보면 되지 않냐구여?

각각의 사용자를 위해 일일이 다 프로그래밍을 해 준다면, 그건 프로그래밍이라고 볼 수

없겠죠. 노가다죠? 만약, 10만 명의 사용자에게 여러분이 만든 버블정렬 컴포넌트를 판매한다고 가정한다면, 각 사용자를 위해 다른 프로그램을 만든다는건 말이 안되겠죠?

자, 이럴 때 필요한 것이 바로 delegate입니다!! 

지금까지 우리가 얘기한 문제(사용자 정의 클래스의 비교 등)를 해결하려면

①의 코드를 j > i 대신에 delegate로 바꾸어 주면 됩니다. 예제를 보겠습니다. 

예제 4) 
//우리가 배포하는 버블정렬 컴포넌트 
class BubbleSorter 
{ 
     ①static public void Sort(object [] sortArray, CompareOp rhsIsGreater) 
     { 
          for (int i=0; i<sortArray.Length; i++) 
          { 
               for (int j=0; j<i; j++) 
               { 
                    ②if (rhsIsGreater(sortArray[i], sortArray[j])) 
                    { 
                         object temp = sortArray[i]; 
                         sortArray[i] = sortArray[j]; 
                         sortArray[j] = temp; 
                    } 
               } 
          } 
     } 
} 

//사용자 정의 클래스 
class Employee 
{ 
     private string name; 
     private decimal salary; 

     public Employee(string name, decimal salary) 
     { 
          this.name = name; 
          this.salary = salary; 
     } 

     public override string ToString() 
     { 
          return string.Format(name + “, {0:C}”, salary); 
     } 

     //사용자 정의 클래스 비교 메서드 
     ③public static bool RhsIsGreater(object lhs, object rhs) 
     { 
          Employee Lhs = (Employee) lhs; 
          Employee Rhs = (Employee) rhs; 
          return (Rhs.Salary > Lhs.Salary) ? true : false; 
     } 
} 

 
delegate bool CompareOp (object lhs, object rhs); 

class DelegateClass 
{ 
     static void Main(string[] args) 
     { 
          Employee [] employees = 
                    { new Employee(“순돌이”, 50000), 
                    new Employee(“순딩이”, 1500), 
                    new Employee(“차돌이”, 30000), 
                    new Employee(“공돌이”,1000)}; 
          ④CompareOp EmployeeCompareOp = new CompareOp(Employee.RhsIsGreater); 
          ⑤BubbleSorter.Sort(employees, EmployeeCompareOp); 

          for (int i=0; i<employees.Length; i++) 
               Console.WriteLine(employees[i].ToString()); 
     } 
} 



위의 소스코드만 보셔두 delegate가 어떤 식으로 활용되었는지 아실 겁니다.


①을 보시면 우리가 배포할 버블정렬 컴포넌트를 다시 편집한 것을 보실 수 있습니다.

컴포넌트는 두 개의 인자를 받는데, 첫번째는 정렬하게 될 object(즉, 모든 데이터 타입을 받을 수 있습니다. Object는 가장 상위클래스이기 때문이죠.) 타입의 데이터이고, 두번째 인자는 CompareOp delegate 입니다.

 

다시 정리하면, object형 데이터와 delegate를 인자로 받는 버블정렬 컴포넌트 입니다. 

②에서는 이전 소스코드에서 문제가 되었던 부분을 수정한 것입니다.

단지, 숫자만 비교할 수 있게 만든 것이 아니라, delegate를 통해서 사용자 정의 비교 메서드를

호출하여 값을 비교하는 것입니다. 여기서는 rhsIsGreater라는 delegate를 사용하고 있죠. 

③은 사용자가 정의한 Employee라는 클래스를 비교하는 메서드 입니다. 
④에서는 CompareOp delegate가 ③에서 정의한 사용자 정의 비교 메서드를 참조하도록

하고 있습니다. 

⑤에서는 employees라는 배열 데이터와 EmployeeCompareOp라는 delegate를 Sort() 메서드의

인자로 넘겨 정렬하고 있습니다. 

위의 예제를 보시면, 이제 우리가 만든 버블정렬 컴포넌트는 어떤 상황에도 문제없이 사용될 수 있다는 것을 알 수 있습니다. 버블정렬 컴포넌트의 핵심기술(?)은 두 데이터의 값을 비교하는 것인데, 이 부분을 delegate로 바꿈으로써, 사용자가 정의할 수 있도록 만든 것이죠.

그만큼 컴포넌트 유연성 혹은 확장성이 확대된 것입니다. ^^ 

Delegate 연산(Multicast Delegates) 
자… 이제 마지막으로 delegate 연산에 대해서 잠깐 알아 보도록 하겠습니다. 

우리는 delegate도 배열로 선언되어 사용될 수 있음을 보았습니다.

하지만, 꼭 배열로 선언하지 않아도 하나의 delegate에 여러 개의 메서드를 넣을 수가 있습니다.

즉, 다음과 같이 쓸 수 있습니다. 

delegate void DoubleOp(double value); 

class MainEntryPoint 
{ 
     static void Main(string[] args) 
     { 
          DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo); 
          operations += new DoubleOp(MathOperations.Square); 
 

여러분이 원하시면, 다음과 같이 하는 것도 똑 같은 효과를 냅니다. 

DoubleOp operation1 = new DoubleOp(MathOperations.MultiplyByTwo); 
DoubleOp operation2 = new DoubleOp(MathOperations.Square); 
DoubleOp operations = operation1 + operation2;



하지만, 주의하실 점은 선언된 delegate의 리턴형이 void가 아닐 경우

delegate 연산은 불가능합니다.

 

만약, 리턴형이 있으면 여러 개의 메서드들 중에서 하나의 메서드만 제대로 실행되기 때문에

나머지 메서드들은 null이나 잘못된 결과를 얻게 됩니다.

 

이런 이유로, 컴파일러는 리턴형이 void가 아닌 delegate의 연산을 허용하지 않습니다. 

똑같은 이유로, delegate 연산 시 out 키워드를 쓰는 인자를 사용하는 것도 바람직하지 않습니다.

이 경우에는 컴파일이 되지만, 마지막 메서드를 제외한 다른 메서드들은 결과값을 잃어버리게 됩니다. 

리턴형이 void인 delegate가 선언되면, 컴파일러는 자동으로 multicast delegate로 인식하게 되어 delegate 연산이 가능해 집니다. 반대로 void가 아니면, 일반적인 delegate로 인식하겠죠? ^^



네.. 지금까지 delegate에 대해서 알아봤습니다. 이해가 되셨는지 모르겠습니다.