Action, Func, Predicate

2024. 5. 21. 23:01·C#

1. 델리게이트 및 제네릭 델리게이트의 필요성

델리게이트는 특정 메서드 시그니처와 일치하는 모든 메서드를 참조할 수 있는 타입 세이프(Type-Safe) 함수 포인터입니다. 예를 들어, 정수 두 개를 받아 정수를 반환하는 메서드를 참조하려면 다음과 같이 커스텀 델리게이트를 선언해야 했습니다.

delegate int MyIntOperation(int a, int b);

마찬가지로, 문자열 하나를 받아 아무것도 반환하지 않는 메서드를 위해서는 또 다른 델리게이트 선언이 필요합니다.

delegate void MyStringProcessor(string message);

이처럼 다양한 시그니처에 대해 매번 delegate 키워드로 새로운 타입을 정의하는 것은 코드의 양을 늘리고 번거로움을 야기합니다. Action, Func, Predicate와 같은 내장 제네릭 델리게이트는 이러한 문제를 해결합니다. 제네릭을 활용하여 다양한 매개변수 개수와 반환 타입 유무에 따른 공통적인 패턴들을 미리 정의해 둠으로써, 개발자는 별도의 델리게이트 선언 없이 이들을 즉시 가져다 사용할 수 있습니다.


2. Action 델리게이트

Action 계열 델리게이트는 반환 값이 없는 (void) 메서드를 참조하기 위해 사용됩니다. 매개변수의 개수에 따라 여러 버전이 제네릭으로 제공됩니다.

  • 종류:

    • Action: 매개변수가 없고 반환 값도 없는 메서드를 참조합니다. (예: void MyMethod())
    • Action<T>: T 타입의 매개변수 하나를 받고 반환 값이 없는 메서드를 참조합니다. (예: void MyMethod(T arg))
    • Action<T1, T2>: T1, T2 타입의 매개변수 두 개를 받고 반환 값이 없는 메서드를 참조합니다. (예: void MyMethod(T1 arg1, T2 arg2))
    • ...
    • Action<T1, ..., T16>: 최대 16개의 매개변수를 받고 반환 값이 없는 메서드를 참조합니다.
  • 주요 용도: 어떤 동작이나 절차를 수행하는 콜백 함수, 이벤트 핸들러, 특정 조건 만족 시 실행될 로직 등을 캡슐화하는 데 주로 사용됩니다.

[예시 코드]

using System;
using UnityEngine;

public class ActionExample : MonoBehaviour
{
    void Start()
    {
        // 1. 매개변수 없는 Action
        Action simpleAction = PrintHello; // 메서드 할당
        simpleAction += () => Debug.Log("Hello from Lambda!"); // 람다식 추가 할당 (멀티캐스트)
        simpleAction(); // 할당된 모든 메서드 호출

        // 2. 매개변수 있는 Action
        Action<string> printMessage = DisplayMessage; // 메서드 할당
        printMessage("This is a message.");

        Action<int, int> calculateSum = (a, b) => // 람다식으로 직접 정의
        {
            int sum = a + b;
            Debug.Log($"{a} + {b} = {sum}");
        };
        calculateSum(10, 5);
    }

    void PrintHello()
    {
        Debug.Log("Hello from PrintHello method!");
    }

    void DisplayMessage(string msg)
    {
        Debug.Log("Received message: " + msg);
    }
}

3. Func<T, TResult> 델리게이트

Func 계열 델리게이트는 반환 값이 있는 메서드를 참조하기 위해 사용됩니다. 마지막 제네릭 타입 인수가 항상 메서드의 반환 타입(TResult) 을 나타냅니다.

  • 종류:

    • Func<TResult>: 매개변수가 없고 TResult 타입의 값을 반환하는 메서드를 참조합니다. (예: TResult MyMethod())
    • Func<T, TResult>: T 타입의 매개변수 하나를 받고 TResult 타입의 값을 반환하는 메서드를 참조합니다. (예: TResult MyMethod(T arg))
    • Func<T1, T2, TResult>: T1, T2 타입의 매개변수 두 개를 받고 TResult 타입의 값을 반환하는 메서드를 참조합니다. (예: TResult MyMethod(T1 arg1, T2 arg2))
    • ...
    • Func<T1, ..., T16, TResult>: 최대 16개의 매개변수를 받고 TResult 타입의 값을 반환하는 메서드를 참조합니다.
  • 주요 용도: 특정 계산을 수행하고 결과를 반환하는 메서드, 데이터를 변환하는 함수, 조건에 따라 다른 값을 반환하는 로직 등을 캡슐화하는 데 사용됩니다. 특히 LINQ(Language Integrated Query)에서 광범위하게 활용됩니다.

[예시 코드]

using System;
using UnityEngine;

