1. 대리자를 선언하는 C# 문법

: 대리자 학습 관점은, 함수를 변수처럼 사용하려는 기법 이라는 것

더보기
[한정자] delegate 반환_형식 대리자_이름(매개변수-목록);
// 예시
public delegate void MyDelegate(string message);

public                                         :접근 제한자(한정자)
       delegate                                :대리자 선언 키워드
                void                           :반환 형식
                     MyDelegate                :대리자 이름
                               (string message):매개변수 목록

 

2. 대리자 학습을 위한 함수 다시 살펴보기

: 문법 비교(함수 vs 대리자)

더보기

A. 함수를 선언하는 C# 문법

[한정자] 반환_형식 메서드_이름(매개변수목록)
{
    // 실행 코드
    return ...; // (반환형식이 void가 아닌 경우 필수)
}
// 예시
public int Add(int a, int b)
{
    return a + b;
}

 

 

B. 문법 비교(함수 vs 대리자)

: delegate 키워드 외 함수와 선언 같다. 

[한정자] delegate 반환_형식 대리자_이름(매개변수_목록);
[한정자] 반환_형식 메서드_이름(매개변수_목록)

 

 

C. 함수 시그니처

: 함수(메서드)를 구분하는 고유한 "형태"입니다.

  • C#에서는 일반적으로
    • 메서드 이름
    • 매개변수 타입
    • 매개변수 순서
      조합이 함수의 시그니처를 구성합니다.
  • 반환형은 오버로드 구분 요소에는 포함되지 않지만, 대리자의 시그니처에는 반드시 포함됩니다.

 


시그니처 비교(함수 vs 대리자)

: 대리자와 함수의 시그니처는 매개변수: (int, int) 반환형: int

public delegate int Add_Delegate(int a, int b);
public int Add(int x, int y) { return x + y; }

 

3. 대리자와 함수 사용 예시

더보기
// int 두 개 받아 int 반환하는 대리자
public delegate int Add_Delegate(int a, int b);

public class Program
{
    public int Add(int x, int y) { return x + y; }
    //public static int Add(int x, int y) => x + y;

    public static void Main()
    {
        // .NET 1.0 스타일
        Add_Delegate d1 = new Add_Delegate(Add);
        Console.WriteLine(d1(2, 3));  // 5
        Console.WriteLine(Add(2, 3)); // 5

        // .NET 2.0 스타일
        Add_Delegate d2 = Add;
        Console.WriteLine(d2(4, 5));  // 9
        Console.WriteLine(Add(4, 5)); // 9
    }
}

 

 

컴파일러가 만들어내는 특별한 클래스(간략)

: 대리자의 Invoke 메서드 시그니처와 참조할 함수의 헤더가 일치해야 연결할 수 있습니다.

// 대리자를 사용할 때, 컴파일러가 만들어내는 클래스 형태
public sealed class Add_Delegate : MulticastDelegate
{
    public Add_Delegate(object target, IntPtr method);

    public int Invoke(int a, int b);
}

 

(+ 참고)

더보기

[ .NET Framework 1.0 스타일 (명시적 생성자 사용) ]

[ .NET Framework 2.0 스타일 (단축 문법) ]

// 대리자 선언
public delegate void MyDelegate(string message);

public class Program
{
    public static void PrintMessage(string msg)
    {
        Console.WriteLine("출력: " + msg);
    }

    public static void Main()
    {
        // 1.0 스타일: new 키워드로 delegate 생성
        MyDelegate d = new MyDelegate(PrintMessage);

        // 대리자 호출
        d("Hello .NET 1.0");
        
        
        // 2.0 스타일: 메서드 이름만 지정 → 컴파일러가 자동으로 new MyDelegate(...) 생성
        MyDelegate d = PrintMessage;

        // 대리자 호출
        d("Hello .NET 2.0");
    }
}

// 출력: Hello .NET 1.0
// 출력: Hello .NET 2.0

 

 

[ 비교 ]


버전   예시  특징
.NET 1.0 MyDelegate d = new MyDelegate(PrintMessage); 항상 new 키워드 필요
.NET 2.0+ MyDelegate d = PrintMessage; 컴파일러가 자동으로 delegate 인스턴스 생성

 

4. 자료형(Data Type)

: 함수를 변수로 만드는 방법

더보기

