델리게이트, 함수 포인터 가이드

C#에서 델리게이트(delegate)는 함수를 가리키는 참조 타입으로, C++의 함수 포인터와 유사하지만 타입 안전성과 객체 지향적 특성을 갖춘 강력한 도구입니다. 이 가이드에서는 델리게이트를 활용해 함수 포인터처럼 사용하는 방법을 단계별로 설명하며, 선언부터 고급 활용까지 다룹니다.


1. 델리게이트란?

델리게이트는 특정 시그니처(반환 타입과 매개변수)를 가진 메서드를 참조할 수 있는 타입입니다. 이를 통해 메서드를 변수처럼 전달하거나 호출할 수 있으며, 이벤트 처리, 콜백 함수, 비동기 프로그래밍 등에 유용하게 사용됩니다.


2. 델리게이트 선언하기

델리게이트를 사용하려면 먼저 타입을 정의해야 합니다. delegate 키워드를 사용하며, 반환 타입과 매개변수를 지정합니다.

// 매개변수가 없는 void 반환 델리게이트
public delegate void MyDelegate();

// 두 개의 int 매개변수를 받고 int를 반환하는 델리게이트
public delegate int MathOperation(int a, int b);

3. 델리게이트 인스턴스 생성하기

델리게이트 타입을 선언한 후, 인스턴스를 만들어 특정 메서드를 참조하게 합니다.

public class Example
{
    // 참조할 메서드 정의
    public void SayHello()
    {
        Console.WriteLine("안녕하세요!");
    }

    public static void Main()
    {
        Example ex = new Example();
        // 델리게이트 인스턴스 생성
        MyDelegate del = ex.SayHello;  // new MyDelegate() 생략 가능
    }
}

4. 델리게이트 호출하기

델리게이트 인스턴스를 생성하면 이를 호출해 참조된 메서드를 실행할 수 있습니다.

del();  // "안녕하세요!" 출력

5. 멀티캐스트 델리게이트

델리게이트는 여러 메서드를 동시에 참조할 수 있는 멀티캐스트 기능을 지원합니다. +=로 메서드를 추가하고, -=로 제거할 수 있습니다.

public class Example
{
    public void SayHello() => Console.WriteLine("안녕하세요!");
    public void SayGoodbye() => Console.WriteLine("안녕히 가세요!");

    public static void Main()
    {
        Example ex = new Example();
        MyDelegate del = ex.SayHello;
        del += ex.SayGoodbye;

        del();  // "안녕하세요!"와 "안녕히 가세요!" 순서대로 출력
    }
}

6. 익명 메서드와 람다 표현식 활용

별도의 메서드 정의 없이 익명 메서드람다 표현식으로 델리게이트를 간결하게 사용할 수 있습니다.

// 익명 메서드
MyDelegate del1 = delegate { Console.WriteLine("익명 메서드"); };

// 람다 표현식
MyDelegate del2 = () => Console.WriteLine("람다 표현식");

del1();  // "익명 메서드" 출력
del2();  // "람다 표현식" 출력

7. 제네릭 델리게이트 사용하기

C#에서는 Action, Func, Predicate 같은 제네릭 델리게이트를 제공해 별도의 선언 없이도 유연하게 사용할 수 있습니다.

  • Action<T>: 반환 값이 없는 메서드
  • Func<T, TResult>: 반환 값이 있는 메서드
  • Predicate<T>: bool 반환 메서드
// Action 예제
Action<string> print = s => Console.WriteLine(s);
print("안녕, Action!");  // "안녕, Action!" 출력

// Func 예제
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 5));  // 8 출력

8. 델리게이트와 이벤트 연결하기

델리게이트는 이벤트 핸들러로 자주 사용됩니다. 이벤트는 외부에서 직접 호출을 막고, 핸들러 추가/제거만 허용합니다.

public class Button
{
    public delegate void ClickHandler(object sender, EventArgs e);
    public event ClickHandler Click;

    public void OnClick()
    {
        Click?.Invoke(this, EventArgs.Empty);  // null 체크 후 호출
    }
}

Button btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("버튼 클릭됨!");
btn.OnClick();  // "버튼 클릭됨!" 출력

9. 비동기 프로그래밍과 델리게이트

과거에는 BeginInvokeEndInvoke로 비동기 호출을 했지만, .NET Core에서는 지원되지 않습니다. 현대적인 방식은 asyncawait를 사용하는 것입니다.

Func<Task<int>> asyncOp = async () =>
{
    await Task.Delay(1000);
    return 42;
};

int result = await asyncOp();
Console.WriteLine(result);  // 42 출력

10. 델리게이트 사용 시 주의사항

  • null 체크: 호출 전 ?.Invoke()로 null 여부를 확인하세요.
  • 멀티캐스트 반환 값: void가 아닌 경우 마지막 메서드의 반환 값만 반환됩니다.
  • 스레드 안전성: 멀티스레드 환경에서는 동기화가 필요할 수 있습니다.

요약

델리게이트는 C#에서 함수 포인터처럼 메서드를 참조하고 호출하는 강력한 도구입니다. 선언, 인스턴스 생성, 호출을 기본으로, 멀티캐스트, 람다 표현식, 제네릭 델리게이트 등을 활용하면 코드를 유연하고 간결하게 작성할 수 있습니다. 이벤트 처리와 비동기 작업에서도 핵심 역할을 하며, 타입 안전성과 객체 지향적 특성 덕분에 유지보수성과 재사용성을 높일 수 있습니다.


이벤트와 델리게이트는 C#에서 객체 간 통신을 느슨하게 결합된 방식으로 처리할 수 있게 해주는 강력한 도구입니다. 아래 예제들은 다양한 상황에서 이를 활용하는 방법을 보여줍니다.


