C# 대리자, 무명 메서드, 람다 식 핵심 정리

1. 대리자(Delegate)의 본질

대리자는 사용자 정의 참조 형태로, delegate 키워드로 표기하며 반환값과 매개변수를 갖습니다. 실무에서는 동일한 시그니처를 가진 메서드들의 참조를 보관하고, 호출 시점에 일괄 실행하는 메커니즘으로 활용됩니다.

1.1 선언 규칙

  • 클래스와 동일한 수준에서 선언하며, 메서드 내부에서는 선언 불가
  • 접근 제한자는 클래스 선언과 동일하게 적용
  • 선언문은 반드시 세미콜론(;)으로 종료
  • 네이밍 컨벤션: Handler 접미사 사용 권장 (예: ClickHandler, DataReceivedHandler)
// 기본 선언 문법
[접근_수식어] delegate 반환_형식 대리자_이름(매개변수_목록);

// 예시
public delegate void LogHandler(string message);
internal delegate void ConnectionCallback();  // 매개변수 없을 때도 () 필수
public delegate TResult TransformHandler<TInput, TResult>(TInput source);

1.2 인스턴스화와 호출

모든 대리자는 암시적으로 System.Delegate를 상속받습니다. 인스턴스화는 메서드 참조를 연결하는 과정이며, 동기/비동기 실행 모두 지원합니다.

public delegate void NotifyHandler(string user, string content);

class NotificationService
{
    static void Main()
    {
        NotifyHandler notifier = SendEmail;
        notifier += SendPush;  // 멀티캐스트 등록
        
        // 동기 실행
        notifier("admin", "서버 점검 예정");
        
        // 명시적 동기 호출
        notifier.Invoke("admin", "서버 점검 예정");
        
        // 비동기 실행 (BeginInvoke/EndInvoke 패턴)
        notifier.BeginInvoke("admin", "긴급 알림", ar => {
            Console.WriteLine("비동기 처리 완료");
        }, null);
    }
    
    static void SendEmail(string recipient, string body) => 
        Console.WriteLine($"[Email] {recipient}: {body}");
    
    static void SendPush(string target, string message) => 
        Console.WriteLine($"[Push] {target}: {message}");
}

2. 범용 대리자: Func와 Action

.NET 3.5부터 표준화된 제네릭 대리자를 제공하여 직접 선언 없이 즉시 사용 가능합니다.

형식반환값용도
Action<T1, T2, ...>void부수효과 중심의 작업
Func<T1, T2, ..., TResult>TResult값을 계산하여 반환
// Action 활용
Action<string, int> repeatPrint = (text, count) => {
    for (int i = 0; i < count; i++) Console.WriteLine(text);
};

// Func 활용
Func<int, int, int> maxFinder = (left, right) => left > right ? left : right;

// 복합 예시
Func<string, bool> validator = input => !string.IsNullOrWhiteSpace(input);

3. 가변성(Variance) in 대리자

3.1 핵심 개념

C# 4.0부터 제네릭 인터페이스와 대리자에 공변성(Covariance)반공변성(Contravariance)을 명시적으로 지정할 수 있습니다. 이는 리스코프 치환 원칙(LSP)을 기반으로 한 타입 안전한 변환을 보장합니다.

  • out 키워드: 반환 타입에 적용 → 공변성 허용
  • in 키워드: 매개변수 타입에 적용 → 반공변성 허용

3.2 대리자에서의 적용

class Animal { }
class Dog : Animal { }
class Cat : Animal { }

class VarianceDemo
{
    // 공변성: 반환 타입의 범위를 넓힐 수 있음
    delegate Animal Factory<out T>();  // T는 반환 타입에만 사용
    
    // 반공변성: 매개변수 타입의 범위를 좁힐 수 있음  
    delegate void Consumer<in T>(T item);  // T는 입력 매개변수에만 사용