A. 자료형 발전

  • C#의 자료형은 크게
    기본 자료형 → 복합 자료형 → 사용자 정의 자료형 → 클래스 → 대리자(delegate)로 확장·발전해 왔습니다.
  • 대리자도 엄연히 참조형(Reference Type, 자료형)이며, 특별한 클래스(MulticastDelegate 상속)입니다.

 

 

B. 함수 매개변수로 함수를 전달

  • 함수의 매개변수로 전달 가능한 것은, 값(value) 또는 참조(reference)였지, 함수를 직접 전달할 수는 없습니다.
  • 대리자를 사용하면 "메서드를 가리키는 참조형 타입"으로,
    함수를 다른 메서드에 전달하거나 나중에 실행할 수 있게,
    함수를 변수처럼 포장해서 전달할 수 있습니다.

 

 

C. 대리자의 목적

  • 대리자는 일종의 특수한 클래스로, 메서드를 참조할 수 있는 데이터 형식입니다.
    기존에는 함수의 매개변수로 값이나 참조(변수 형태)만 전달할 수 있었지만,
    대리자를 사용하면 함수 자체를 변수처럼 다루어 매개변수로 전달, 저장, 호출할 수 있게 해 줍니다.
  • 이벤트(Event) 시스템이나 콜백(Callback) 메커니즘에서 핵심적으로 쓰입니다.

 

 

Step 1. 소스코드 구현 예시

// int 두 개 받아 int 반환하는 대리자
public delegate int Add_Delegate(int a, int b);

public class Program
{
    public int Add(int x, int y) { return x + y; }
    //public static int Add(int x, int y) => x + y;

    public static void Main()
    {
        // .NET 1.0 스타일
        Add_Delegate d1 = new Add_Delegate(Add);
        Console.WriteLine(d1(2, 3));  // 5
    }
}

 

 

Step 2. 빌드 시, 컴파일러가 대리자로 생성하는 클래스 형태

// 실제로는 MulticastDelegate 상속
public sealed class Add_Delegate : MulticastDelegate
{
    // 생성자: 어떤 객체, 어떤 메서드 참조할지 저장
    public Add_Delegate(object target, IntPtr method);

    // 시그니처에 맞는 Invoke 메서드 생성
    public int Invoke(int a, int b);

    // 비동기 호출 지원 (BeginInvoke/EndInvoke)도 자동 생성됨
}

 

 

Step 3. 실행 흐름

d1(2,3) 
   ↓
d1.Invoke(2,3)  //(컴파일 과정에서 대리자가 변환된 클래스 내부의 invoke 호출)
   ↓
Add(2,3) 실행

 

5. 대리자는 왜, 언제 사용하는가?

: SOLID 원칙을 학습 후에 참고합니다.

더보기

A. 대리자 없이 if/else 분기 (일반 호출)

: 새로운 계산 방식이 필요하면 if/else나 switch문으로 메서드를 고정 분기해야 함.

public class Program
{
    public static int Add(int x, int y) { return x + y; }
    public static int Multiply(int x, int y) { return x * y; }
    public static int Subtract(int x, int y) { return x - y; }

    public static void Main()
    {
        int a = 10, b = 5;
        string operation = "Multiply"; // 실행 시 원하는 연산 선택

        int result = 0;

        if (operation == "Add")
            result = Add(a, b);
        else if (operation == "Multiply")
            result = Multiply(a, b);
        else if (operation == "Subtract")
            result = Subtract(a, b);
        else
            Console.WriteLine("지원하지 않는 연산입니다.");

        Console.WriteLine($"결과: {result}");
    }
}

단점

  • 새로운 연산(Divide, Power 등)을 추가할 때마다
    메서드 정의 + if/else 또는 switch문 수정을 반복해야 합니다.
  • 코드가 점점 길어지고, 확장성·유연성이 떨어집니다.

 

 

B. 대리자 사용 – 전략 주입으로 분기 제거

public delegate int Calc(int a, int b);

public class Program
{
    public static int Add(int x, int y) { return x + y; }
    public static int Multiply(int x, int y) { return x * y; }
    public static int Subtract(int x, int y) { return x - y; }

    // “무슨 연산을 할지” 외부에서 주입받는 공통 실행 함수
    public static int Run(Calc operation, int a, int b)
    {
        //if (operation == null) throw new ArgumentNullException(nameof(operation));
        return operation(a, b);
    }

    public static void Main()
    {
        int a = 10, b = 5;

        Console.WriteLine(Run(Add, a, b)); // 15
        Console.WriteLine(Run(Multiply, a, b)); // 50
        Console.WriteLine(Run(Subtract, a, b)); // 5
    }
}

