Lambda

Lambda

lambda
expression
Published

April 30, 2025

Abstract

람다 표현식(Lambda Expression)은 C# 3.0에서 도입된 간결한 함수 정의 방식으로, 익명 메소드를 더욱 간소화하여 코드의 가독성과 생산성을 높입니다. LINQ와 함께 도입되어 데이터 처리 작업을 효율적으로 수행할 수 있게 해주며, => 연산자를 사용하여 매개변수와 실행 코드를 명확히 구분합니다. 람다식은 델리게이트 타입이나 식 트리에 할당될 수 있어 함수형 프로그래밍 패러다임을 C#에 적용하는 데 중요한 역할을 합니다.

람다 표현식(Lambda Expression)이란?

람다식은 C# 3.0에서 LINQ(Language-Integrated Query)와 함께 도입되었습니다. 그 뿌리는 익명 메소드(Anonymous Method)에 있습니다.

  • 델리게이트(Delegate): C# 1.0부터 존재했던 델리게이트는 메소드를 참조할 수 있는 형식입니다. 하지만 델리게이트를 사용하려면 별도의 메소드를 선언해야 했습니다.
  • 익명 메소드(Anonymous Method, C# 2.0): 간단한 로직을 위해 별도의 메소드를 선언하는 번거로움을 줄이고자 도입되었습니다. delegate 키워드를 사용하여 코드 블록 내에서 직접 메소드 본문을 작성할 수 있게 했습니다.
delegate int Calculate(int a, int b);

// ...

// 익명 메소드 사용
Calculate calc = delegate (int a, int b)
{
    return a + b;
};
Console.WriteLine(calc(3, 5)); // 출력: 8
  • 람다식(Lambda Expression, C# 3.0): 익명 메소드를 더욱 간결하게 표현하기 위해 도입되었습니다. 특히 LINQ 쿼리에서 작은 함수 조각들을 인라인으로 정의하는 데 매우 유용합니다. 람다식은 => 연산자를 사용하여 입력 매개변수와 실행 코드를 구분합니다.
delegate int Calculate(int a, int b);

// ...

// 람다식 사용
Calculate calcLambda = (a, b) => a + b; // 익명 메소드보다 훨씬 간결
Console.WriteLine(calcLambda(3, 5)); // 출력: 8

결론적으로 람다식은 델리게이트와 익명 메소드의 개념 위에서 발전했으며, 코드를 더 짧고 읽기 쉽게 만드는 현대 C#의 핵심 기능으로 자리 잡았습니다.

처음 만나는 람다식

람다식은 이름이 없는 간단한 함수를 만드는 방법입니다. 주로 델리게이트 형식이나 식 트리(Expression Tree) 형식이 필요한 곳에 인라인 코드로 사용됩니다.

  • 기본 구조
(입력 매개변수) => { 실행 코드 }

또는 실행 코드가 단일 표현식일 경우 중괄호 {}return 문을 생략할 수 있습니다.

(입력 매개변수) => 표현식
  • => 연산자: ‘goes to’ 또는 ’람다 연산자’라고 읽으며, 왼쪽의 매개변수 목록과 오른쪽의 실행 코드를 구분합니다.
  • 매개변수:
    • 매개변수가 없으면 ()로 표시합니다. () => Console.WriteLine("Hello")
    • 매개변수가 하나이고 컴파일러가 타입을 추론할 수 있으면 괄호를 생략할 수 있습니다. x => x - x
    • 매개변수가 여러 개이면 쉼표로 구분합니다. (x, y) => x + y
    • 명시적으로 타입을 지정할 수도 있습니다. (int x, int y) => x + y
using System;

// 델리게이트 선언
delegate int SquareDelegate(int x);

public class LambdaIntro
{
    // 1. 이름 있는 메소드 사용 (전통 방식)
    static int SquareMethod(int x)
    {
        return x - x;
    }

    public static void Main(string[] args)
    {
        // 1. 이름 있는 메소드를 델리게이트에 할당
        SquareDelegate sqDelegate1 = SquareMethod;
        Console.WriteLine($"이름 있는 메소드: {sqDelegate1(5)}"); // 출력: 25

        // 2. 익명 메소드 사용 (C# 2.0)
        SquareDelegate sqDelegate2 = delegate (int x)
        {
            return x - x;
        };
        Console.WriteLine($"익명 메소드: {sqDelegate2(5)}"); // 출력: 25

        // 3. 람다식 사용 (C# 3.0) - 가장 간결
        SquareDelegate sqDelegate3 = x => x - x; // 타입 추론, 단일 표현식
        Console.WriteLine($"람다식: {sqDelegate3(5)}"); // 출력: 25

        // 매개변수가 없는 람다
        Action printHello = () => Console.WriteLine("Hello Lambda!");
        printHello(); // 출력: Hello Lambda!

        // 여러 매개변수를 가진 람다
        Func<int, int, int> add = (a, b) => a + b; // Func 대리자 사용 (14.4 참조)
        Console.WriteLine($"덧셈 람다: {add(3, 4)}"); // 출력: 7
    }
}

문 형식의 람다식

람다식의 본문이 단일 표현식(expression)이 아닌 여러 개의 문장(statement)으로 구성될 경우, 중괄호 {}를 사용하여 코드 블록을 만들어야 합니다. 이를 문 형식 람다(Statement Lambda)라고 합니다.

  • 문 형식 람다는 일반 메소드처럼 여러 줄의 코드를 포함할 수 있습니다.
  • 변수 선언, 제어문(if, for 등) 사용이 가능합니다.
  • 값을 반환해야 하는 람다(예: Func 대리자에 할당)는 return 문을 명시적으로 사용해야 합니다.
using System;

public class StatementLambda
{
    public static void Main(string[] args)
    {
        // Func<int, int, int>는 두 개의 int를 입력받아 int를 반환하는 델리게이트
        Func<int, int, int> complexCalculation = (x, y) =>
        {
            Console.WriteLine($"입력값: x={x}, y={y}"); // 여러 문장 사용 가능
            int sum = x + y;
            int product = x - y;
            if (sum > 10) // 제어문 사용 가능
            {
                Console.WriteLine("합계가 10보다 큽니다.");
                return sum; // return 문으로 값 반환
            }
            else
            {
                Console.WriteLine("곱셈 결과를 반환합니다.");
                return product; // return 문으로 값 반환
            }
        };

        int result1 = complexCalculation(3, 4); // 합계가 7 (10 이하) -> 곱셈 결과 반환
        Console.WriteLine($"결과 1: {result1}"); // 출력: 곱셈 결과를 반환합니다. \n 결과 1: 12

        Console.WriteLine("---");

        int result2 = complexCalculation(5, 6); // 합계가 11 (10 초과) -> 합계 반환
        Console.WriteLine($"결과 2: {result2}"); // 출력: 합계가 10보다 큽니다. \n 결과 2: 11

        // Action<string>은 string 하나를 입력받고 반환값이 없는(void) 델리게이트
        Action<string> logMessage = message =>
        {
            string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            Console.WriteLine($"[Log - {timestamp}] {message}"); // 여러 문장
            // 반환값이 없으므로 return 문 불필요 (있어도 되지만 값 없이 return;)
        };

        logMessage("프로그램 시작");
        // 출력 예: [Log - 2025-04-16 19:21:49] 프로그램 시작
    }
}

Func와 Action으로 더 간편하게 무명 함수 만들기

람다식을 사용하려면 해당 람다식의 시그니처(매개변수 타입과 반환 타입)와 일치하는 델리게이트 타입이 필요합니다. 매번 새로운 델리게이트를 선언하는 것은 번거롭기 때문에, .NET 프레임워크는 자주 사용되는 시그니처에 대한 제네릭 델리게이트인 FuncAction을 미리 정의해 두었습니다.

Func 대리자

  • Func 대리자는 반환 값이 있는 메소드(람다식 포함)를 참조하는 데 사용됩니다.
  • 제네릭 타입 파라미터를 받으며, 마지막 타입 파라미터는 항상 반환 타입을 나타냅니다. 나머지 타입 파라미터는 입력 매개변수의 타입입니다.
    • Func<TResult>: 매개변수 없고, TResult 타입 반환
    • Func<T, TResult>: T 타입 매개변수 하나, TResult 타입 반환
    • Func<T1, T2, TResult>: T1, T2 타입 매개변수 둘, TResult 타입 반환
    • … 최대 16개의 입력 매개변수를 가질 수 있습니다 (Func<T1, ..., T16, TResult>).
using System;

public class FuncDelegateExample
{
    public static void Main(string[] args)
    {
        // Func<int, int>: int 하나를 받아 int를 반환
        Func<int, int> square = x => x - x;
        Console.WriteLine($"제곱: {square(5)}"); // 출력: 25

        // Func<string, int, string>: string과 int를 받아 string을 반환
        Func<string, int, string> formatString = (text, number) => $"{text}: {number}";
        Console.WriteLine(formatString("아이템 번호", 123)); // 출력: 아이템 번호: 123

        // Func<double>: 매개변수 없이 double 반환
        Func<double> getRandomNumber = () => new Random().NextDouble();
        Console.WriteLine($"랜덤 실수: {getRandomNumber()}"); // 출력 예: 랜덤 실수: 0.781...

        // LINQ에서 Func 사용 예 (Where 확장 메소드는 Func<TSource, bool>을 인자로 받음)
        int[] numbers = { 1, 2, 3, 4, 5, 6 };
        var evenNumbers = numbers.Where(n => n % 2 == 0); // n => n % 2 == 0 은 Func<int, bool> 타입
        Console.WriteLine("짝수: " + string.Join(", ", evenNumbers)); // 출력: 짝수: 2, 4, 6
    }
}

Action 대리자

  • Action 대리자는 반환 값이 없는 (void) 메소드(람다식 포함)를 참조하는 데 사용됩니다.
  • 제네릭 타입 파라미터를 받으며, 이들은 모두 입력 매개변수의 타입을 나타냅니다.
    • Action: 매개변수 없음
    • Action<T>: T 타입 매개변수 하나
    • Action<T1, T2>: T1, T2 타입 매개변수 둘
    • … 최대 16개의 입력 매개변수를 가질 수 있습니다 (Action<T1, ..., T16>).
using System;
using System.Collections.Generic;

public class ActionDelegateExample
{
    public static void Main(string[] args)
    {
        // Action: 매개변수 없고 반환값 없음
        Action showTime = () => Console.WriteLine($"현재 시간: {DateTime.Now}");
        showTime(); // 출력 예: 현재 시간: 2025-04-16 오후 7:21:49

        // Action<string>: string 하나를 받고 반환값 없음
        Action<string> printMessage = message => Console.WriteLine($"메시지: {message}");
        printMessage("안녕하세요!"); // 출력: 메시지: 안녕하세요!

        // Action<int, string>: int와 string을 받고 반환값 없음
        Action<int, string> logError = (errorCode, errorMessage) =>
        {
            Console.Error.WriteLine($"[오류 {errorCode}] {errorMessage}");
        };
        logError(404, "페이지를 찾을 수 없습니다."); // 표준 오류 스트림에 출력

        // List<T>.ForEach 메소드는 Action<T>를 인자로 받음
        List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
        names.ForEach(name => Console.WriteLine($"이름: {name}"));
        /- 출력:
           이름: Alice
           이름: Bob
           이름: Charlie
        */
    }
}

FuncAction 덕분에 대부분의 경우 람다식을 사용하기 위해 별도의 델리게이트를 선언할 필요가 없어졌습니다.

표현식 트리 (Expression Tree)

람다식은 두 가지 방식으로 컴파일될 수 있습니다.

  1. 실행 가능한 코드 (Delegate): 일반적인 경우, 람다식은 IL(Intermediate Language) 코드로 컴파일되어 델리게이트 인스턴스로 만들어집니다. 이 델리게이트는 직접 호출하여 실행할 수 있습니다.
  2. 데이터 구조 (Expression Tree): 람다식을 System.Linq.Expressions.Expression<TDelegate> 형식의 변수에 할당하면, 컴파일러는 실행 가능한 코드가 아닌 식 트리라는 데이터 구조를 생성합니다. 이 식 트리는 람다식의 구조 자체를 나타냅니다 (예: 어떤 연산자가 어떤 피연산자에 적용되는지 등).

식 트리는 코드를 데이터처럼 취급하여 분석하거나 수정하고, 다른 형식(예: SQL 쿼리)으로 변환하는 데 사용됩니다. 가장 대표적인 사용 사례는 LINQ 공급자(LINQ Provider)입니다. 예를 들어, LINQ to SQL이나 Entity Framework Core는 C# LINQ 쿼리(람다식 포함)를 식 트리로 받아서 분석한 후, 이를 데이터베이스가 이해할 수 있는 SQL 쿼리로 변환하여 실행합니다.

using System;
using System.Linq.Expressions;

public class ExpressionTreeExample
{
    public static void Main(string[] args)
    {
        // 1. 람다식을 Func 델리게이트에 할당 (실행 가능한 코드로 컴파일됨)
        Func<int, bool> isAdultDelegate = age => age >= 18;
        Console.WriteLine($"델리게이트 실행 (age=20): {isAdultDelegate(20)}"); // 출력: True
        Console.WriteLine($"델리게이트 타입: {isAdultDelegate.GetType()}");

        Console.WriteLine("---");

        // 2. 람다식을 Expression<Func<...>> 에 할당 (식 트리로 컴파일됨)
        Expression<Func<int, bool>> isAdultExpression = age => age >= 18;
        Console.WriteLine($"식 트리 내용: {isAdultExpression}"); // 출력: age => (age >= 18)
        Console.WriteLine($"식 트리 본문: {isAdultExpression.Body}"); // 출력: (age >= 18)
        Console.WriteLine($"식 트리 노드 타입: {isAdultExpression.NodeType}"); // 출력: Lambda
        Console.WriteLine($"식 트리 타입: {isAdultExpression.GetType()}");

        // 식 트리를 분석할 수 있습니다.
        ParameterExpression param = isAdultExpression.Parameters[0];
        BinaryExpression body = (BinaryExpression)isAdultExpression.Body;
        ConstantExpression constant = (ConstantExpression)body.Right;

        Console.WriteLine($"파라미터 이름: {param.Name}, 타입: {param.Type}"); // 출력: 파라미터 이름: age, 타입: System.Int32
        Console.WriteLine($"본문 연산자: {body.NodeType}"); // 출력: GreaterThanOrEqual
        Console.WriteLine($"본문 우측 상수 값: {constant.Value}"); // 출력: 18

        // 식 트리를 컴파일하여 실행 가능한 델리게이트로 만들 수도 있습니다.
        Func<int, bool> compiledDelegate = isAdultExpression.Compile();
        Console.WriteLine($"컴파일된 델리게이트 실행 (age=15): {compiledDelegate(15)}"); // 출력: False
    }
}

식 트리는 코드 자체를 데이터로 다루는 강력한 메타프로그래밍 기능을 제공합니다.

식으로 이루어지는 멤버 (Expression-bodied Members)

C# 6.0부터 도입된 기능으로, 멤버(메소드, 속성 등)의 본문이 단일 표현식으로만 구성될 경우, 람다식과 유사한 => 구문을 사용하여 더 간결하게 정의할 수 있게 해줍니다. 이는 코드의 가독성을 높이고 불필요한 중괄호와 return 키워드를 줄여줍니다.

적용 가능한 멤버:

  • 메소드 (Methods): public int Add(int a, int b) => a + b;
  • 읽기 전용 속성 (Read-only Properties): public string FullName => $"{FirstName} {LastName}";
  • 속성 접근자 (Property Getters/Setters, C# 7.0+):
private int _value;
public int MyValue
{
    get => _value;
    set => _value = value > 0 ? value : 0; // 예시: 0보다 클 때만 할당
}
  • 생성자 (Constructors, C# 7.0+): public Person(string name) => Name = name;
  • 종료자 (Finalizers, C# 7.0+): ~MyResource() => Console.WriteLine("Finalizing");
  • 인덱서 (Indexers, C# 7.0+):
private int[] _data = new int[10];
public int this[int index]
{
    get => _data[index];
    set => _data[index] = value;
}
using System;

public class Point
{
    public double X { get; }
    public double Y { get; }

    // 생성자 (Expression-bodied constructor, C# 7.0+)
    public Point(double x, double y) => (X, Y) = (x, y); // 튜플 할당 활용

    // 메소드 (Expression-bodied method)
    public void Move(double dx, double dy) => (X, Y) = (X + dx, Y + dy); // 튜플 할당은 안되므로 실제로는 개별 할당 필요

    // 위의 Move 메소드는 실제로 아래와 같이 구현해야 함 (튜플 할당은 프로퍼티에 직접 불가)
    // public void Move(double dx, double dy) { X += dx; Y += dy; }
    // 하지만 개념 설명을 위해 => 사용 예시 제시

    // 읽기 전용 속성 (Expression-bodied property)
    public double DistanceFromOrigin => Math.Sqrt(X - X + Y - Y);

    // 메소드 (Expression-bodied method)
    public override string ToString() => $"({X}, {Y})"; // 문자열 보간 활용
}

public class ExpressionBodiedMembersExample
{
    public static void Main(string[] args)
    {
        Point p = new Point(3, 4);
        Console.WriteLine($"초기 위치: {p}"); // 출력: 초기 위치: (3, 4)
        Console.WriteLine($"원점 거리: {p.DistanceFromOrigin}"); // 출력: 원점 거리: 5

        // p.Move(1, 1); // 주석 처리된 실제 Move 메소드 사용 가정
        // Console.WriteLine($"이동 후 위치: {p}");
        // Console.WriteLine($"이동 후 원점 거리: {p.DistanceFromOrigin}");
    }
}

// 속성 접근자 예시 (C# 7.0+)
public class Person
{
    private string _name;

    public string Name
    {
        get => _name;
        set => _name = value ?? throw new ArgumentNullException(nameof(value)); // null 검사
    }
}

식으로 이루어지는 멤버는 코드를 훨씬 간결하게 만들어 주지만, 본문 로직이 복잡해지면 가독성을 위해 일반적인 블록 본문({...})을 사용하는 것이 더 좋습니다.