람다 식 및 익명 함수

‘람다 식’을 사용하여 익명 함수를 만듭니다. 람다 선언 연산자=>를 사용하여 본문에서 람다의 매개 변수 목록을 구분합니다. 람다 식은 다음과 같은 두 가지 형식 중 하나일 수 있습니다.

  • 식이 본문으로 포함된 식 람다:

    (input-parameters) => expression
    
  • 문 블록이 본문으로 포함된 문 람다:

    (input-parameters) => { <sequence-of-statements> }
    

람다 식을 만들려면 람다 연산자 왼쪽에 입력 매개 변수를 지정하고(있는 경우) 다른 쪽에 식이나 문 블록을 지정합니다.

람다 식은 대리자 형식으로 변환할 수 있습니다. 람다 식을 변환할 수 있는 대리자 형식은 해당 매개 변수 및 반환 값의 형식에 따라 정의됩니다. 람다 식에서 값을 반환하지 않는 경우 Action 대리자 형식 중 하나로 변환할 수 있습니다. 값을 반환하는 경우 Func 대리자 형식으로 변환할 수 있습니다. 예를 들어 매개 변수는 두 개지만 값을 반환하지 않는 람다 식은 Action<T1,T2> 대리자로 변환할 수 있습니다. 매개 변수가 하나이고 값을 반환하는 람다 식은 Func<T,TResult> 대리자로 변환할 수 있습니다. 다음 예제에서는 x라고 이름이 지정되고 x 제곱 값을 반환하는 매개 변수를 지정하는 람다 식 x => x * x는 대리자 형식의 변수에 할당됩니다.

Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25

다음 예제에 표시된 대로 식 람다는 식 트리 형식으로 변환할 수도 있습니다.

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)

대리자 형식이나 식 트리의 인스턴스가 필요한 코드에서 람다 식을 백그라운드에서 실행해야 하는 코드를 전달하는 Task.Run(Action) 메서드의 인수 등으로 사용할 수 있습니다. 다음 예제에 표시된 대로 C#에 LINQ를 작성할 때 람다 식을 사용할 수도 있습니다.

int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25

예를 들어 LINQ to Objects 및 LINQ to XML에서 메서드 기반 구문을 사용하여 System.Linq.Enumerable 클래스에서 Enumerable.Select 메서드를 호출하는 경우 매개 변수는 대리자 형식 System.Func<T,TResult>입니다. 예를 들어 LINQ to SQL에서 System.Linq.Queryable 클래스에서 Queryable.Select 메서드를 호출하는 경우 매개 변수 형식은 식 트리 형식 Expression<Func<TSource,TResult>>입니다. 두 경우 모두 동일한 람다 식을 사용하여 매개 변수 값을 지정할 수 있습니다. 그러면 두 Select 호출이 비슷하게 보일 수 있지만 실제로 람다 식을 통해 생성되는 개체 형식은 다릅니다.

식 람다

=> 연산자의 오른쪽에 식이 있는 람다 식을 식 람다라고 합니다. 식 람다는 식의 결과를 반환하며 기본 형식은 다음과 같습니다.

(input-parameters) => expression

식 람다의 본문은 메서드 호출로 구성될 수 있습니다. 하지만 SQL Server에서처럼 .NET CLR(공용 언어 런타임)의 컨텍스트 외부에서 평가되는 식 트리를 만드는 경우에는 람다 식에서 메서드 호출을 사용하면 안 됩니다. 메서드는 .NET CLR(공용 언어 런타임)의 컨텍스트 내에서만 의미가 있습니다.

문 람다

문 람다는 다음과 같이 중괄호 안에 문을 지정한다는 점을 제외하면 식 람다와 비슷합니다.

(input-parameters) => { <sequence-of-statements> }

문 람다의 본문에 지정할 수 있는 문의 개수에는 제한이 없지만 일반적으로 2-3개 정도만 지정합니다.

