CodeGym /행동 /C# SELF /Func, Action, Predicate와 함께하는 람다 식

Func, Action, Predicate와 함께하는 람다 식

C# SELF
레벨 50 , 레슨 1
사용 가능

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());

시각적 도식

시그니처 예제 설명
Func<int, int>
x => x * 2
int를 받고 int를 반환
Func<int, int, int>
(a, b) => a + b
두 개의 int를 받아 int를 반환
Func<string>
() => "hi"
아무 것도 받지 않고 문자열을 반환

실제 애플리케이션에서의 모습

예를 들어 미니 애플리케이션(가칭 "사용자 안내서")에서 숫자 리스트에 다양한 처리를 적용하고 싶을 수 있습니다. 예: 제곱하거나 고정된 수와 합을 구하기 등.

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은 뭔가를 수행하지만 값을 반환하지 않는 메서드용 범용 델리게이트입니다(예: 화면에 출력).

Action0개에서 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

시각적 도식

시그니처 예제 설명
Action
() => ...
파라미터 없음, 반환 없음
Action<int>
x => ...
하나의 파라미터
Action<int, string>
(x, s) => ...
여러 파라미터

우리 앱에서

사용자 안내서에 모든 이름을 출력하는 메서드를 추가해볼게요:

List<string> names = new() { "안나", "보리스", "비카" };
Action<string> printName = name => Console.WriteLine("사용자: " + name);

names.ForEach(printName);
// 또는 이렇게: names.ForEach(name => Console.WriteLine(name));

4. Predicate<T> — 참/거짓 판정

하나의 파라미터에 대해 truefalse만 반환하면 되는 함수가 필요할 때는 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);

비교용 표

델리게이트 시그니처 람다 예시 사용되는 곳
Func<T, U>
T → U
user => user.Name
Select, 모든 변환
Action<T>
T → void
user => Console.WriteLine(...)
ForEach, 동작을 수행하는 메서드들
Predicate<T>
T → bool
user => user.Age > 18
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. voidreturn을 헷갈리지 말 것. 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); // 생성자를 통해 변환 가능
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION