delegate 01.
C# 은 MSDN이 정답이다.
함수 & 매개변수
학습목표
언제나 그렇듯 프로그래밍 학습은 개발자 관점에서 대리자(delegate)라는 기술이,
프로그래밍에서 어떤 기존 개발 방법을 조금이라도 쉽고, 편리하게 하기 위한 것일까? 를 파악하는 것이 목표다.
학습순서
- Callback 개념과 Delegate 용어
- 대리자를 사용하는 이유
- Delegate를 선언하고 사용하는 방법
- 일반화 대리자를 사용하는 방법
- Multicast Delegete (대리차 체인)
01. Callback 개념과 Delegate 용어
01.1 Callback 의 구조
01.2 C# 의 Callback 의 구조
01.3 Delegate 용어
Delegate 라는 용어는, 한국어로 '대리자', '대리인'을 의미합니다.
대신해서 일해주는 것을 전문으로 하는 사람을 의미합니다.
C, C++ 은, 변수 대신에 포인터와 참조를 사용했습니다.
C# 은, 포인터는 없지만 참조를 변수 대신 사용할 수 있습니다.
그리고 C# 은, 함수의 참조를 함수 대신 사용할 수 있습니다.
함수의 참조값을 가진 참조 변수를 Delegate 변수라고 합니다.
01.4 정리
Delegate 학습을 시작할 때, 명심해야 할 점은 다음과 같습니다.
- delegate 는 프로그래밍에서 어떤 점을 편리하게 하는가?
- 일반적으로 함수를 사용할 때, 매개변수로 값을 전달 할 수 있고, "전달하는 값"에 따라 함수의 결과가 달라진다.
Console.WriteLine("매개변수 값");
- delegate 는 함수를 사용할 때, 매개변수에 로직을 전달 할 수 있고, "전달하는 로직"이 다르다면 어떻게 되는가?
Console.WriteLine("대리자 함수 로직");
02. 대리자를 사용하는 이유
함수에 값이 아닌 '로직' 자체를 매개변수 처럼 전달하고 싶을 때 사용합니다.
C++ Qt 에서 커넥트의 시그널과 슬롯처럼 사용합니다.
C++ Qt 에서 커넥트 기능이 없으면 커넥트와 동일한 구조를 어떻게 구현 할 수 있나요?
03. 대리자 사용법
03.1 기본 사용법
03.1.1 선언방법
함수 헤더에 delegate 로 수식하면 된다.
[델리게이트_키워드] [함수_반환형] [델리게이트_이름] (함수_시그니처);
delegate return_type delegate_name(int num1, int num2);
[.NET Frame 1.0]
델리게이트_키워드 함수_반환형 델리게이트_이름 = new 델리게이트_키워드(함수_이름)
delegate return_type delegate_name = new delegate(funtion);
[.NET Frame 2.0]
델리게이트_키워드 함수_ 반환형 델리게이트_이름 = 함수_이름
delegate void delegate_name = funtion;
03.1.2 시그니처
매개변수 타입, 매개변수 갯수, 리턴 타입
함수의 헤더(function header)
03.1.3 대리자를 사용해, 함수를 변수처럼 사용 할 수 있다.
/* 03.Mar.2024
* 변수명, 함수명 따라하지 말 것
* 최소한 코딩 컨벤션을 지킬 것
* https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/coding-style/coding-conventions
*/
namespace DelegateTest01
{
// Plus, Minus 함수를 가진 Calculator 클래스
class Calculator
{
public int Plus(int a, int b)
{
return a + b;
}
public static int Minus(int a, int b)
{
return a - b;
}
}
/* Calculator 클래스의 Plus, Minus 함수와
* 동일한 시그니처를 가진 대리자 선언 */
delegate int Connect(int a, int b);
class MainClass
{
static void Main(string[] args)
{
/* [01]
* Calculator 클래스의 Plus, Minus 함수 사용 */
Calculator Calc = new Calculator();
Console.WriteLine(Calc.Plus(3, 4));
Console.WriteLine(Calculator.Minus(3, 4));
/* [02]
* Calculator 클래스의 Plus 함수 사용 */
Console.WriteLine(Calc.Plus(10, 5));
// 대리자 Callback을 Calculator 클래스의 Plus 함수 대신 사용
Connect Callback1 = new Connect(Calc.Plus);
Console.WriteLine(Callback1(10, 5));
/* [03]
* Calculator 클래스의 Minus 함수 사용 */
Console.WriteLine(Calculator.Minus(20, 10));
// 대리자 Callback을 Calculator 클래스의 Minus 함수 대신 사용
Connect Callback2 = new Connect(Calculator.Minus);
Console.WriteLine(Callback2(20, 10));
}
}
}
03.2 함수의 매개변수 사용법 비교
아래 나열된 3가지 매개변수 사용법과, 위 대리자 기본 사용법을 응용한다.
*함수의 매개변수 자리에는 변수 형태로 전달되어야 한다는 것과
*대리자는 매개변수를 전달하는 방법일 뿐임을 확인한다.
매개변수 사용법 1. 매개변수 자리에 변수를 사용해 값 전달
int num1 = 1;
int num2 = 2;
int Add(int a, int b)
{
int result = 0;
// 매개변수로 전달받은 두 값 계산
result = a + b;
return result;
}
매개변수 사용법 2. 매개변수 자리에 객체를 사용해 값 전달
class object_
{
public int a;
public int b;
}
int Add_obj(object_ obj)
{
int result = 0;
// 객체로 전달받은 두 값 계산
result = (int)obj.a + (int)obj.a;
return result;
}
매개변수 사용법 3. 매개변수에 대리자를 사용해 로직 전달
delegate int A(int a, int b);
int Add_dele(A func)
{
int result = 0;
// 매개변수로 전달받은 대리자를 사용한 계산
result = func(1, 2);
return result;
}
03.3 함수의 매개변수로 전달되는 값 사용법 비교
매개변수 사용법 1. 변수 값을 함수에 전달 하기
namespace DelegateTest02
{
class MainClass
{
int Plus(int a, int b)
{
int result = 0;
// 매개변수로 전달받은 두 값 계산
result = a + b;
return result;
}
static void Main(string[] args)
{
// 매개변수 사용법 1. 변수 값을 함수에 전달 하기
int num1 = 1;
int num2 = 2;
MainClass A = new MainClass();
Console.WriteLine(A.Plus(num1, num2));
}
}
}
매개변수 사용법 2. 객체의 변수 값을 함수에 전달 하기
namespace DelegateTest03
{
class Object_
{
public int num1;
public int num2;
}
class MainClass
{
int Plus(Object_ obj)
{
int result = 0;
// 매개변수로 전달받은 두 값 계산
result = obj.num1 + obj.num2;
return result;
}
static void Main(string[] args)
{
// 매개변수 사용법 2. 객체의 변수 값을 함수에 전달 하기
Object_ obj = new Object_();
obj.num1 = 1;
obj.num2 = 2;
MainClass A = new MainClass();
Console.WriteLine(A.Plus(obj));
}
}
}
매개변수 사용법 3. 함수 로직을 함수에 전달 하기
- 함수를 실행시킬 때, 다른 함수도 실행되도록 만듦
namespace DelegateTest04
{
class Object_
{
public int Plus(int a, int b)
{
return a + b;
}
}
/* Object_ 클래스의 Plus 함수와
* 동일한 시그니처를 가진 대리자 선언 */
delegate int Connect(int a, int b);
class MainClass
{
static void Main(string[] args)
{
int num1 = 1;
int num2 = 2;
// 매개변수 사용법 2. 객체의 변수 값을 함수에 전달 하기
Object_ obj = new Object_();
// 대리자 Callback을 Object_ 클래스의 Plus 함수 대신 사용
Connect Callback = new Connect(obj.Plus);
Console.WriteLine(Callback(num1, num2));
//Console.WriteLine(obj.Plus(num1, num2));
}
}
}
namespace DelegateTest05
{
/* Object_ 클래스의 함수와
* 동일한 시그니처를 가진 대리자 선언 */
delegate int Connect(int a, int b);
class Object_
{
public int Plus(int a, int b)
{
return a + b;
}
public static int Minus(int a, int b)
{
return a - b;
}
public int Calc(int a, int b, Connect callaback)
{
int num1 = a;
int num2 = b;
return callaback(1,2);
}
}
class MainClass
{
static void Main(string[] args)
{
int num1 = 1;
int num2 = 2;
Object_ obj = new Object_();
if (num1 < num2)
Console.WriteLine(obj.Calc(num1, num2, obj.Plus));
else
Console.WriteLine(obj.Calc(num1, num2, Object_.Minus));
/* Q.문제
* Object_ 클래스의 Calc() 함수를 수정하여
* num1이 num2보다 크면 Plus()함수를 실행하고,
* num1이 num2보다 작으면 Minus()를 실행하는 함수를 구현하세요.
*/
}
}
}
namespace DelegateTest06
{
/* [2]
* 일반함수 Plus(int a, int b)와 동일한
* 시그니처를 가진 대리자 선언 */
delegate int Connect(int a, int b);
class MainClass
{
/*[1] 일반함수*/
static int Plus(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
Connect callback = new Connect(Plus);
int res = Add(callback);
Console.WriteLine("계산 결과: " + res);
}
/*[3] 대리자를 매개변수로 하는 함수*/
static int Add(Connect func)
{
return func(1, 2);
}
}
}
매개변수 사용법 4. 대리자와 버블 정렬
버블 정렬
https://youtu.be/YbsQiiubO74?si=DCvRp8qu-k6ecHaX
/* 03.Mar.2024
* 변수명, 함수명 따라하지 말 것, 최소한 코딩 컨벤션을 지킬 것
* https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/coding-style/coding-conventions
*/
namespace DelegateTest08
{
delegate int Compare(int a, int b);
class Sort
{
public int AscendCompare(int a, int b)
{
if (a > b)
return 1;
else if (a == b)
return 0;
else
return -1;
}
public int DescendCompare(int a, int b)
{
if (a < b)
return 1;
else if (a == b)
return 0;
else
return -1;
}
public void BubbleSort(int[] DataSet, Compare Comparer)
{
int i = 0;
int j = 0;
int temp = 0;
for (i = 0; i < DataSet.Length - 1; i++)
{
for (j = 0; j < DataSet.Length - (i + 1); j++)
{
if (Comparer(DataSet[j], DataSet[j + 1]) > 0)
{
temp = DataSet[j + 1];
DataSet[j + 1] = DataSet[j];
DataSet[j] = temp;
}
}
}
}
}
class MainApp
{
static void Main(string[] args)
{
Sort sort = new Sort();
/*[1] 오름차순
*/
int[] array = { 3, 7, 4, 2, 10 };
Console.WriteLine("Sorting ascending...");
sort.BubbleSort(array, new Compare(sort.AscendCompare));
for (int i = 0; i < array.Length; i++)
Console.Write($"{array[i]} ");
/*[2] 내림차순
*/
int[] array2 = { 7, 2, 8, 10, 11 };
Console.WriteLine("\nSorting descending...");
sort.BubbleSort(array2, new Compare(sort.DescendCompare));
for (int i = 0; i < array2.Length; i++)
Console.Write($"{array2[i]} ");
Console.WriteLine();
}
}
}
04. 일반화 대리자
namespace DelegateTest09
{
delegate int Compare<T>(T a, T b);
class Sort
{
public int AscendCompare<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b);
}
public int DescendCompare<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) * -1;
}
public 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 - (i + 1); j++)
{
if (Comparer(DataSet[j], DataSet[j + 1]) > 0)
{
temp = DataSet[j + 1];
DataSet[j + 1] = DataSet[j];
DataSet[j] = temp;
}
}
}
}
}
class MainApp
{
static void Main(string[] args)
{
Sort sort = new Sort();
/*[1] 오름차순
*/
int[] array = { 3, 7, 4, 2, 10 };
Console.WriteLine("Sorting ascending...");
sort.BubbleSort<int>(array, new Compare<int>(sort.AscendCompare));
for (int i = 0; i < array.Length; i++)
Console.Write($"{array[i]} ");
/*[2] 내림차순
*/
int[] array2 = { 7, 2, 8, 10, 11 };
Console.WriteLine("\nSorting descending...");
sort.BubbleSort(array2, new Compare<int>(sort.DescendCompare));
for (int i = 0; i < array2.Length; i++)
Console.Write($"{array2[i]} ");
Console.WriteLine();
}
}
}
05. Multicast Delegate(대리자 체인)
C++ Qt에서 Connect 의 Signal 과 Slot 을 1:1 로 연결하는 것이 아니라 1:多로 연결해 사용한것처럼
대리자도 동일한 사용 방법을 제공한다.
namespace DelegateTest01_1
{
// Plus, Minus 함수를 가진 Calculator 클래스
class Calculator
{
public void Plus(int a, int b)
{
Console.WriteLine("Plus " + (a + b));
}
public static void Minus(int a, int b)
{
Console.WriteLine("Minus " + (a - b));
}
}
/* Calculator 클래스의 Plus, Minus 함수와
* 동일한 시그니처를 가진 대리자 선언 */
delegate void Connect(int a, int b);
class MainClass
{
static void Main(string[] args)
{
/* [01]
* Calculator 클래스의 Plus, Minus 함수 사용 */
Calculator Calc = new Calculator();
Calc.Plus(3, 4);
Calculator.Minus(3, 4);
Console.WriteLine();
// 대리자 Callback을 Calculator 클래스의 Plus 함수 대신 사용
Connect Callback1 = new Connect(Calc.Plus);
Callback1(10, 5);
Console.WriteLine();
Callback1 += Calculator.Minus;
Callback1(20, 10);
Console.WriteLine();
Callback1 += Calculator.Minus;
Callback1(30, 10);
}
}
}
C++ Qt의 Connect 에서 Signal 과 Slot 관계를
C#의 Event 와 EventListener 관계와 동일 개념으로 이해하고 접근해도 된다.
using System;
namespace DelegateTest10
{
delegate void Callback(string message);
class Connect
{
public Callback? Signal;
}
class Label
{
private string name;
public Label(string name)
{
this.name = name;
}
public void Slot_Function(string message)
{
Console.WriteLine($"{name}.SomethingHappened : {message}");
}
}
class MainApp
{
static void Main(string[] args)
{
// 버튼을 클릭하면, 레이블에 출력된다고 가정합니다.
// 버튼에 Signal, 레이블에 slot
// Signal 대리자를 가진 Connect 객체를 생성합니다.
Connect conn = new Connect();
// 레이블을 만들고
Label lbl = new Label("Label");
// Signal이 발생하면, 동작할 레이블의 slot 함수와 연결합니다.
conn.Signal += lbl.Slot_Function;
// 버튼이 눌렸을 때, Connect 객체의 Signal이발생한다고 가정합니다.
// conn 객체의 Signal 대리자가 실행되면(버튼이 눌리거나, 값이 입력되면)
// label의 Slot_Function 함수가 "You've got mail."을 전달받아 실행됩니다.
conn.Signal("You've got mail.");
}
}
}
C++ Qt 의 Signal, Slot 개념과 용어에서 벗어나
Event 와 Event Listener 개념과 용어에 익숙해져 봅시다.
using System;
namespace DelegateTest10
{
delegate void Callback(string message);
class Connect
{
public Callback? Signal;
}
class EventListener
{
private string name;
public EventListener(string name)
{
this.name = name;
}
public void SomethingHappend(string message)
{
Console.WriteLine($"{name}.SomethingHappened : {message}");
}
}
class MainApp
{
static void Main(string[] args)
{
// 대리자를 가진 Connect 객체
Connect conn = new Connect();
// 객체 3개 생성(ex. 레이블 3개 생성, 텍스트 뷰어 3개 생성)
// @Callback을 전달받아 실행해야 할 객체 생성, 아직 연결 X
EventListener listener1 = new EventListener("Listener1");
EventListener listener2 = new EventListener("Listener2");
EventListener listener3 = new EventListener("Listener3");
/*[1]
*/
// conn 객체에 EventListener 객체 3개를 대리자 체인으로 묶음
// @Callback을 전달받아 실행해야 할 객체의 함수를 대리자에 연결
conn.Signal += listener1.SomethingHappend;
conn.Signal += listener2.SomethingHappend;
conn.Signal += listener3.SomethingHappend;
// conn 객체의 EventOccured 대리자가 실행되면,
// @버튼이 눌리거나, 값이 입력되면 (= 대리자가 실행되면)
conn.Signal("You've got mail.");
Console.WriteLine();
/*[2]
*/
conn.Signal -= listener2.SomethingHappend;
conn.Signal("Download complete.");
Console.WriteLine();
/*[3]
*/
conn.Signal = new Callback(listener2.SomethingHappend)
+ new Callback(listener3.SomethingHappend);
conn.Signal("Nuclear launch detected.");
Console.WriteLine();
/*[4]
*/
Callback notify1 = new Callback(listener1.SomethingHappend);
Callback notify2 = new Callback(listener2.SomethingHappend);
conn.Signal = (Callback)Delegate.Combine(notify1, notify2);
conn.Signal("Fire!!");
Console.WriteLine();
conn.Signal = (Callback)Delegate.Remove(conn.Signal, notify2);
conn.Signal("RPG!");
}
}
}