[C#] Delegate, Func, Action 이해하기: 차이점 및 사용 사례

C#의 Delegate, Func, Action은 C/C++의 함수 포인터와 비슷한 개념으로, 이벤트 처리 및 비동기 프로세스 처리 등을 위해 유연하고 재사용 가능한 코드를 작성하는 데 필수적인 요소다.

본 포스팅에서는 Delegate, Func, Action 각각의 개념, 차이점, 사용 사례에 대해 설명한다.

1. Delegate

Delegate는 특정 메서드 시그니처(입력 매개변수와 반환 타입)와 일치하는 메서드를 참조할 수 있는 타입이다. 이를 통해 함수를 변수처럼 다룰 수 있다.

델리게이트는 명시적으로 정의되어야 하며, 호출하려는 메서드와 일치하는 시그니처를 가져야 한다. 예를 들어, 두 개의 int 매개변수를 사용하고 float를 반환하는 메서드를 참조하는 델리게이트를 정의하려면 다음과 같이 사용할 수 있다.

public delegate float CustomDelegate(int a, int b);

public class MyClass
{
    public static float Divide(int a, int b)
    {
        return (float)a / b;
    }
}

public class MainClass
{
    public static void Main()
    {
        CustomDelegate del = MyClass.Divide;
        float result = del(10, 5);
        Console.WriteLine("Result: " + result);
    }
}

이 경우, CustomDelegate 델리게이트는 두 개의 int 입력 매개변수를 받아들이고 float 값을 반환하는 MyClass.Divide 메서드를 참조한다.

델리게이트는 메서드 참조를 저장하는 데 사용되며, 호출 시 해당 메서드를 실행한다. 델리게이트는 하나의 메서드뿐만 아니라 여러 개의 메서드를 가리킬 수 있다(멀티캐스팅 델리게이트).

public delegate void MyDelegate(string message);

public class Example
{
    public static void PrintMessage1(string message)
    {
        Console.WriteLine("Message 1: " + message);
    }

    public static void PrintMessage2(string message)
    {
        Console.WriteLine("Message 2: " + message);
    }

    public static void Main()
    {
        MyDelegate del = PrintMessage1;
        del += PrintMessage2; // 델리게이트에 두 번째 메서드 추가

        del("Hello, Multicast!"); // 두 메서드를 순차적으로 호출
    }
}

위 코드를 실행하면 출력은 다음과 같다.

Message 1: Hello, Multicast!
Message 2: Hello, Multicast!

2. Func

Func는 반환 타입이 void가 아닌 0~16개의 매개변수를 가진 함수를 나타내는 제네릭 델리게이트다. 제네릭(generic)이란 프로그래밍 언어에서 타입에 종속되지 않고, 재사용 가능한 코드를 작성하는 방법이다.

public class MyClass
{
    public static int GetNumber()
    {
        return 42;
    }

    public static string ToString(int number)
    {
        return "Number: " + number;
    }
}

public class MainClass
{
    public static void Main()
    {
        Func<int> numberFunc = MyClass.GetNumber;
        int number = numberFunc();
        Console.WriteLine("Number: " + number);

        Func<int, string> toStringFunc = MyClass.ToString;
        string result = toStringFunc(42);
        Console.WriteLine(result);
    }
}

이 경우, Func<int>는 매개변수가 없고 int 값을 반환하는 MyClass.GetNumber 메서드를 참조한다. 그리고 Func<int, string>은 하나의 int 매개변수를 받고 string을 반환하는 MyClass.ToString 메서드를 참조한다.

Func도 멀티캐스팅이 가능하지만, 반환 값을 가지기 때문에 마지막으로 추가된 메서드의 반환 값이 최종적으로 반환됨에 유의해야 한다.

using System;

public class Example
{
    public static int Add(int x, int y)
    {
        Console.WriteLine("Add: " + (x + y));
        return x + y;
    }

    public static int Multiply(int x, int y)
    {
        Console.WriteLine("Multiply: " + (x * y));
        return x * y;
    }

    public static void Main()
    {
        Func<int, int, int> func = Add;
        func += Multiply; // Func에 두 번째 메서드 추가

        int result = func(3, 4); // 두 메서드를 순차적으로 호출, 마지막 메서드의 반환 값이 최종 반환됨
        Console.WriteLine("Result: " + result);
    }
}

위 코드의 실행 결과는 다음과 같다.

Add: 7
Multiply: 12
Result: 12

3. Action

Action은 반환 타입이 void인 메소드를 위해 특별히 설계된 제네릭 델리게이트다. Action은 Func와 마찬가지로 사용자 정의 델리게이트 대신 사용할 수 있어 코드를 간소화하고 표현력을 높일 수 있다. 매개변수도 Func와 마찬가지로 0~16개를 가질 수 있다. Action도 멀티캐스팅이 가능하다.

public class MyClass
{
    public static void PrintHello()
    {
        Console.WriteLine("Hello!");
    }

    public static void PrintSum(float a, float b)
    {
        Console.WriteLine("Sum: " + (a + b));
    }
}

public class MainClass
{
    public static void Main()
    {
        Action helloAction = MyClass.PrintHello;
        helloAction();

        Action<float, float> sumAction = MyClass.PrintSum;
        sumAction(3.5f, 5.5f);
    }
}

이 경우, Action은 매개변수가 없는 MyClass.PrintHello 메서드를 참조한다. 그리고 Action<float, float>는 두 개의 float 매개변수를 받는 MyClass.PrintSum 메서드를 참조한다.

🤔
Func, Action보다 Delegate를 사용하는 것이 좋은 경우
이벤트 핸들러 등의 경우 특정 시그니처가 요구되는데(특정한 형식의 매개변수를 받고, 특정한 형식의 값을 반환해야 하는데), 이 경우 해당 시그니처를 명확히 표현하는 Delegate를 사용하는 것이 더 명확하고 직관적이다. 예를 들어, 클릭 이벤트 핸들러는 (object sender, EventArgs e) 형식을 받아야 한다. (Action으로 대체가 가능은 하다.)
// Delegate 사용
public delegate void ClickEventHandler(object sender, EventArgs e);

class Program
{
    public static event ClickEventHandler ClickEvent;

    static void Main()
    {
        ClickEvent += OnClick;
        ClickEvent?.Invoke(null, EventArgs.Empty);
    }

    static void OnClick(object sender, EventArgs e)
    {
        Console.WriteLine("Clicked");
    }
}
// Action 사용
class Program
{
    public static event Action<object, EventArgs> ClickEvent;

    static void Main()
    {
        ClickEvent += OnClick;
        ClickEvent?.Invoke(null, EventArgs.Empty);
    }

    static void OnClick(object sender, EventArgs e)
    {
        Console.WriteLine("Clicked");
    }
}

Summary

  • Delegate: 특정 메서드 시그니처(입력 매개변수와 반환 타입)와 일치하는 메서드를 참조할 수 있는 타입이다.
  • Func: 반환 값이 있는 메서드를 참조하는 제네릭 델리게이트다. 마지막 타입 매개 변수는 반환 타입을 나타내며, 나머지 매개 변수는 입력 매개 변수의 타입이다.
  • Action: 반환 값이 없는(void) 메서드를 참조하는 제네릭 델리게이트다. 모든 타입 매개 변수는 입력 매개 변수의 타입을 나타낸다.
  • 모두 멀티캐스팅이 가능하나, 반환값이 있는 Delegate/Func를 여러개 캐스팅한 경우 그 Delegate/Func의 반환값은 멀티캐스팅 시 마지막으로 추가된 메서드의 반환값이 된다.

C# Delegate, Func, Action은 코드의 유연성과 재사용성을 높이는 데 도움이 되는 도구이다. 이들의 개념과 차이점을 이해하고 적절한 상황에 사용하면 효율적인 코드를 작성할 수 있다.

이 글을 통해 각각의 개념과 사용 사례에 대해 더 잘 이해할 수 있기를 바란다.