예제 8: 타이머 이벤트와 델리게이트

타이머가 주기적으로 이벤트를 발생시키고, 이를 구독한 핸들러가 실행되는 예제입니다.

using System;

public class Timer
{
    // 델리게이트 정의
    public delegate void TickHandler(object sender, EventArgs e);

    // 이벤트 선언
    public event TickHandler Tick;

    // 타이머 시작 메서드
    public void Start(int seconds)
    {
        for (int i = 0; i < seconds; i++)
        {
            System.Threading.Thread.Sleep(1000); // 1초 대기
            Tick?.Invoke(this, EventArgs.Empty); // 이벤트 발생
        }
    }
}

public class Program
{
    public static void Main()
    {
        Timer timer = new Timer();

        // 이벤트 핸들러 등록
        timer.Tick += (sender, e) => Console.WriteLine("틱! 1초가 지났습니다.");

        // 타이머 시작 (3초 동안 실행)
        timer.Start(3);
        // 출력:
        // 틱! 1초가 지났습니다.
        // 틱! 1초가 지났습니다.
        // 틱! 1초가 지났습니다.
    }
}

설명: Timer 클래스에서 Tick 이벤트를 정의하고, Start 메서드가 주기적으로 이벤트를 호출합니다. 구독자는 매초 메시지를 출력합니다.


예제 9: 이벤트에 조건 추가

특정 조건에서만 이벤트가 호출되도록 설정한 예제입니다.

using System;

public class Stock
{
    public delegate void PriceChangedHandler(string stockName, double newPrice);
    public event PriceChangedHandler PriceChanged;

    private double _price;
    public double Price
    {
        get => _price;
        set
        {
            if (_price != value && value > 100) // 가격이 100 이상일 때만 이벤트 발생
            {
                _price = value;
                PriceChanged?.Invoke("ABC 주식", _price);
            }
        }
    }
}

public class Program
{
    public static void Main()
    {
        Stock stock = new Stock();

        // 이벤트 핸들러 등록
        stock.PriceChanged += (name, price) => 
            Console.WriteLine($"{name}의 가격이 {price}로 변경되었습니다!");

        stock.Price = 50;   // 이벤트 발생 안 함 (100 미만)
        stock.Price = 150;  // "ABC 주식의 가격이 150로 변경되었습니다!" 출력
    }
}

설명: Stock 클래스에서 가격이 100을 초과할 때만 PriceChanged 이벤트를 발생시키며, 주식 이름과 새로운 가격을 핸들러에 전달합니다.


예제 10: 이벤트 발생자 식별

이벤트를 발생시킨 객체를 구체적으로 식별할 수 있도록 sender를 활용한 예제입니다.

using System;

public class Device
{
    public string Name { get; }
    public delegate void StatusChangedHandler(object sender, string status);
    public event StatusChangedHandler StatusChanged;

    public Device(string name)
    {
        Name = name;
    }

    public void ChangeStatus(string newStatus)
    {
        StatusChanged?.Invoke(this, newStatus);
    }
}

public class Program
{
    public static void Main()
    {
        Device device1 = new Device("장치1");
        Device device2 = new Device("장치2");

        // 이벤트 핸들러 등록
        device1.StatusChanged += (sender, status) => 
            Console.WriteLine($"{((Device)sender).Name} 상태: {status}");
        device2.StatusChanged += (sender, status) => 
            Console.WriteLine($"{((Device)sender).Name} 상태: {status}");

        device1.ChangeStatus("켜짐");  // "장치1 상태: 켜짐" 출력
        device2.ChangeStatus("꺼짐");  // "장치2 상태: 꺼짐" 출력
    }
}

설명: sender를 통해 이벤트를 발생시킨 Device 객체를 식별하고, 해당 객체의 Name 속성을 출력합니다.


예제 11: 이벤트와 상태 패턴 결합

상태 변화를 이벤트로 알리는 예제입니다.

using System;

public enum TrafficLightState { Red, Yellow, Green }

public class TrafficLight
{
    public delegate void StateChangedHandler(TrafficLightState newState);
    public event StateChangedHandler StateChanged;

    private TrafficLightState _state;
    public TrafficLightState State
    {
        get => _state;
        set
        {
            if (_state != value)
            {
                _state = value;
                StateChanged?.Invoke(_state);
            }
        }
    }
}

public class Program
{
    public static void Main()
    {
        TrafficLight light = new TrafficLight();

        // 이벤트 핸들러 등록
        light.StateChanged += state => 
            Console.WriteLine($"신호등 상태: {state}");

        light.State = TrafficLightState.Red;    // "신호등 상태: Red" 출력
        light.State = TrafficLightState.Green;  // "신호등 상태: Green" 출력
        light.State = TrafficLightState.Yellow; // "신호등 상태: Yellow" 출력
    }
}

설명: TrafficLight 클래스에서 상태가 변경될 때마다 StateChanged 이벤트를 발생시켜 새로운 상태를 알립니다.


요약

위 예제들은 이벤트와 델리게이트를 활용한 다양한 시나리오를 다룹니다:

  • 타이머 이벤트: 주기적인 이벤트 발생.
  • 조건 기반 이벤트: 특정 조건에서만 이벤트 호출.
  • 발생자 식별: sender를 활용해 이벤트를 발생시킨 객체 식별.
  • 상태 패턴과의 결합: 상태 변화를 이벤트로 통지.

이처럼 이벤트와 델리게이트는 유연성과 확장성을 제공하며, 객체 간의 느슨한 결합을 유지하면서도 강력한 통신 메커니즘을 구현할 수 있게 해줍니다.

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다