개선점

  • 가독성/유연성
    • A: 호출부가 연산 “선택”과 “실행”을 동시에 담당 → 연산 추가 시 분기문 수정 필요(개방-폐쇄 원칙 OCP 위배)
    • B: 호출부는 “실행”만, “선택”은 외부 주입 → 새 전략 추가 시 호출부 무변경(확장 용이)
      개방-폐쇄 원칙(OCP): 새 연산을 추가할 때 Run은 수정하지 않음(닫힘), 새 메서드만 추가해 전달(열림).
  • 결합도/테스트성
    • A: 호출부가 구체 메서드에 강한 결합.
    • B: 시그니처만 의존 → 다른 구현/객체 주입으로 단위 테스트 용이

 

 

C. 딕셔너리로 치환

using System;
using System.Collections.Generic;

public delegate int Calc(int a, int b);

public class Program
{
    public static int Add(int x, int y) { return x + y; }
    public static int Multiply(int x, int y) { return x * y; }
    public static int Subtract(int x, int y) { return x - y; }

    private static readonly Dictionary<string, Calc> Ops =
        new(StringComparer.OrdinalIgnoreCase)
        {
            ["Add"]      = Add,
            ["Multiply"] = Multiply,
            ["Subtract"] = Subtract,
        };

    public static void Main()
    {
        int a = 10, b = 5;
        string op = "Multiply";

        if (Ops.TryGetValue(op, out var f))
            Console.WriteLine(f(a, b));   // 전략 실행
        else
            Console.WriteLine("지원하지 않는 연산입니다.");
    }
}

 

추가 예제

더보기

예제 1

run은 대리자의 변수이지만, 연결된 함수를 실행하듯이 사용 가능하다.

namespace delegate_test2
{
    delegate void A(int i);

    class MainClass
    {
        static void Run1(int val)
        {
            Console.WriteLine("{0}", val);// 콘솔출력 : 1024
        }

        static void Run2(int value)
        {
            Console.WriteLine("0x{0:X}", value);// 콘솔출력 : 0x800
        }

        static void Main(string[] args)
        {
            A run = run = new A(Run1); // [.NET Frame 1.0]
            run(1024); // 대리자 변수명 run 사용

            run = Run2; //[.NET Frame 2.0]
            run(2048);
        }
    }
}

 

예제 2

namespace delegate_test3
{
    class MySort
    {
        // 델리게이트 CompareDelegate 선언
        public delegate int CompareDelegate(int i1, int i2);

        public static void Sort(int[] arr, CompareDelegate comp)
        {
            if (arr.Length < 2) 
                return;

            Console.WriteLine("함수 Prototype: " + comp.Method);

            int ret;
            for (int i = 0; i < arr.Length - 1; i++)
            {
                for (int j = i + 1; j < arr.Length; j++)
                {
                    // 오름차순, 기준값과 다음값의 차가 음수면, 다음값이 "  큰 수" 이므로 교환
                    // 내림차순, 기준값과 다음값의 차가 양수면, 다음값이 "작은 수" 이므로 교환
                    ret = comp(arr[i], arr[j]);
                    if (ret == -1)
                    {
                        // 교환
                        int tmp = arr[j];
                        arr[j] = arr[i];
                        arr[i] = tmp;
                    }
                    Display(arr);
                }
            }
            Display(arr);
        }
        static void Display(int[] arr)
        {
            foreach (var i in arr) 
                Console.Write(i + " ");

            Console.WriteLine();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            (new Program()).Run();
        }

        void Run()
        {
            int[] unsortedList = { 3, 7, 2, 10, 1, 21, 6 };

            // 오름차순으로 정렬
            MySort.CompareDelegate compDelegate = AscendingCompare;
            MySort.Sort(unsortedList, compDelegate);

            // 내림차순으로 정렬
            compDelegate = DescendingCompare;
            MySort.Sort(unsortedList, compDelegate);
        }

        // CompareDelegate 델리게이트와 동일한 Prototype
        int AscendingCompare(int num1, int num2)
        {
            if (num1 == num2) 
                return 0;
            return (num1 - num2) > 0 ? -1 : 1;
        }

        // CompareDelegate 델리게이트와 동일한 Prototype
        int DescendingCompare(int num1, int num2)
        {
            if (num1 == num2) 
                return 0;
            return (num1 - num2) > 0 ? 1 : -1;
        }
    }
}