Action<string> greet = name =>
{
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!

문 람다를 사용하여 식 트리를 만들 수는 없습니다.

람다 식 입력 매개 변수

람다 식의 입력 매개 변수는 괄호로 묶습니다. 입력 매개 변수가 0개이면 다음과 같이 빈 괄호를 지정합니다.

Action line = () => Console.WriteLine();

람다 식에 입력 매개 변수가 하나만 있는 경우 괄호는 선택 사항입니다.

Func<double, double> cube = x => x * x * x;

두 개 이상의 입력 매개 변수는 쉼표로 구분합니다.

Func<int, int, bool> testForEquality = (x, y) => x == y;

컴파일러가 입력 매개 변수의 형식을 유추할 수 없는 경우도 있습니다. 다음 예제와 같이 형식을 명시적으로 지정할 수 있습니다.

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

입력 매개 변수 형식은 모두 명시적이거나 암시적이어야 합니다. 그렇지 않으면 CS0748 컴파일러 오류가 발생합니다.

무시 항목을 사용하여 람다 식에서 사용하지 않는 입력 매개 변수를 두 개 이상 지정할 수 있습니다.

Func<int, int, int> constant = (_, _) => 42;

람다 무시 항목 매개 변수는 람다 식을 사용하여 이벤트 처리기를 제공하는 경우에 유용할 수 있습니다.

참고 항목

이전 버전과의 호환성을 위해 단일 입력 매개 변수만 _로 명명된 경우 람다 식 내에서 _가 해당 매개 변수의 이름으로 처리됩니다.

C# 12부터 람다 식의 매개 변수에 대한 기본값을 제공할 수 있습니다. 구문 및 기본 매개 변수 값에 대한 제한은 메서드 및 로컬 함수의 경우와 동일합니다. 다음 예제에서는 기본 매개 변수를 사용하여 람다 식을 선언한 다음 기본값을 사용하여 한 번 호출하고 두 개의 명시적 매개 변수를 사용하여 한 번 호출합니다.

var IncrementBy = (int source, int increment = 1) => source + increment;

Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7

params 배열을 매개 변수로 사용하여 람다 식을 선언할 수도 있습니다.

var sum = (params int[] values) =>
{
    int sum = 0;
    foreach (var value in values) 
        sum += value;
    
    return sum;
};

var empty = sum();
Console.WriteLine(empty); // 0

var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15

이러한 업데이트의 일부로 기본 매개 변수가 있는 메서드 그룹이 람다 식에 할당되는 경우 해당 람다 식에도 동일한 기본 매개 변수가 있습니다. params 배열 매개 변수가 있는 메서드 그룹을 람다 식에 할당할 수도 있습니다.

기본 매개 변수 또는 params 배열을 매개 변수로 사용하는 람다 식에는 Func<> 또는 Action<> 형식에 해당하는 자연 형식이 없습니다. 그러나 기본 매개 변수 값을 포함하는 대리자 형식을 정의할 수 있습니다.

delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);

또는 var 선언과 함께 암시적으로 형식화된 변수를 사용하여 대리자 형식을 정의할 수 있습니다. 컴파일러는 올바른 대리자 형식을 합성합니다.

자세한 내용은 람다 식의 기본 매개 변수에 대한 기능 사양을 참조하세요.

비동기 람다

asyncawait 키워드를 사용하여 비동기 처리를 통합하는 람다 식과 문을 쉽게 만들 수 있습니다. 예를 들어 다음 Windows Forms 예제에는 비동기 메서드 ExampleMethodAsync를 호출하고 기다리는 이벤트 처리기가 포함되어 있습니다.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += button1_Click;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await ExampleMethodAsync();
        textBox1.Text += "\r\nControl returned to Click event handler.\n";
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

비동기 람다를 사용하여 동일한 이벤트 처리기를 추가할 수 있습니다. 이 처리기를 추가하려면 다음 예제에 표시된 것처럼 람다 매개 변수 목록에 async 한정자를 추가합니다.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += async (sender, e) =>
        {
            await ExampleMethodAsync();
            textBox1.Text += "\r\nControl returned to Click event handler.\n";
        };
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

비동기 메서드를 만들고 사용하는 방법에 대한 자세한 내용은 Async 및 Await를 사용한 비동기 프로그래밍을 참조하세요.

람다 식 및 튜플

