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가 포함된 네임스페이스)