    static void Main()
    {
        // 공변성: Dog 팩토리를 Animal 팩토리로 참조
        Factory<Dog> dogMaker = () => new Dog();
        Factory<Animal> animalMaker = dogMaker;  // OK: Dog → Animal
        
        // 반공변성: Animal 소비자를 Dog 소비자로 참조
        Consumer<Animal> animalFeeder = (a) => Console.WriteLine(a.GetType().Name);
        Consumer<Dog> dogFeeder = animalFeeder;  // OK: Animal → Dog
        
        dogFeeder(new Dog());  // 실제로는 animalFeeder 실행
    }
}

4. 무명 메서드와 람다 식의 진화

4.1 단계별 발전 과정

// 1단계: 명명된 메서드 (C# 1.0)
bool IsPositive(int value) => value > 0;
Predicate<int> predicate1 = IsPositive;

// 2단계: 무명 메서드 (C# 2.0)
Predicate<int> predicate2 = delegate(int value) {
    return value > 0;
};

// 3단계: 람다 식 (C# 3.0+)
Predicate<int> predicate3 = (int value) => { return value > 0; };

// 4단계: 식 본문 람다 (가장 간결)
Predicate<int> predicate4 = value => value > 0;

4.2 람다 식의 캡처 메커니즘

class ClosureExample
{
    static void DemonstrateCapture()
    {
        int threshold = 50;
        var random = new Random();
        
        // 외부 변수 threshold를 캡처
        Func<int, bool> aboveThreshold = score => score > threshold;
        
        // 캡처된 변수 수정 시 주의: 람다 내에서도 영향받음
        var scores = Enumerable.Range(1, 10).Select(_ => random.Next(100));
        var filtered = scores.Where(aboveThreshold);
        
        threshold = 80;  // 람다 실행 전 값 변경
        Console.WriteLine(string.Join(", ", filtered));  // 80보다 큰 값만 출력
    }
}

5. 실무 활용 패턴

5.1 이벤트 기반 아키텍처

class EventBroker<TEvent> where TEvent : EventArgs
{
    private readonly Dictionary<string, List<Func<object, TEvent, Task>>> _handlers = new();

    public void Subscribe(string channel, Func<object, TEvent, Task> handler)
    {
        if (!_handlers.ContainsKey(channel))
            _handlers[channel] = new List<Func<object, TEvent, Task>>();
        
        _handlers[channel].Add(handler);
    }

    public async Task PublishAsync(string channel, object sender, TEvent eventData)
    {
        if (!_handlers.TryGetValue(channel, out var channelHandlers))
            return;
            
        var tasks = channelHandlers.Select(h => h(sender, eventData));
        await Task.WhenAll(tasks);
    }
}

// 사용
var broker = new EventBroker<FileSystemEventArgs>();
broker.Subscribe("file:changed", async (sender, e) => {
    await File.AppendAllTextAsync("log.txt", $"{e.FullPath} 변경됨\n");
});

5.2 전략 패턴의 함수형 구현

record Order(decimal Amount, string Category, bool IsVIP);

class DiscountEngine
{
    private readonly List<Func<Order, decimal?>> _strategies = new();

    public void AddRule(Func<Order, decimal?> calculator) => 
        _strategies.Add(calculator);

    public decimal CalculateFinalPrice(Order order)
    {
        var applicableDiscount = _strategies
            .Select(s => s(order))
            .Where(d => d.HasValue)
            .DefaultIfEmpty(0)
            .Max();
            
        return order.Amount * (1 - applicableDiscount.Value);
    }
}

// 구성
var engine = new DiscountEngine();
engine.AddRule(o => o.Amount > 10000 ? 0.1m : null);   // 금액 기반
engine.AddRule(o => o.IsVIP ? 0.05m : null);          // 등급 기반
engine.AddRule(o => o.Category == "BOOK" ? 0.03m : null); // 카테고리 기반

태그: C# Delegate lambda Func Action

6월 26일 03:19에 게시됨