람다 식은 => 연산자("goes to")로 코드 안에 바로 쓰는 작은 익명 함수입니다. 매개변수는 왼쪽에, 결과는 오른쪽에 옵니다:
출력:
60.0
12
ORDER SHIPPED!
96.0, 180.0
람다에는 자기 타입이 없습니다. 문맥이 기대하는 델리게이트 타입의 델리게이트(함수를 가리키는 객체)가 됩니다. 여기서는 Func<decimal, decimal>, List.FindAll에서는 Predicate<T>, 이벤트에서는 EventHandler입니다. Func 델리게이트는 값을 반환하고 Action 델리게이트는 반환하지 않습니다. 전체 목록은 Func와 Action 페이지에 있습니다.
문법 형태
x => x * x // one parameter: parentheses optional
(x, y) => x + y // several parameters
() => DateTime.Now.Year // no parameters
(int x, string s) => s.Length > x // explicit parameter types
order => // statement lambda: a block with return
{
decimal subtotal = order.Qty * order.Price;
return subtotal > 100m ? subtotal * 0.9m : subtotal;
}
식 람다는 => 뒤에 식 하나가 있고 그 값이 결과입니다. 문 람다는 중괄호로 감싼 블록을 가지며, 어떤 문장이든 담을 수 있고, 델리게이트가 값을 반환한다면 모든 경로에서 값을 return해야 합니다(그렇지 않으면 CS1643 Not all code paths return a value in lambda expression).
새 버전은 형태를 더 추가했으며, 모두 선택 사항입니다:
- C# 9: 무시 매개변수. 쓰지 않는 여러 매개변수를 모두
_로 둘 수 있고((_, _) => Save()), 변수 캡처가 금지된static람다도 생겼습니다. - C# 10: "자연 타입".
var square = (int x) => x * x;는Func<int, int>로 추론됩니다. 그 전에는var f = x => x * 2;가 실패하며, C# 10에서도 매개변수 타입이 없으면 CS8917The delegate type could not be inferred로 여전히 실패합니다. - C# 12: 기본 매개변수 값,
(int x, int y = 1) => x + y.
메서드에 람다 넘기기
델리게이트 매개변수를 받는 메서드는 모두 람다를 받습니다. 데이터뿐 아니라 동작을 넘기는 방법입니다:
출력:
report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf
Filter는 "keep"이 무슨 뜻인지 모릅니다. 호출하는 쪽이 람다로 정합니다. List.Sort도 같은 방식입니다. 람다는 음수, 0, 양수를 반환하는 비교이고, Sort는 필요한 만큼 이를 호출합니다.
LINQ 안의 람다
대부분의 C# 코드가 람다를 만나는 곳이 LINQ입니다. 모든 쿼리 연산자가 람다를 받습니다. Where는 조건을, Select는 투영을, OrderBy는 키를, Sum은 더할 값을 받습니다:
출력:
Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45
람다는 작성된 곳에서 실행되지 않습니다. LINQ는 이를 저장해 두었다가 foreach, ToList(), Sum()으로 쿼리가 열거될 때 호출하며, 이를 지연 실행이라고 합니다. 연산자 자체는 LINQ 페이지에서 다룹니다.
클로저: 캡처된 변수
람다는 자신이 작성된 메서드의 지역 변수와 매개변수를 쓸 수 있습니다. 현재 값이 아니라 변수를 캡처하며, 변수는 람다가 살아 있는 동안 계속 살아 있습니다:
출력:
1
2
3
1
50.0
MakeCounter를 호출할 때마다 새 count가 만들어지므로 두 카운터는 독립적입니다. 할인 람다는 rate를 작성될 때가 아니라 실행될 때 읽었으므로 50%를 적용했습니다. 컴파일러는 캡처된 변수를 메서드와 람다가 공유하는 숨겨진 클래스로 옮겨서 이를 구현합니다.
루프 캡처 함정
클로저가 문제를 일으키는 곳은 루프 변수 캡처입니다. for 루프에는 루프 전체에 변수가 하나뿐이므로, 그 안에서 만든 모든 람다가 그 변수를 공유합니다:
출력:
3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach
첫 번째 루프의 세 람다는 모두 루프가 끝난 뒤 i가 3일 때 실행되었습니다. 본문 안에서 선언한 변수에 i를 복사하면 람다마다 자기 변수를 갖게 됩니다. foreach는 알아서 올바르게 처리합니다. C# 5부터 반복 변수가 매번 새 변수이기 때문입니다. C# 5 이전에는 같은 버그가 있었으며, 그래서 인터넷의 오래된 답변은 여전히 foreach에도 복사를 권합니다.
같은 함정은 나중에 실행되는 모든 것에 해당합니다. 이벤트 처리기, 타이머, Task.Run, 그리고 루프 안에서 만들고 루프 뒤에 열거하는 LINQ 쿼리가 그렇습니다.
람다, 익명 메서드, 로컬 함수
람다는 C# 2의 익명 메서드 문법을 대체했으며, 익명 메서드도 여전히 컴파일됩니다:
Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; }; // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0; // lambda
한 메서드 안에서만 쓰는 헬퍼라면 C# 7.0의 로컬 함수도 있습니다. 이름이 있고, 재귀가 가능하며, 델리게이트로 변환하지 않는 한 델리게이트를 할당하지 않습니다. 무언가(LINQ, Sort, 이벤트)에 동작을 넘길 때는 람다를, 직접 호출할 때는 로컬 함수나 private 메서드를 쓰세요.
Entity Framework 같은 LINQ 공급자에서는 차이가 하나 더 있습니다. Expression<Func<T, bool>>에 대입한 람다는 코드로 컴파일되지 않고, 코드를 설명하는 데이터 구조로 컴파일되며, 공급자가 이를 SQL로 변환합니다. 문법은 똑같고, 호출하는 메서드의 매개변수 타입이 어느 쪽이 될지 정합니다.
자주 묻는 질문
C#에서 람다 식이란 무엇인가요?
=> 연산자로 쓰는 익명 함수입니다. x => x * 2는 x를 받아 x * 2를 반환합니다. 람다에는 자기 이름이 없으며, Func<int, int>나 Action<string> 같은 델리게이트 타입으로 변환되어 변수에 저장하거나, 메서드에 넘기거나, 메서드에서 반환할 수 있습니다.
매개변수가 여러 개이거나 없는 람다는 어떻게 쓰나요?
매개변수를 괄호로 감쌉니다. 두 개는 (a, b) => a + b, 없으면 () => DateTime.Now입니다. 매개변수가 하나일 때만 괄호를 생략할 수 있습니다(x => x + 1). 타입을 명시할 수도 있습니다: (int a, int b) => a + b.
C# 람다의 클로저란 무엇인가요?
둘러싼 메서드의 지역 변수를 쓰는 람다는 그 값의 스냅샷이 아니라 변수 자체를 캡처합니다. 변수는 람다가 살아 있는 동안 함께 살아 있고, 어느 쪽이든 나중에 값을 바꾸면 다른 쪽에서도 보입니다. 카운터와 콜백이 동작하는 원리이자, 전형적인 for 루프 캡처 버그의 원인입니다.
for 루프 안의 람다가 모두 같은 값을 보는 이유는 무엇인가요?
for 루프에는 루프 전체에 대해 루프 변수가 하나뿐이므로, 안에서 만든 모든 람다가 같은 i를 캡처하고 나중에 실행될 때 마지막 값을 봅니다. 본문 안에서 지역 변수에 복사하고(int copy = i;) 그 복사본을 캡처하세요. foreach는 C# 5부터 반복마다 새 변수를 만들므로 이 문제가 없습니다.
var f = x => x * 2가 컴파일되지 않는 이유는 무엇인가요?
컴파일러가 원하는 델리게이트 타입도 x의 타입도 알 수 없으므로 CS8917 The delegate type could not be inferred(C# 10 이전에는 CS0815)를 보고합니다. 타입을 선언하거나(Func<int, int> f = x => x * 2;), C# 10 이상에서는 매개변수에 타입을 붙이세요: var f = (int x) => x * 2;.