1. 도입
커피 머신이 하나 있다고 상상해보세요. 보통은 커피만 내리고 아무에게도 간섭하지 않지만, 가끔 상사가 말하죠: "커피가 다 되면 '완료!'라고 소리 좀 질러줄래?" — 그럴 때 설정이 필요합니다. 커피 머신 본체를 고치지 않습니다. 그냥 마지막에 실행할 함수를 만들어서 전달하면 됩니다.
C#에서는 이 역할을 델리게이트가 합니다 — 델리게이트는 메서드에 코드 조각(메서드, 람다 또는 익명 메서드)을 전달할 수 있게 해주어, 해당 메서드가 적절한 시점에 그것을 호출하게 합니다. 간단히 말해, 델리게이트는 특정 시그니처를 가진 메서드에 대한 참조를 저장할 수 있는 타입입니다.
델리게이트 정의
C#에서 델리게이트는 delegate 키워드로 정의합니다. 예:
// int을 받아 bool을 반환하는 메서드를 가리키는 델리게이트
public delegate bool PredicateInt(int x);
이제 PredicateInt 타입의 변수는 하나의 int를 받고 bool을 반환하는 어떤 메서드(또는 람다)든 참조할 수 있습니다.
델리게이트가 필요한 이유
- 로직을 인수로 전달하기(예: 정렬, 필터링, 이벤트 처리)
- 이벤트 구독(나중에 다룰 예정)
- 콜백(callback) 구현
- 호출자 쪽에서 일부 동작을 정의할 수 있는 유연한 API
간단한 시각적 도식
| 델리게이트 타입 | 시그니처 | 호출 예 |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. 람다가 델리게이트로 변하는 방식
문법
예를 들어 x => x > 5 같은 람다 식을 작성하면, 본질적으로 델리게이트 객체를 만드는 겁니다. 람다는 진공 상태로 존재하지 않습니다: 반드시 타입이 필요합니다(파라미터와 반환 타입을 누가 알아야 하죠). 그래서 C#의 람다 식은 항상 암묵적이거나 명시적으로 델리게이트로 변환됩니다.
예제 1: 델리게이트를 메서드와 바인딩
// 델리게이트를 명시적으로 정의
public delegate bool MyPredicate(int number);
class Program
{
static void Main()
{
// MyPredicate 타입 변수에 람다를 할당
MyPredicate isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // true
Console.WriteLine(isEven(7)); // false
}
}
예제 2: 표준 델리게이트 사용
C#에는 제네릭 표준 델리게이트들이 있습니다: Action, Func<>, Predicate<>. 람다를 쓸 때 거의 모든 곳에서 이걸 사용합니다.
// Func
사용 Func
isPositive = number => number > 0; Console.WriteLine(isPositive(-5)); // false
3. 표준 델리게이트: Func, Action, Predicate
Func<...>
무언가를 받고 무언가를 반환하는 메서드에 사용합니다.
시그니처:
— 마지막 타입이 반환값이고, 그 앞의 타입들이 파라미터 타입입니다. 예를 들어:
Func<int, string> — int를 받고 string을 반환합니다.
Func
intToString = number => "숫자: " + number; Console.WriteLine(intToString(7)); // "숫자: 7"
Action<...>
무언가를 수행하지만 반환할 필요가 없을 때(void).
Action
printHello = name => Console.WriteLine("안녕, " + name + "!"); printHello("바실리"); // "안녕, 바실리!"
Predicate<T>
사실상 Func<T, bool>의 축약형입니다. 객체에 대한 논리적 검사(true/false)가 필요할 때 사용합니다.
Predicate
isOdd = x => x % 2 != 0; Console.WriteLine(isOdd(3)); // true
시각적 요약
| 델리게이트 | 시그니처 | 용도 |
|---|---|---|
|
|
변환, 프로젝션 |
|
|
사이드 이펙트, 출력 |
|
|
필터링, 검색 |
람다 식에 어떤 델리게이트 타입을 선택할까?
- 반환값이 필요하면 Func<...>
- 메서드가 아무 것도 반환하지 않으면(void) Action<...>
- 조건 검사가 필요하면 Predicate<T>
예: 리스트 필터링
List
numbers = new List
{ 1, 2, 3, 4, 5, 6 }; // Predicate
를 기대함 List
evenNumbers = numbers.FindAll(x => x % 2 == 0); Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
4. 유용한 뉘앙스
람다 식과 컬렉션 메서드: 내부에서 어떻게 동작하나?
예를 들어 컬렉션 메서드에 람다를 넘기면:
var adults = users.Where(u => u.Age >= 18);
메서드 Where는 Func<T, bool> 타입의 인수를 기대합니다. 즉, 여러분의 람다 u => u.Age >= 18은 컴파일러에 의해 해당 타입의 델리게이트 객체로 변환됩니다.
흐름도: 어떻게 동작하는가
여러분의 람다 --> C# 컴파일러 --> 델리게이트 객체 (Func
) (u => u.Age >= 18) [타입이 알려짐] (Where()에서 호출 가능)
타이핑 상세: 타입 추론
대부분의 경우 델리게이트 타입은 문맥에서 컴파일러가 자동으로 추론합니다. 예를 들어, List<T>.Find 메서드는 Predicate<T>를 기대하므로 컴파일러는 파라미터 타입을 시그니처에서 알 수 있습니다.
List
words = new List
{ "one", "two", "three" }; var result = words.Find(word => word.Length == 5); // Find는 Predicate
을 기대함 Console.WriteLine(result); // "three"
문맥이 불명확하면 컴파일러를 도와줘야 합니다:
// 타입을 명시적으로 적어줌
Func
check = x => x > 10;
델리게이트를 반환하기: 함수 팩토리
가끔 메서드가 델리게이트를 반환할 수 있습니다 — 즉 "함수 공장"을 만드는 것과 같죠. 동적 동작을 생성할 때 유용합니다.
// 델리게이트(람다)를 반환하는 함수
Func
GetMultiplier(int factor) { return x => x * factor; } var times3 = GetMultiplier(3); Console.WriteLine(times3(5)); // 15
이게 가능한 이유는 람다(x => x * factor)가 외부 컨텍스트의 변수 factor를 캡처(closure/클로저)하고, 그 람다가 Func<int, int> 타입의 객체로 반환되기 때문입니다.
5. 델리게이트와 람다에서 발생하는 실수와 오해
시그니처 불일치
파라미터나 반환 타입이 맞지 않으면 컴파일러는 람다를 델리게이트에 할당하지 않습니다.
Func
f = x => "문자열을 반환할 수 없음!"; // 컴파일 오류
델리게이트 없이 람다를 사용하려 할 때의 오류
타입 없이 그냥 람다를 적고 호출하려 하면 안 됩니다:
// 이렇게는 안 됨 - 컴파일러가 타입을 추론할 수 없음
// var myFunc = x => x * 2; // 오류 CS0815
// myFunc(10);
동작시키려면 타입을 명시하거나 문맥을 제공해야 합니다:
Func
myFunc = x => x * 2; Console.WriteLine(myFunc(10)); // 20
Action, Func, Predicate 혼동
가끔 잘못된 델리게이트 타입을 골라 시그니처 불일치가 나옵니다. 간단한 규칙을 기억하세요: 결과가 있으면 Func, 결과가 없으면(void) Action, 논리적 응답이 필요하면 Predicate<T>.
GO TO FULL VERSION