1. 소개
지금까지 본 것처럼 우리는 LINQ 메서드에 람다를 넘겨 사용했어. 그런데 C#의 메서드는 어떻게 함수(람다)를 받을 수 있다는 걸 알까?
간단히 말하면, 델리게이트는 함수용 인터페이스 같은 거야: 시그니처(매개변수 타입과 반환 타입)를 정의하면, 그 시그니처와 맞는 모든 메서드(람다 포함)를 델리게이트를 기대하는 곳에 넘길 수 있어.
문자열을 파라미터로 넘겼던 걸 기억해? 로직을 넘기는 것도 비슷한데, 파라미터 타입이 델리게이트일 뿐이야.
델리게이트: 쉽게 풀어쓴 기본 이론
C#에서 델리게이트는 "이런 시그니처를 가진 함수"를 설명하는 타입이야.
// int를 받고 bool을 반환하는 델리게이트
public delegate bool IntPredicate(int x);
시그니처가 맞는 어떤 함수든 이 타입의 변수에 할당할 수 있어:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
그리고 이렇게 람다도 할당돼:
IntPredicate pred = x => x % 2 == 0;
제네릭 델리게이트: Func, Action, Predicate
- Func<T1, ..., TResult> — T1, ... 같은 파라미터를 받고 TResult를 반환하는 함수.
- Action<T1, ...> — 파라미터는 받지만 값을 반환하지 않는 함수(void).
- Predicate<T> — T를 받고 bool을 반환하는 함수.
2. 람다를 내 메서드에 전달하기
예를 들어 우리가 학습용 미니 프로젝트 — 사용자 리스트를 다루는 콘솔 앱 — 를 발전시키고 있다고 해보자. 이전엔 컬렉션을 LINQ로 필터링했지. 이제는 람다 조건을 받는 자체 메서드를 만들어 보자.
람다 파라미터가 있는 메서드 만들기
// 예시용 User 클래스 정의 (애플리케이션에 추가)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// 리스트와 델리게이트 조건(람다)을 받는 메서드
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // 람다를 호출!
result.Add(user);
}
return result;
}
이제 어떤 람다든 전달할 수 있어:
var users = new List<User>
{
new User { Name = "바샤", IsActive = true },
new User { Name = "페챠", IsActive = false },
new User { Name = "마샤", IsActive = true }
};
// 활성 사용자만 필터링
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // 바샤, 마샤
끝이야! 메서드 FilterUsers가 Predicate<User>를 기대하니까, 거기에 맞는 람다를 그냥 전달한 거야 — 작은 로직 조각을 파라미터로 넘긴 셈이지.
Func<T, TResult>를 쓰는 경우
Predicate<T>는 조건(즉 bool 반환)이 필요할 때 좋아. 그런데 각 사용자에 대해 뭔가를 "계산"해서 모으고 싶다면?
// 각 요소에 함수를 적용해서 결과를 모으는 메서드
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
var result = new List<TResult>();
foreach (var user in users)
{
result.Add(selector(user));
}
return result;
}
사용 예:
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // ВАСЯ, ПЕТЯ, МАША
3. 유용한 팁
다양한 전달 형태
람다뿐 아니라 일반 메서드도 전달할 수 있어 — 시그니처만 맞으면 돼.
// 일반 메서드
static bool NameHasS(User user) => user.Name.Contains("с");
// 일반 메서드 전달:
var usersWithS = FilterUsers(users, NameHasS);
// 람다 전달
var usersWithA = FilterUsers(users, u => u.Name.Contains("а"));
옛 스타일의 익명 메서드도 가능한데(권장하진 않음):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("м"); });
요즘 스타일은 당연히 람다야!
LINQ에 람다 전달하기: 실제로 무슨 일이 일어나는가
var result = users.Where(u => u.IsActive).ToList();
내부적으로 Where는 Func<User, bool>를 받는다는 걸 알아두자. 그래서 Func<...>를 받는 어떤 메서드도 비슷하게 사용할 수 있어.
두 개의 파라미터가 필요하면?
// 필터용으로 두 개의 람다를 받는 메서드
public static List<User> FilterUsersCustom(
List<User> users,
Func<User, bool> include,
Func<User, bool> exclude)
{
var result = new List<User>();
foreach (var user in users)
{
if (include(user) && !exclude(user))
result.Add(user);
}
return result;
}
사용 예:
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("В"),
u => u.IsActive == false
);
// 이름이 "В"로 시작하고 활성 상태인 사용자만 가져올 거야
시나리오: 필터 팩토리
Console.WriteLine("이름의 최소 길이를 입력하세요:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// 꽤 인터랙티브하고 생동감 있어!
4. 흔한 오류와 주의사항
컴파일러가 람다의 파라미터 타입을 "추론"하지 못하는 경우가 있어 — 특히 오버로드가 많거나 메서드가 특정 반환 타입/여러 파라미터를 요구할 때. 그럴 땐 람다의 타입을 명시해주면 돼:
FilterUsers(users, (User u) => u.Name.Length > 3);
또는 이렇게도 가능:
MapUsers(users, (User u) => u.Name.ToUpper());
오류: 람다가 시그니처에 맞지 않음
FilterUsers(users, user => Console.WriteLine(user.Name)); // 오류! bool이 예상되었는데 void가 나옴
이는 기대되는 함수가 bool을 반환하는데, 이 람다는 void를 반환(명시적으로 아무것도 반환하지 않음)하기 때문이야. 반환 타입을 항상 확인하자!
오류: 람다 남용
람다가 10줄 이상으로 길어지면 별도 메서드로 빼는 게 좋아. 가독성도 좋아지고 디버깅도 쉬워져.
GO TO FULL VERSION