C# 언어는 튜플에 대한 기본 지원을 제공합니다. 람다 식에 인수로 튜플을 제공할 수 있으며 람다 식에서 튜플을 반환할 수도 있습니다. 경우에 따라 C# 컴파일러는 형식 유추를 사용하여 튜플 구성 요소의 형식을 확인할 수 있습니다.

쉼표로 구분된 해당 구성 요소 목록을 괄호로 묶어 튜플을 정의합니다. 다음 예제에서는 3개 구성 요소가 있는 튜플을 사용하여 숫자 시퀀스를 람다 식에 전달하고 각 값을 두 배로 늘린 후 곱하기의 결과가 포함된, 3개 구성 요소가 있는 튜플을 반환합니다.

Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)

일반적으로 튜플 필드의 이름은 Item1, Item2 등과 같이 지정됩니다. 그러나 다음 예제에서처럼 명명된 구성 요소가 있는 튜플을 정의할 수 있습니다.

Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");

C# 튜플에 관한 자세한 내용은 튜플 형식을 참조하세요.

표준 쿼리 연산자와 람다 식

다른 구현 중에 LINQ to Objects는 형식이 제네릭 대리자의 Func<TResult> 패밀리 중 하나인 입력 매개 변수를 사용합니다. 이러한 대리자는 형식 매개 변수를 사용하여 입력 매개 변수의 수와 형식 및 대리자의 반환 형식을 정의합니다. Func 대리자는 소스 데이터 집합에 있는 각 요소에 적용할 사용자 정의 식을 캡슐화하는 데 유용합니다. 예를 들어 Func<T,TResult> 대리자 형식을 고려합니다.

public delegate TResult Func<in T, out TResult>(T arg)

이 경우 대리자를 Func<int, bool> 인스턴스로 인스턴스화할 수 있습니다. 여기서 int는 입력 매개 변수이고, bool은 반환 값입니다. 반환 값은 항상 마지막 형식 매개 변수에 지정됩니다. 예를 들어 Func<int, string, bool>은 두 입력 매개 변수 intstring과 반환 형식 bool을 사용하여 대리자를 정의합니다. 다음 Func 대리자를 호출하면 입력 매개 변수가 5인지 여부를 나타내는 부울 값이 반환됩니다.

Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result);   // False

Queryable 형식에 정의되어 있는 표준 쿼리 연산자의 경우와 같이 인수 형식이 Expression<TDelegate>인 경우에도 람다 식을 사용할 수 있습니다. Expression<TDelegate> 인수를 지정하면 람다 식이 식 트리로 컴파일됩니다.

이 예제에서는 Count 표준 쿼리 연산자를 사용합니다.

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");

컴파일러에서 입력 매개 변수의 형식을 유추하거나 사용자가 형식을 명시적으로 지정할 수 있습니다. 이 람다 식은 2로 나누었을 때 나머지가 1인 정수(n)의 수를 계산합니다.

다음 예제에서는 숫자 시퀀스에서 조건을 만족하지 않는 첫 번째 숫자가 9이기 때문에 numbers 배열에서 9 앞에 오는 모든 요소가 포함된 시퀀스를 생성합니다.

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3

다음 예제에서는 입력 매개 변수를 괄호로 묶어 여러 개 지정합니다. 이 메서드는 값이 배열의 서수 위치보다 작은 숫자를 발견할 때까지 numbers 배열의 모든 요소를 반환합니다.

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4

람다 식은 쿼리 식에서 바로 사용하지는 않지만, 다음 예제에서처럼 쿼리 식 내의 메서드 호출에서 사용할 수는 있습니다.

var numberSets = new List<int[]>
{
    new[] { 1, 2, 3, 4, 5 },
    new[] { 0, 0, 0 },
    new[] { 9, 8 },
    new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};

var setsWithManyPositives = 
    from numberSet in numberSets
    where numberSet.Count(n => n > 0) > 3
    select numberSet;

