System.Delegate와
이 문서에서는 대리자를 지원하는 .NET의 클래스와 이러한 클래스가 키워드에 delegate 매핑되는 방법을 설명합니다.
대리자란?
대리자는 개체에 대한 참조를 저장하는 방법과 비슷하게 메서드에 대한 참조를 저장하는 방법으로 간주합니다. 메서드에 개체를 전달할 수 있는 것처럼 대리자를 사용하여 메서드 참조를 전달할 수 있습니다. 이 기능은 다양한 메서드를 "연결"하여 다른 동작을 제공할 수 있는 유연한 코드를 작성하려는 경우에 유용합니다.
예를 들어 두 숫자로 작업을 수행할 수 있는 계산기가 있다고 상상해 보십시오. 추가, 빼기, 곱하기 및 분할을 별도의 메서드로 하드 코딩하는 대신 대리자를 사용하여 두 숫자를 사용하고 결과를 반환하는 모든 작업을 나타낼 수 있습니다.
대리자 형식 정의
이제 키워드를 사용하여 delegate 대리자 형식을 만드는 방법을 살펴보겠습니다. 대리자 형식을 정의할 때는 기본적으로 해당 대리자에서 저장할 수 있는 메서드 종류를 설명하는 템플릿을 만듭니다.
메서드 시그니처와 비슷한 구문을 사용하여 delegate 키워드로 대리자 형식을 정의합니다.
// Define a simple delegate that can point to methods taking two integers and returning an integer
public delegate int Calculator(int x, int y);
이 Calculator 대리자는 두 개의 int 매개변수를 받고 int를 반환하는 모든 메서드에 대한 참조를 보유할 수 있습니다.
좀 더 실용적인 예제를 살펴보겠습니다. 목록을 정렬하려는 경우 정렬 알고리즘에 항목을 비교하는 방법을 알려야 합니다. 대리자가 메서드에 어떻게 도움이 List.Sort() 되는지 살펴보겠습니다. 첫 번째 단계는 비교 작업에 대한 대리자 형식을 만드는 것입니다.
// From the .NET Core library
public delegate int Comparison<in T>(T left, T right);
이 Comparison<T> 대리자는 다음과 같은 모든 메서드에 대한 참조를 보유할 수 있습니다.
- 형식의 두 매개 변수를 사용합니다.
T - (일반적으로 "보다 작음
int", "같음" 또는 "보다 큼"을 나타내는 -1, 0 또는 1)을 반환합니다.
이와 같이 대리자 형식을 정의하면 컴파일러는 서명과 일치하는 클래스에서 System.Delegate 파생된 클래스를 자동으로 생성합니다. 이 클래스는 메서드 참조를 저장하고 호출하는 모든 복잡성을 처리합니다.
Comparison 대리자 형식은 제네릭 형식이므로 모든 형식T에서 작동할 수 있습니다. 제네릭에 대한 자세한 내용은 제네릭 클래스 및 메서드를 참조하세요.
구문은 변수 선언과 비슷하지만 실제로 새 형식을 선언하는 것입니다. 클래스 내부, 네임스페이스 내부 또는 전역 네임스페이스에서 대리자 형식을 정의할 수 있습니다.
비고
전역 네임스페이스에서 직접 대리자 형식(또는 기타 형식)을 선언하는 것은 권장되지 않습니다.
또한 컴파일러는 이 클래스의 클라이언트가 인스턴스의 호출 목록에서 메서드를 추가하고 제거할 수 있도록 이 새 형식에 대한 추가 및 제거 처리기를 생성합니다. 컴파일러는 추가 또는 제거되는 메서드의 서명이 대리자 형식을 선언할 때 사용되는 서명과 일치하게 합니다.
대리자의 인스턴스 선언
대리자 형식을 정의한 후 해당 형식의 인스턴스(변수)를 만들 수 있습니다. 메서드에 대한 참조를 저장할 수 있는 "슬롯"을 만드는 것으로 간주합니다.
C#의 모든 변수와 마찬가지로 네임스페이스 또는 전역 네임스페이스에서 직접 대리자 인스턴스를 선언할 수 없습니다.
// Inside a class definition:
public Comparison<T> comparator;
이 변수의 형식은 Comparison<T> (앞에서 정의한 대리자 형식) 변수의 이름입니다 comparator. 이 시점에서 comparator 는 아직 메서드를 가리키지 않습니다. 이는 채워질 때까지 기다리는 빈 슬롯과 같습니다.
다른 변수 형식과 마찬가지로 대리자 변수를 지역 변수 또는 메서드 매개 변수로 선언할 수도 있습니다.
대리자 호출
메서드를 가리키는 대리자 인스턴스가 있으면 대리자를 통해 그 메서드를 호출할 수 있습니다. 대리자를 마치 메서드를 호출하듯이 호출하여 대리자의 호출 목록에 있는 메서드를 실행합니다.
메서드가 비교 대리자를 Sort() 사용하여 개체의 순서를 결정하는 방법은 다음과 같습니다.
int result = comparator(left, right);
이 줄에서 코드는 대리자에 연결된 메서드를 호출합니다 . 대리자 변수는 메서드 이름인 것처럼 처리하고 일반 메서드 호출 구문을 사용하여 호출합니다.
그러나 이 코드 줄은 안전하지 않다고 가정합니다. 대상 메서드가 대리자에 추가되었다고 가정합니다. 메서드가 연결되지 않은 경우, 위의 줄로 인해 NullReferenceException 예외가 발생합니다. 이 문제를 해결하는 데 사용되는 패턴은 간단한 null 검사보다 더 정교하며 이 시리즈의 뒷부분에서 다룹니다.
호출 대상 할당, 추가 및 제거
이제 대리자 형식을 정의하고, 대리자 인스턴스를 선언하고, 대리자를 호출하는 방법을 알아봅니다. 그러나 실제로 메서드를 대리자에 연결하려면 어떻게 해야 할까요? 대리자 할당이 들어오는 위치입니다.
대리자를 사용하려면 메서드를 할당해야 합니다. 할당하는 메서드는 대리자 형식이 정의하는 것과 동일한 서명(동일한 매개 변수 및 반환 형식)을 가져야 합니다.
실제 예제를 살펴보겠습니다. 문자열 목록을 길이별로 정렬하려는 경우를 가정해 보겠습니다. 대리자 서명과 일치하는 비교 메서드를 Comparison<string> 만들어야 합니다.
private static int CompareLength(string left, string right) =>
left.Length.CompareTo(right.Length);
이 메서드는 두 개의 문자열을 사용하고 어떤 문자열이 "더 큼"(이 경우 더 길어지는지)을 나타내는 정수를 반환합니다. 메서드는 프라이빗으로 선언되어 완벽하게 괜찮습니다. 대리자와 함께 사용하기 위해 메서드를 공용 인터페이스의 일부로 만들 필요는 없습니다.
이제 이 메서드를 List.Sort() 메서드에 전달할 수 있습니다.
phrases.Sort(CompareLength);
괄호 없이 메서드 이름을 사용합니다. 그러면 메서드 참조를 나중에 호출할 수 있는 대리자로 변환하도록 컴파일러에 지시합니다.
Sort() 메서드는 두 문자열을 비교해야 할 때마다 CompareLength 메서드를 호출합니다.
대리자 변수를 선언하고 메서드를 할당하여 더 명시적일 수도 있습니다.
Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);
두 방법 모두 동일한 작업을 수행합니다. 첫 번째 방법은 더 간결하지만 두 번째 방법은 대리자 할당을 보다 명시적으로 만듭니다.
간단한 메서드의 경우 별도의 메서드를 정의하는 대신 람다 식을 사용하는 것이 일반적입니다.
Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);
람다 식은 간단한 메서드를 인라인으로 정의하는 간단한 방법을 제공합니다. 대리자 대상에 람다 식을 사용하는 방법은 이후 섹션에서 자세히 설명합니다.
지금까지의 예제에서는 단일 대상 메서드를 사용하는 대리자를 보여 줍니다. 그러나 대리자 개체는 단일 대리자 개체에 연결된 여러 대상 메서드가 있는 호출 목록을 지원할 수 있습니다. 이 기능은 이벤트 처리 시나리오에 특히 유용합니다.
Delegate 및 MulticastDelegate 클래스
백그라운드에서 사용했던 대리자 기능은 .NET Framework Delegate 의 두 가지 키 클래스를 기반으로 빌드됩니다 MulticastDelegate. 일반적으로 이러한 클래스를 직접 다루지는 않지만, 이 클래스들은 대리자가 작동하는 기반을 제공합니다.
System.Delegate 클래스와 해당 직접 서브클래스는 System.MulticastDelegate 대리자를 만들고, 메서드를 대리자 대상으로 등록하고, 대리자로 등록된 모든 메서드를 호출하기 위한 프레임워크 지원을 제공합니다.
다음은 흥미로운 디자인 세부 사항입니다. System.Delegate와 System.MulticastDelegate는 사용할 수 있는 대리자 형식이 아닙니다. 대신 사용자가 만드는 모든 특정 대리자 형식에 대한 기본 클래스 역할을 합니다. C# 언어를 사용하면 이러한 클래스에서 직접 상속할 수 없으므로 대신 키워드를 delegate 사용해야 합니다.
delegate 키워드를 사용하여 대리자 형식을 선언하면, C# 컴파일러는 여러분의 구체적인 서명을 가진 MulticastDelegate로부터 파생된 클래스를 자동으로 생성합니다.
왜 이 디자인인가요?
이 디자인은 C# 및 .NET의 첫 번째 릴리스에 해당합니다. 디자인 팀은 다음과 같은 몇 가지 목표를 가지고 있었습니다.
형식 안전: 팀은 대리인을 사용할 때 언어가 형식 안전을 적용하도록 하고 싶었습니다. 즉, 대리자가 올바른 형식과 인수 수를 사용하여 호출되고 반환 형식이 컴파일 시간에 올바르게 확인되도록 합니다.
성능: 컴파일러가 특정 메서드 서명을 나타내는 구체적인 대리자 클래스를 생성하게 함으로써 런타임은 대리자 호출을 최적화할 수 있습니다.
단순성: 제네릭이 도입되기 전인 1.0 .NET 릴리스에 대리자가 포함되었습니다. 시간의 제약 조건 내에서 작동하는 데 필요한 디자인입니다.
솔루션은 컴파일러가 메서드 서명과 일치하는 구체적인 대리자 클래스를 만들어 복잡성을 숨기면서 형식 안전성을 보장하는 것이었습니다.
대리자 메서드를 사용한 작업
파생 클래스를 직접 만들 수는 없지만 때때로 Delegate 클래스 및 MulticastDelegate 클래스에 정의된 메서드를 사용합니다. 다음은 알아야 할 가장 중요한 사항입니다.
함께 작업하는 모든 대리자는 MulticastDelegate에서 파생됩니다. "멀티캐스트" 대리자는 대리자를 통해 호출할 때 둘 이상의 메서드 대상을 호출할 수 있음을 의미합니다. 원래 디자인에서는 하나의 메서드만 호출할 수 있는 대리자와 여러 메서드를 호출할 수 있는 대리자를 구분하는 것을 고려했습니다. 실제로 이러한 구분은 원래 생각보다 덜 유용했기 때문에 .NET의 모든 대리자는 여러 대상 메서드를 지원합니다.
대리자를 사용할 때 가장 일반적으로 사용되는 메서드는 다음과 같습니다.
-
Invoke(): 대리자에 연결된 모든 메서드를 호출합니다. -
BeginInvoke()/EndInvoke(): 비동기 호출 패턴에 사용됩니다(async/await지금은 선호됨).
대부분의 경우 이러한 메서드를 직접 호출하지 않습니다. 대신 위의 예제와 같이 대리자 변수에 메서드 호출 구문을 사용합니다. 그러나 이 시리즈의 뒷부분에서 볼 수 있듯이 이러한 메서드와 직접 작동하는 패턴이 있습니다.
요약
이제 C# 언어 구문이 기본 .NET 클래스에 매핑되는 방식을 살펴보았으므로 더 복잡한 시나리오에서 강력한 형식의 대리자가 사용, 생성 및 호출되는 방법을 살펴볼 준비가 되었습니다.
.NET