public class FuncExample : MonoBehaviour
{
    void Start()
    {
        // 1. 매개변수 없고 반환값 있는 Func
        Func<int> getRandomNumber = () => UnityEngine.Random.Range(1, 101); // 람다식 사용
        int randomNumber = getRandomNumber();
        Debug.Log("Random number: " + randomNumber);

        // 2. 매개변수 있고 반환값 있는 Func
        Func<float, float> square = CalculateSquare; // 메서드 할당
        float result = square(5.0f);
        Debug.Log("Square of 5.0: " + result);

        Func<string, int, bool> checkStringLength = (str, length) => str.Length > length; // 람다식 사용
        bool isLong = checkStringLength("Hello World", 10);
        Debug.Log("'Hello World' length is greater than 10: " + isLong);
    }

    float CalculateSquare(float number)
    {
        return number * number;
    }
}

4. Predicate 델리게이트

Predicate<T> 델리게이트는 특별한 형태의 Func로, 하나의 매개변수를 받아 bool 값을 반환하는 메서드를 참조합니다. 이는 주로 어떤 조건을 검사(Predicate) 하는 목적의 메서드를 나타내기 위해 사용됩니다. 시그니처는 Func<T, bool>과 완전히 동일하지만, "조건 판별"이라는 의미를 명확히 전달하기 위해 별도로 정의되었습니다.

  • 종류: Predicate<T>: T 타입의 매개변수 하나를 받고 bool 값을 반환합니다. (예: bool MyCondition(T arg))

  • 주요 용도: 컬렉션(List<T> 등)에서 특정 조건을 만족하는 요소를 검색하거나 필터링하는 메서드에 조건 판별 로직을 전달하는 데 매우 유용합니다. (예: List<T>.Find(), List<T>.FindAll(), List<T>.Exists(), List<T>.RemoveAll())

[예시 코드]

using System;
using System.Collections.Generic;
using UnityEngine;

public class PredicateExample : MonoBehaviour
{
    void Start()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // Predicate<T> 정의 (메서드 할당)
        Predicate<int> isEvenPredicate = IsEven;

        // Predicate<T> 정의 (람다식 할당)
        Predicate<int> isGreaterThanFive = (n) => n > 5;

        // List<T>.Find() 메서드에 Predicate 사용
        int firstEvenNumber = numbers.Find(isEvenPredicate); // 첫 번째 짝수 찾기
        Debug.Log("First even number: " + firstEvenNumber); // 출력: 2

        // List<T>.FindAll() 메서드에 Predicate 사용 (람다식 직접 전달)
        List<int> oddNumbers = numbers.FindAll(n => n % 2 != 0); // 모든 홀수 찾기
        Debug.Log("Odd numbers:");
        foreach (int odd in oddNumbers)
        {
            Debug.Log("- " + odd);
        }

        // List<T>.Exists() 메서드에 Predicate 사용
        bool existsGreaterThanFive = numbers.Exists(isGreaterThanFive); // 5보다 큰 수가 있는지 확인
        Debug.Log("Exists number greater than 5: " + existsGreaterThanFive); // 출력: true
    }

    // Predicate<int> 시그니처와 일치하는 메서드
    bool IsEven(int number)
    {
        return number % 2 == 0;
    }
}

5. 람다 표현식(Lambda Expressions)과 함께 사용

Action, Func, Predicate 델리게이트는 람다 표현식(=>) 과 함께 사용될 때 그 진가를 발휘합니다. 람다 표현식을 사용하면 별도의 메서드를 정의하지 않고도 델리게이트가 참조할 익명 함수(Anonymous Function)를 코드 내에 간결하게 직접 작성할 수 있습니다.

[람다 표현식 기본 구문]

(매개변수 목록) => { 실행 코드 블록 }
// 또는 실행 코드가 한 줄이면 중괄호와 return(Func의 경우) 생략 가능
(매개변수 목록) => 단일 실행문 또는 표현식

[델리게이트와 람다 표현식 결합 예시]

// Action
Action logTime = () => Debug.Log(DateTime.Now);
Action<string, Color> logColored = (message, color) => Debug.Log($"<color=#{ColorUtility.ToHtmlStringRGB(color)}>{message}</color>");

// Func
Func<float, float, float> multiply = (x, y) => x * y;
Func<GameObject, bool> isActive = (go) => go.activeSelf;

// Predicate
Predicate<Collider> isPlayer = (col) => col.CompareTag("Player");

람다 표현식은 코드를 훨씬 간결하고 읽기 쉽게 만들어주므로, 내장 제네릭 델리게이트와 함께 적극적으로 활용하는 것이 좋습니다.


6. Unity에서의 활용 사례

