6. 대리자(1)
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;
}
}
}