foreach (var numberSet in setsWithManyPositives)
{
    Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0

람다 식에서의 형식 유추

컴파일러에서는 람다 식 본문, 매개 변수 형식 및 C# 언어 사양에 설명되어 있는 기타 요소를 기준으로 형식을 유추할 수 있기 때문에 대부분의 경우에는 람다 식을 작성할 때 입력 매개 변수의 형식을 지정하지 않아도 됩니다. 대부분의 표준 쿼리 연산자에서 첫 번째 입력 형식은 소스 시퀀스 요소의 형식입니다. IEnumerable<Customer>를 쿼리할 경우 입력 변수가 Customer 개체로 유추됩니다. 즉, 다음과 같이 이 개체의 메서드와 속성에 액세스할 수 있습니다.

customers.Where(c => c.City == "London");

람다 식의 형식 유추에 대한 일반적인 규칙은 다음과 같습니다.

  • 람다 식과 대리자 형식에 포함된 매개 변수 수가 같아야 합니다.
  • 람다 식의 각 입력 매개 변수는 해당되는 대리자 매개 변수로 암시적으로 변환될 수 있어야 합니다.
  • 람다 식의 반환 값(있는 경우)은 대리자의 반환 형식으로 암시적으로 변환될 수 있어야 합니다.

람다 식의 자연 형식

공용 형식 시스템에는 “람다 식”이라는 개념이 기본적으로 포함되지 않으므로 람다 식 자체에는 형식이 없습니다. 그러나 람다 식의 “형식”을 비공식적으로 언급해야 할 경우도 있는데 이때 이 비공식적인 “형식”은 대리자 형식 또는 람다 식이 변환되는 Expression 형식을 가리킵니다.

C# 10부터 람다 식은 자연 형식을 가질 수 있습니다. 컴파일러는 사용자가 람다 식의 Func<...>Action<...> 같은 대리자 형식을 선언하도록 강제하는 대신 람다 식에서 대리자 형식을 유추할 수 있습니다. 예를 들어, 다음 선언을 참조하십시오.

var parse = (string s) => int.Parse(s);

컴파일러는 parseFunc<string, int>일 것이라 유추할 수 있습니다. 컴파일러는 적당한 대리자가 존재할 경우 사용 가능한 Func 또는 Action 대리자를 선택합니다. 그렇지 않다면 대리자 형식을 합성합니다. 예를 들어 람다 식에 ref 매개 변수가 있다면 대리자 형식이 합성됩니다. 람다 식이 자연 형식을 갖는다면 System.Object 또는 System.Delegate과 같은 덜 명시적인 형식에 할당할 수 있습니다.

object parse = (string s) => int.Parse(s);   // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>

정확히 하나의 오버로드를 갖는 메서드 그룹(즉 매개 변수 목록이 없는 메서드 이름)은 자연 형식을 갖습니다.

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

System.Linq.Expressions.LambdaExpression 또는 System.Linq.Expressions.Expression에 람다 식을 할당했고 람다가 자연 대리자 형식을 갖는다면, 식은 System.Linq.Expressions.Expression<TDelegate> 자연 형식을 갖고 자연 대리자 형식은 형식 매개 변수의 인수로 사용됩니다.

LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s);       // Expression<Func<string, int>>

자연 형식을 갖지 않는 람다 식도 있습니다. 다음 선언을 살펴보세요.

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

컴파일러가 s의 매개 변수 형식을 유추할 수 없습니다. 컴파일러가 자연 형식을 유추할 수 없으면 사용자가 형식을 선언해야 합니다.

Func<string, int> parse = s => int.Parse(s);

명시적 반환 형식

일반적으로 람다 식의 반환 형식은 명확하고 유추됩니다. 그러나 일부 식에서는 그렇지 않습니다.

var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type

C# 10부터는 람다 식의 반환 형식을 입력 매개 변수 앞에 지정할 수 있습니다. 명시적 반환 형식을 지정하는 경우 입력 매개 변수를 괄호로 묶어야 합니다.

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

특성

C# 10부터는 람다 식 및 관련 매개 변수에 특성을 추가할 수 있습니다. 다음 예제에는 람다 식에 특성을 추가하는 방법이 나와 있습니다.

Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;

다음 예제에서처럼 입력 매개 변수 또는 반환 값에 특성을 추가할 수도 있습니다.

var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;