Action, Func, Predicate는 Unity 개발의 다양한 영역에서 활용됩니다.

  • 이벤트 처리:

    • UnityEngine.UI.Button.onClick.AddListener(() => { /* 클릭 시 실행될 람다식 */ }); (내부적으로는 UnityAction 사용하지만 Action과 유사)
    • 커스텀 이벤트 시스템 구현 시 public static event Action OnPlayerDied; 와 같이 사용. OnPlayerDied?.Invoke();로 호출.
    • 특정 이벤트 발생 시 데이터를 함께 전달: public static event Action<int> OnScoreChanged; OnScoreChanged?.Invoke(newScore);
  • 콜백 함수:

    • 비동기 작업 완료 후 실행될 로직 전달: StartCoroutine(LoadAssetAsync("path", (loadedAsset) => { /* 로드 완료 후 처리 */ })); (Action<Object> 형태의 콜백)
    • 애니메이션(예: DoTween) 완료 콜백: transform.DOMoveX(5, 1.0f).OnComplete(() => Debug.Log("Movement Complete!")); (Action 형태의 콜백)
    • 타이머 만료 후 실행될 함수: InvokeRepeating("MyMethod", delay, repeatRate); 대신 StartCoroutine(Timer(delay, () => { /* 시간 만료 후 실행 */ })); (Action 콜백 사용)
  • LINQ 사용: 컬렉션 데이터 처리 시 LINQ 메서드에 람다식 형태로 Func나 Predicate를 전달.

      using System.Linq; // LINQ 사용 위해 필요
    
      List<Enemy> enemies = FindObjectsOfType<Enemy>().ToList();
      // 체력이 50 이상인 적만 필터링 (Where 메서드는 Func<Enemy, bool>을 인자로 받음)
      var highHealthEnemies = enemies.Where(enemy => enemy.CurrentHealth >= 50);
      // 적들의 이름만 선택 (Select 메서드는 Func<Enemy, string>을 인자로 받음)
      var enemyNames = enemies.Select(enemy => enemy.name);
  • List<T> 등의 컬렉션 메서드 활용: 앞선 PredicateExample에서 보듯 Find, FindAll, RemoveAll, Exists, ForEach 등 다양한 메서드에 활용.

  • UI Toolkit (UITK): UI 요소의 이벤트 콜백 등록 시 RegisterCallback<TEventType>((evt) => { /* 이벤트 처리 로직 */ }); 형태로 많이 사용.

  • 커스텀 에디터 스크립트: 에디터 창의 버튼 클릭 시 실행될 동작 등을 Action으로 정의하여 연결.


7. 선택 가이드 및 요약

어떤 내장 제네릭 델리게이트를 사용해야 할지 결정하는 것은 간단합니다.

목적 사용할 델리게이트 계열 예시 시그니처 (괄호 안은 예시)
반환 값이 없는 메서드 참조 Action 또는 Action<T...> void (), void (int), void (string, float)
반환 값이 있는 메서드 참조 Func<..., TResult> int (), string (int), bool (float, float)
하나의 인자를 받아 bool 조건 검사 Predicate<T> (또는 Func<T, bool>) bool (GameObject), bool (float)

결론

Action, Func, Predicate는 C#에서 제공하는 매우 유용하고 기본적인 내장 제네릭 델리게이트입니다. 이들은 다양한 메서드 시그니처에 대해 별도의 커스텀 델리게이트를 선언할 필요성을 없애주어 코드의 간결성과 표준성을 크게 향상시킵니다. 특히 람다 표현식과 결합될 때 그 가독성과 편의성은 극대화됩니다. Unity 개발에서 콜백 함수, 이벤트 처리, LINQ, 컬렉션 조작 등 수많은 시나리오에서 핵심적인 역할을 수행하므로, 이 세 가지 델리게이트의 개념과 사용법을 명확히 이해하고 능숙하게 활용하는 것은 현대적인 Unity C# 개발자에게 필수적인 역량입니다. 이들을 적재적소에 활용함으로써 더욱 깔끔하고, 효율적이며, 표현력이 풍부한 코드를 작성할 수 있을 것입니다.


참고 자료

  • Microsoft C# 공식 문서 - Action 대리자 (및 다른 Action 버전들)
  • Microsoft C# 공식 문서 - Func<T,TResult> 대리자 (및 다른 Func 버전들)
  • Microsoft C# 공식 문서 - Predicate 대리자
  • Microsoft C# 공식 문서 - 람다 식 (=> 연산자)
  • System 네임스페이스 (Action, Func, Predicate가 포함된 네임스페이스)
'C#' 카테고리의 다른 글
  • 이벤트
  • 타입 추론과 동적 타입
  • 델리게이트
  • 제네릭
뇌장하드 주인장
뇌장하드 주인장
  • 뇌장하드 주인장
    뇌장하드
    뇌장하드 주인장
    • 분류 전체보기 (86)
      • C++ (9)
      • C# (15)
      • CS (9)
      • 유니티 (38)
      • 유니팁 (7)
      • 유니티 패턴 (8)
      • - (0)
  • 블로그 메뉴

    • 홈
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
뇌장하드 주인장
Action, Func, Predicate
상단으로

티스토리툴바