1. 소개
재밌는 사실 아세요? 람다식 자체는 냄비 없는 레시피 같아요. 뭘 할지 설명하지만 코드 안에서 살아가기 위한 '컨테이너'가 필요합니다. C#에는 이를 위한 범용 델리게이트 타입이 이미 준비되어 있어요: Func, Action, Predicate.
이걸 람다용 틀이라고 생각하세요 — 필요한 걸 골라 로직을 부어 넣으면 됩니다. 이미 필요한 게 있는데 굳이 직접 델리게이트 타입 만들어서 시간 낭비할 필요 없어요.
약간의 역사
처음에는 함수를 파라미터로 넘기려면 매번 자체 델리게이트 타입을 선언해야 했습니다. 느리고 귀찮았고 대략 이렇게 생겼죠:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
C#에 Func, Action, Predicate가 도입되면서, 대부분의 경우 직접 델리게이트를 선언할 필요가 사라졌습니다. 지금은 훨씬 간단하고 범용적으로 쓸 수 있죠:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — 반환값이 있는 함수
문법과 용도
Func는 제네릭 델리게이트로, 0개에서 16개까지의 파라미터를 받고 값을 반환합니다.
Func<int, int, int> sum = (x, y) => x + y;
Func<타입1, 타입2, ..., 타입N, TResult> — 마지막을 제외한 모든 타입은 인수 타입이고, 마지막 타입이 반환 타입입니다.
예제들
1. 두 수의 합
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. 숫자 제곱
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. 파라미터 없음
Func<string> greet = () => "안녕, 람다!";
Console.WriteLine(greet());
시각적 도식
| 시그니처 | 예제 | 설명 |
|---|---|---|
|
|
int를 받고 int를 반환 |
|
|
두 개의 int를 받아 int를 반환 |
|
|
아무 것도 받지 않고 문자열을 반환 |
실제 애플리케이션에서의 모습
예를 들어 미니 애플리케이션(가칭 "사용자 안내서")에서 숫자 리스트에 다양한 처리를 적용하고 싶을 수 있습니다. 예: 제곱하거나 고정된 수와 합을 구하기 등.
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — 반환이 없는 동작
Action은 뭔가를 수행하지만 값을 반환하지 않는 메서드용 범용 델리게이트입니다(예: 화면에 출력).
Action은 0개에서 16개의 파라미터를 받을 수 있지만 항상 void를 반환합니다.
예제
1. 화면에 출력
Action<string> print = text => Console.WriteLine("데이터: " + text);
print("안녕, 세상!");
2. 파라미터 없는 동작
Action greet = () => Console.WriteLine("어서 오세요!");
greet();
3. 여러 파라미터가 있는 동작
Action<int, int> showSum = (a, b) => Console.WriteLine($"합계: {a + b}");
showSum(2, 3); // 합계: 5
시각적 도식
| 시그니처 | 예제 | 설명 |
|---|---|---|
|
|
파라미터 없음, 반환 없음 |
|
|
하나의 파라미터 |
|
|
여러 파라미터 |
우리 앱에서
사용자 안내서에 모든 이름을 출력하는 메서드를 추가해볼게요:
List<string> names = new() { "안나", "보리스", "비카" };
Action<string> printName = name => Console.WriteLine("사용자: " + name);
names.ForEach(printName);
// 또는 이렇게: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — 참/거짓 판정
하나의 파라미터에 대해 true나 false만 반환하면 되는 함수가 필요할 때는 Predicate<T>를 사용하세요. 이건 한 파라미터를 받고 bool을 반환하는 델리게이트입니다.
Predicate는 간단히 말해 "불리언 체크가 필요해" 라는 목적의 래퍼입니다.
예제
1. 숫자가 5보다 큰지 검사
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. List<T>.Find와 함께 사용
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // Predicate<int> 사용
Console.WriteLine(found); // 7
3. 모두 성인인가?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll은 Predicate<int>를 받음
Func<T, bool>와의 차이점
사실상 둘은 많이 겹칩니다. Microsoft 문서에도 "Predicate<T>는 특별한 API를 위한 Func<T, bool>일 뿐"이라고 쓰여 있죠. 하지만 표준 라이브러리의 어떤 메서드는 정확히 Predicate를 요구하기도 합니다.
5. 람다식이 Func, Action, Predicate에 '들어맞는' 방식
람다식을 작성하면 C# 컴파일러가 분석합니다: "아, 이 형태가 요구되는 델리게이트와 맞네 — 대입 가능!" 이렇게요.
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
모든 곳에 람다! 하지만 내부적으로는 시그니처가 다른 세 가지 델리게이트가 존재합니다.
실제 코드 예제로 적용
List<User> users = new() {
new User("안나", 24),
new User("보리스", 17),
new User("비카", 31),
};
// 성인 사용자만 반환하는 함수 (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("성인 목록: " + string.Join(", ", adults.Select(u => u.Name)));
모든 사용자 이름을 Action<User>으로 출력하고 싶다면:
users.ForEach(user => Console.WriteLine(user.Name));
이름들을 얻고 싶다면 (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
비교용 표
| 델리게이트 | 시그니처 | 람다 예시 | 사용되는 곳 |
|---|---|---|---|
|
T → U | |
Select, 모든 변환 |
|
T → void | |
ForEach, 동작을 수행하는 메서드들 |
|
T → bool | |
Find, Exists, 필터 |
6. 내부 애플리케이션 예제: 단계별
작은 "사용자 안내서" 애플리케이션을 확장해봅시다. 우선 User 클래스가 있습니다:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — 사용자가 성인인지 검사
Func<User, bool> isAdult = user => user.Age >= 18;
LINQ에서 사용:
var adults = users.Where(isAdult);
2. Predicate<User> — 활성 사용자를 찾기
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — 사용자를 비활성화
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — 간단한 설명 얻기
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
이 모든 람다들은 '데이터 형태의 코드'로서 메서드에 전달하거나 변수에 저장하고, 조합할 수 있습니다.
7. 눈에 잘 띄지 않는 뉘앙스와 흔한 실수들
1. 람다식은 델리게이트의 시그니처와 일치해야 합니다. 시그니처가 맞지 않으면 컴파일 오류가 납니다.
Func<int, string> wrong = x => x * 2; // 오류: string이 예상되지만 int가 반환됨
// 올바른 예:
Func<int, string> right = x => (x * 2).ToString();
2. void과 return을 헷갈리지 말 것. Action은 값을 반환하지 않습니다 — 예를 들어 Action<int> a = x => x * x; 같은 시도는 동작하지 않습니다. 반환값이 있는데 반환하면 안 되기 때문이죠.
3. Predicate<T>와 Func<T, bool>는 자주 서로 대체 가능하지만 항상 그런 건 아닙니다. 어떤 컬렉션 메서드는 정확히 Predicate<T>를 요구하고, 어떤 건 Func<T, bool>를 요구합니다. 직접 대입이 안 될 수 있습니다.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // 오류
// 하지만:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // 생성자를 통해 변환 가능
GO TO FULL VERSION