람다 식과 익명 함수
람다 식과 익명 함수
한 번 쓰고 버릴 함수가 필요할 때, 이름을 붙이기가 오히려 번거로울 때가 있어요. 그럴 때 쓰는 게 *람다 식(lambda expression)*이에요. 람다 식으로 익명 함수를 만들고, 람다의 매개 변수 목록과 본문을 구분하는 데는 람다 선언 연산자 =>를 써요.
본문
람다 식은 다음 두 가지 형태 중 하나예요.
-
본문으로 *식(expression)*을 갖는 식 람다:
(input-parameters) => expression -
본문으로 문(statement) 블록을 갖는 문 람다:
(input-parameters) => { <sequence-of-statements> }
람다 식을 만들려면 람다 연산자 왼쪽에 입력 매개 변수(있다면)를, 그 반대쪽에 식이나 문 블록을 지정하면 돼요.
모든 람다 식은 delegate 형식으로 변환할 수 있어요. 람다 식이 어느 delegate 형식으로 변환될 수 있는지는 그 매개 변수와 반환 값의 형식이 결정해요. 람다 식이 값을 반환하지 않으면 Action delegate 형식 중 하나로, 값을 반환하면 Func delegate 형식 중 하나로 변환해요. 예를 들어 매개 변수가 두 개이고 값을 반환하지 않는 람다 식은 xref:System.Action`2 delegate로, 매개 변수가 하나이고 값을 반환하는 람다 식은 xref:System.Func`2 delegate로 변환할 수 있죠. 아래 예시에서 x라는 매개 변수를 받아 x의 제곱 값을 반환하는 람다 식 x => x * x를 delegate 형식의 변수에 할당하고 있어요.
:::code language="csharp" source="snippets/lambda-expressions/Introduction.cs" id="SnippetDelegate":::
식 람다는 식 트리(expression tree) 형식으로도 변환할 수 있어요. 다음 예시를 볼게요.
:::code language="csharp" source="snippets/lambda-expressions/Introduction.cs" id="SnippetExpressionTree":::
람다 식은 delegate 형식이나 식 트리의 인스턴스가 필요한 코드라면 어디든 쓸 수 있어요. 그중 하나가 xref:System.Threading.Tasks.Task.Run(System.Action)?displayProperty=nameWithType 메서드의 인자로 백그라운드에서 실행할 코드를 넘기는 경우예요. 또 C#의 LINQ를 작성할 때도 람다 식을 쓸 수 있어요. 다음 예시가 그런 경우에요.
:::code language="csharp" source="snippets/lambda-expressions/Introduction.cs" id="SnippetArgument":::
메서드 기반 구문으로 xref:System.Linq.Enumerable?displayProperty=nameWithType 클래스의 xref:System.Linq.Enumerable.Select*?displayProperty=nameWithType 메서드를 호출할 때, 예를 들어 LINQ to Objects나 LINQ to XML에서 그럴 때, 매개 변수는 delegate 형식 xref:System.Func`2?displayProperty=nameWithType이에요. 반면 xref:System.Linq.Queryable?displayProperty=nameWithType 클래스의 xref:System.Linq.Queryable.Select*?displayProperty=nameWithType 메서드를 호출할 때, 예를 들어 LINQ to SQL에서 그럴 때는 매개 변수 형식이 식 트리 형식인 Expression<Func<TSource,TResult>>이 됩니다. 두 경우 모두 같은 람다 식으로 매개 변수 값을 지정할 수 있어요. 그래서 두 Select 호출은 비슷해 보이지만, 실제로 람다에서 만들어지는 객체의 형식은 서로 달라요.
Expression lambdas
=> 연산자 오른쪽에 식을 갖는 람다 식을 *식 람다(expression lambda)*라고 불러요. 식 람다는 그 식의 결과를 반환하며 다음의 기본 형태를 가져요.
(input-parameters) => expression
식 람다의 본문에는 메서드 호출이 들어갈 수 있어요. 다만 쿼리 공급자가 평가하는 식 트리를 만들 때는, 호출을 쿼리 공급자가 자기 형식으로 변환할 수 있는 메서드로 제한해야 해요. 쿼리 공급자마다 할 수 있는 기능이 달라요. 예를 들어 SQL 기반 공급자 상당수는 xref:System.String.StartsWith*?displayProperty=nameWithType 같은 메서드를 LIKE 같은 적절한 SQL 식으로 변환해요. 쿼리 공급자가 메서드 호출을 인식하지 못하면 그 식을 변환하거나 실행할 수 없어요.
Statement lambdas
문 람다는 식 람다와 비슷하지만, 문장들을 중괄호로 감싼다는 점이 달라요.
(input-parameters) => { <sequence-of-statements> }
문 람다의 본문에는 문장을 몇 개든 넣을 수 있어요. 다만 실제로는 보통 두세 개를 넘지 않죠.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetStatementLambda":::
문 람다는 식 트리를 만드는 데 쓸 수 없어요.
람다 식의 입력 매개 변수
람다 식의 입력 매개 변수는 괄호로 감싸요. 입력 매개 변수가 없으면 빈 괄호로 지정합니다.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetZeroParameters":::
입력 매개 변수가 하나뿐이면 괄호를 생략할 수 있어요.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetOneParameter":::
입력 매개 변수가 둘 이상이면 쉼표로 구분해요.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetTwoParameters":::
컴파일러는 보통 람다 식의 매개 변수 형식을 추론하는데, 이걸 *암시적 형식 매개 변수 목록(implicitly typed parameter list)*이라고 불러요. 형식을 명시적으로 지정할 수도 있는데, 그 경우는 *명시적 형식 매개 변수 목록(explicitly typed parameter list)*이라고 해요. 다음 예시는 명시적 형식 매개 변수 목록을 보여줘요.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetExplicitlyTypedParameters":::
입력 매개 변수 형식은 전부 명시적이거나 전부 암시적이어야 해요. 그렇지 않으면 CS0748 컴파일 오류가 발생해요. C# 14 이전에는 ref나 out 같은 한정자가 붙은 매개 변수는 반드시 형식을 명시적으로 적어야 했어요. C# 14부터는 그 제한이 사라졌죠. 다만 params 한정자를 쓸 때는 여전히 형식을 선언해야 해요.
식에서 쓰지 않는 입력 매개 변수를 둘 이상 지정할 때는 discards를 사용해요.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetDiscards":::
람다 discard 매개 변수는 람다 식으로 이벤트 처리기를 제공할 때 유용해요.
[!NOTE] 하위 호환성을 위해, 입력 매개 변수가 하나뿐이고 그 이름이
_이면 컴파일러는 그 람다 식 안에서_를 매개 변수의 이름으로 취급해요.
C# 12부터 명시적 형식 매개 변수 목록에 *기본값(default values)*을 지정할 수 있어요. 기본 매개 변수 값의 구문과 제한은 메서드나 로컬 함수와 같아요. 다음 예시는 기본 매개 변수가 있는 람다 식을 선언한 다음, 한 번은 기본값으로, 한 번은 명시적 매개 변수 두 개로 호출해요.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetDefaultParameters":::
명시적 형식 매개 변수 목록에서 마지막 매개 변수로 params 배열이나 컬렉션을 쓰는 람다 식도 선언할 수 있어요.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetParamsArray":::
이 업데이트의 일부로, 기본 매개 변수를 가진 메서드 그룹을 람다 식에 할당하면 그 람다 식도 같은 기본 매개 변수를 갖게 돼요. params 컬렉션 매개 변수를 가진 메서드 그룹 역시 람다 식에 할당할 수 있어요.
기본 매개 변수나 params 컬렉션을 매개 변수로 갖는 람다 식은 Func<>나 Action<> 형식에 대응하는 자연 형식이 없어요. 다만 기본 매개 변수 값을 포함하는 delegate 형식을 직접 정의할 수는 있죠.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="DelegateDeclarations":::
아니면 var 선언으로 암시적 형식 변수를 써서 delegate 형식을 정의할 수도 있어요. 컴파일러가 알맞은 delegate 형식을 합성해 줘요.
람다 식의 기본 매개 변수에 대한 자세한 내용은 람다 식의 기본 매개 변수 기능 사양을 참고하세요.
Async lambdas
async와 await 키워드를 쓰면 비동기 처리를 포함한 람다 식과 문을 쉽게 만들 수 있어요. 아래 Windows Forms 예시에는 비동기 메서드 ExampleMethodAsync를 호출하고 기다리는(await) 이벤트 처리기가 들어 있어요.
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 람다로도 추가할 수 있어요. 처리기를 추가하려면 다음 예시처럼 람다 매개 변수 목록 앞에 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 메서드를 만들고 사용하는 방법에 대한 자세한 내용은 async 및 await를 이용한 비동기 프로그래밍을 참고하세요.
람다 식과 튜플
C# 언어는 튜플을 기본으로 지원해요. 람다 식에 튜플을 인자로 넘길 수 있고, 람다 식이 튜플을 반환할 수도 있어요. 어떤 경우에는 C# 컴파일러가 형식 추론으로 튜플 요소의 형식을 결정하기도 해요.
튜플은 구성 요소들을 쉼표로 구분한 목록을 괄호로 감싸서 정의해요. 다음 예시는 구성 요소가 세 개인 튜플로 숫자 시퀀스를 람다 식에 넘겨서, 각 값을 두 배로 만든 다음 곱셈 결과를 담은 구성 요소 세 개짜리 튜플을 반환해요.
:::code language="csharp" source="snippets/lambda-expressions/LambdasAndTuples.cs" id="SnippetWithoutComponentName":::
보통 튜플의 필드는 Item1, Item2처럼 이름이 붙어 있어요. 다만 다음 예시처럼 이름이 붙은 구성 요소가 있는 튜플을 정의할 수도 있죠.
:::code language="csharp" source="snippets/lambda-expressions/LambdasAndTuples.cs" id="SnippetWithComponentName":::
C# 튜플에 대한 자세한 내용은 튜플 형식을 참고하세요.
표준 쿼리 연산자와 람다
LINQ to Objects를 비롯한 여러 구현이 xref:System.Func`1 계열 제네릭 delegate 중 하나를 형식으로 갖는 입력 매개 변수를 사용해요. 이 delegate들은 형식 매개 변수로 입력 매개 변수의 개수·형식, 그리고 delegate의 반환 형식을 정의해요. Func delegate는 원본 데이터 집합의 각 요소에 적용되는 사용자 정의 식을 캡슐화하는 데 유용해요. 예를 들어 xref:System.Func`2 delegate 형식을 살펴볼게요.
public delegate TResult Func<in T, out TResult>(T arg)
이 delegate를 Func<int, bool> 인스턴스로 인스턴스화하면 int가 입력 매개 변수, bool이 반환 값이 돼요. 반환 값은 항상 마지막 형식 매개 변수에 지정돼요. 예를 들어 Func<int, string, bool>은 입력 매개 변수 int와 string을 갖고 bool을 반환 형식으로 갖는 delegate를 정의해요. 다음 Func delegate는 호출되면 입력 매개 변수가 5와 같은지 나타내는 Boolean 값을 반환해요.
:::code language="csharp" source="snippets/lambda-expressions/LambdasWithQueryMethods.cs" id="SnippetFunc":::
인자 형식이 xref:System.Linq.Expressions.Expression`1일 때도 람다 식을 제공할 수 있어요. 예를 들어 xref:System.Linq.Queryable 형식이 정의하는 표준 쿼리 연산자에서 그렇죠. xref:System.Linq.Expressions.Expression`1 인자를 지정하면 람다는 식 트리로 컴파일돼요.
다음 예시는 xref:System.Linq.Enumerable.Count* 표준 쿼리 연산자를 사용해요.
:::code language="csharp" source="snippets/lambda-expressions/LambdasWithQueryMethods.cs" id="SnippetCount":::
컴파일러가 입력 매개 변수의 형식을 추론할 수도 있고, 직접 명시적으로 지정할 수도 있어요. 이 특정 람다 식은 2로 나누었을 때 나머지가 1인 정수(n)들을 세요.
다음 예시는 numbers 배열에서 9보다 앞에 있는 모든 요소를 담은 시퀀스를 만들어요. 9가 조건을 충족하지 않는 시퀀스의 첫 번째 숫자이기 때문이에요.
:::code language="csharp" source="snippets/lambda-expressions/LambdasWithQueryMethods.cs" id="SnippetTakeWhile":::
다음 예시는 입력 매개 변수 여러 개를 괄호로 감싸서 지정해요. 이 메서드는 배열에서 값이 배열 내 서수 위치보다 작은 숫자를 찾을 때까지 numbers 배열의 모든 요소를 반환해요.
:::code language="csharp" source="snippets/lambda-expressions/LambdasWithQueryMethods.cs" id="SnippetTakeWhileWithIndex":::
쿼리 식 안에서 람다 식을 직접 쓰지는 않지만, 쿼리 식 안의 메서드 호출에는 쓸 수 있어요. 다음 예시를 볼게요.
:::code language="csharp" source="snippets/lambda-expressions/LambdasWithQueryMethods.cs" id="Query":::
람다 식의 형식 추론
람다를 작성할 때 입력 매개 변수의 형식을 지정하지 않아도 되는 경우가 많아요. 컴파일러가 람다 본문, 매개 변수 형식, 그리고 C# 언어 사양에 설명된 다른 요소들을 바탕으로 형식을 추론해요. 대부분의 표준 쿼리 연산자에서 첫 번째 입력은 원본 시퀀스에 있는 요소의 형식이에요. IEnumerable<Customer>를 쿼리한다면 입력 변수는 Customer 객체로 추론되므로, 그 객체의 메서드와 속성에 접근할 수 있어요.
customers.Where(c => c.City == "London");
람다의 형식 추론에 대한 일반적인 규칙은 다음과 같아요.
- 람다는 delegate 형식과 같은 수의 매개 변수를 가져야 해요.
- 람다의 각 입력 매개 변수는 대응하는 delegate 매개 변수로 암시적으로 변환 가능해야 해요.
- 람다의 반환 값(있다면)은 delegate의 반환 형식으로 암시적으로 변환 가능해야 해요.
람다 식의 자연 형식
공통 형식 시스템에는 "람다 식"이라는 고유한 개념이 없기 때문에 람다 식 자체는 형식이 없어요. 그래도 람다 식의 "형식"을 비공식적으로 말하는 게 편할 때가 있어요. 그 비공식적인 "형식"은 람다 식이 변환되는 delegate 형식이나 xref:System.Linq.Expressions.Expression 형식을 가리켜요.
람다 식은 *자연 형식(natural type)*을 가질 수 있어요. Func<...>나 Action<...> 같은 delegate 형식을 직접 선언하라고 요구하는 대신, 컴파일러가 람다 식에서 delegate 형식을 추론해 주는 거예요. 예를 들어 다음 선언을 볼게요.
var parse = (string s) => int.Parse(s);
컴파일러는 parse를 Func<string, int>로 추론해요. 컴파일러는 적합한 것이 있으면 사용 가능한 Func나 Action delegate를 고르고, 그렇지 않으면 delegate 형식을 합성해요. 예를 들어 람다 식에 ref 매개 변수가 있으면 delegate 형식이 합성돼요. 람다 식에 자연 형식이 있으면 xref:System.Object?displayProperty=nameWithType이나 xref:System.Delegate?displayProperty=nameWithType처럼 덜 구체적인 형식에 할당할 수 있어요.
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
람다 식을 xref:System.Linq.Expressions.LambdaExpression?displayProperty=nameWithType이나 xref:System.Linq.Expressions.Expression?displayProperty=nameWithType에 할당하고, 람다에 자연 delegate 형식이 있다면, 그 식은 자연 delegate 형식을 형식 매개 변수의 인자로 사용하는 xref:System.Linq.Expressions.Expression`1?displayProperty=nameWithType 형식의 자연 형식을 가져요.
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
입력 매개 변수 앞에 람다 식의 반환 형식을 명시적으로 지정할 수 있어요. 명시적 반환 형식을 지정하면 입력 매개 변수를 반드시 괄호로 감싸야 해요.
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
특성(Attributes)
람다 식과 그 매개 변수에 특성을 추가할 수 있어요. 다음 예시는 람다 식에 특성을 추가하는 방법을 보여줘요.
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] 람다 식은 기반이 되는 delegate 형식을 통해 호출돼요. 이 호출 방식은 메서드나 로컬 함수와는 달라요. delegate의
Invoke메서드는 람다 식의 특성을 검사하지 않아요. 따라서 람다 식을 호출할 때 특성은 아무 효과가 없어요. 람다 식의 특성은 코드 분석에 유용하며 리플렉션을 통해 발견할 수 있어요. 이 결정의 한 가지 결과로, xref:System.Diagnostics.ConditionalAttribute?displayProperty=nameWithType는 람다 식에 적용할 수 없어요.
람다 식에서 외부 변수 캡처와 변수 범위
람다는 *외부 변수(outer variables)*를 참조할 수 있어요. 이 외부 변수란 람다 식을 정의하는 메서드 안에서, 또는 람다 식을 포함하는 형식 안에서 범위(scope)가 유효한 변수를 말해요. 이렇게 변수를 캡처하면, 람다 식은 그 변수가 범위를 벗어나서 보통은 가비지 컬렉션의 대상이 되더라도 그 변수를 사용할 수 있도록 저장해요. 람다 식에서 외부 변수를 소비하기 전에 그 변수에 값을 반드시 할당해야 해요. 다음 예시가 이 규칙들을 보여줘요.
:::code language="csharp" source="snippets/lambda-expressions/VariableScopeWithLambdas.cs" id="SnippetVariableScope":::
람다 식의 변수 범위에는 다음 규칙이 적용돼요.
- 캡처한 변수는 그 변수를 참조하는 delegate가 가비지 컬렉션 대상이 될 때까지 수집되지 않아요.
- 람다 식 안에서 도입한 변수는 바깥 메서드에서는 보이지 않아요.
- 람다 식은 바깥 메서드의 in, ref, out 매개 변수를 직접 캡처할 수 없어요.
- 람다 식 안의 return 문은 바깥 메서드가 반환되게 하지 않아요.
- 점프 문의 대상이 람다 식 블록 밖에 있을 때, 람다 식 안에 goto, break, continue 문을 둘 수 없어요. 또한 점프 문의 대상이 블록 안에 있을 때 람다 식 블록 밖에 점프 문을 두는 것도 오류예요.
람다가 지역 변수나 인스턴스 상태를 의도치 않게 캡처하지 않도록, 람다 식에 static 한정자를 적용할 수 있어요.
:::code language="csharp" source="snippets/lambda-expressions/GeneralExamples.cs" id="SnippetStatic":::
static 람다는 바깥 범위의 지역 변수나 인스턴스 상태를 캡처할 수 없지만, static 멤버와 상수 정의는 참조할 수 있어요.
C# 언어 사양
더 자세한 내용은 C# 언어 사양의 익명 함수 식 섹션을 참고하세요.