앞의 예제에서처럼 람다 식 또는 관련 매개 변수에 특성을 추가할 때는 입력 매개 변수를 괄호로 묶어야 합니다.

Important

람다 식은 기본 대리자 형식을 통해 호출됩니다. 이는 메서드 및 로컬 함수와 다릅니다. 대리자의 Invoke 메서드는 람다 식의 특성을 확인하지 않습니다. 람다 식이 호출될 때 특성은 아무런 효과가 없습니다. 람다 식의 특성은 코드 분석용으로 유용하며, 리플렉션을 통해 검색할 수 있습니다. 이 결정에 따른 대표적인 결과는 System.Diagnostics.ConditionalAttribute를 람다 식에 적용할 수 없다는 것입니다.

람다 식에서 외부 변수 및 변수 범위 캡처

람다는 외부 변수를 참조할 수 있습니다. 이러한 외부 변수는 람다 식을 정의하는 메서드 범위 내에 있거나 람다 식을 포함하는 형식 범위 내에 있는 변수입니다. 이러한 방식으로 캡처되는 변수는 변수가 범위를 벗어나 가비지 수집되는 경우에도 람다 식에 사용할 수 있도록 저장됩니다. 외부 변수는 명확하게 할당해야만 람다 식에 사용할 수 있습니다. 다음 예제에서는 이러한 규칙을 보여 줍니다.

public static class VariableScopeWithLambdas
{
    public class VariableCaptureGame
    {
        internal Action<int>? updateCapturedLocalVariable;
        internal Func<int, bool>? isEqualToCapturedLocalVariable;

        public void Run(int input)
        {
            int j = 0;

            updateCapturedLocalVariable = x =>
            {
                j = x;
                bool result = j > input;
                Console.WriteLine($"{j} is greater than {input}: {result}");
            };

            isEqualToCapturedLocalVariable = x => x == j;

            Console.WriteLine($"Local variable before lambda invocation: {j}");
            updateCapturedLocalVariable(10);
            Console.WriteLine($"Local variable after lambda invocation: {j}");
        }
    }

    public static void Main()
    {
        var game = new VariableCaptureGame();

        int gameInput = 5;
        game.Run(gameInput);

        int jTry = 10;
        bool result = game.isEqualToCapturedLocalVariable!(jTry);
        Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");

        int anotherJ = 3;
        game.updateCapturedLocalVariable!(anotherJ);

        bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
        Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
    }
    // Output:
    // Local variable before lambda invocation: 0
    // 10 is greater than 5: True
    // Local variable after lambda invocation: 10
    // Captured local variable is equal to 10: True
    // 3 is greater than 5: False
    // Another lambda observes a new value of captured variable: True
}

람다 식의 변수 범위에는 다음과 같은 규칙이 적용됩니다.

  • 캡처된 변수는 해당 변수를 참조하는 대리자가 가비지 수집 대상이 될 때까지 가비지 수집되지 않습니다.
  • 람다 식에서 사용된 변수는 바깥쪽 메서드가 볼 수 없습니다.
  • 람다 식은 바깥쪽 메서드의 in, ref 또는 out 매개 변수를 직접 캡처할 수 없습니다.
  • 람다 식의 return 문에 의해서는 바깥쪽 메서드가 반환되지 않습니다.
  • 해당 점프 문의 대상이 람다 식 블록 바깥에 있는 경우 람다 식은 goto, break 또는 continue 문을 포함할 수 없습니다. 대상이 블록 내에 있는 경우 람다 식 블록 외부에 점프 문을 사용해도 오류가 발생합니다.

람다 식에 static 한정자를 적용하여 의도치 않게 람다가 지역 변수 또는 인스턴스 상태를 캡처하는 것을 방지할 수 있습니다.

Func<double, double> square = static x => x * x;

정적 람다는 바깥쪽 범위에서 지역 변수 또는 인스턴스 상태를 캡처할 수 없지만 정적 멤버와 상수 정의를 참조할 수 있습니다.

C# 언어 사양

자세한 내용은 C# 언어 사양익명 함수 식 섹션을 참조하세요.

이러한 기능에 대한 자세한 내용은 다음 기능 제안 노트를 참조하세요.

참고 항목