
=>player.OnLevelUp += ui.OnPlayerLevelUp_ShowEffect;
이 코드는 간단한 기능을 위해 클래스 어딘가에 메서드를 선언해야 했습니다.
익명 메서드를 사용하면 delegate(...) { ... }형태로 조금은 간결해졌지만,
delegate라는 키워드를 사용해야 합니다. 여기서 더 간결하게 표현하는 방법이 없을까요?
그래서, C# 3.0에서 람다식(Lambda Expression)이 등장했습니다.
이번 글에서는 람다식이 무엇인지, 어떻게 사용하는지 알아보겠습니다!
람다식은 간단히 말해, 메서드를 하나의 '식(Expression)'으로 표현하는 방법입니다.
=>연산자를 사용하여 "입력이 주어지면(goes to) 이런 출력을 내보내라"라는 형태입니다.
(입력) => (처리)람다식의 기본 구조: (매개변수 목록) => { 실행문; }
이 마법의 화살표 =>를 람다 연산자라고 부릅니다.
이 연산자를 기준으로 왼쪽은 입력, 오른쪽은 처리 로직입니다.
숫자 하나가 짝수인지 판별하는 간단한 기능을 예로, 코드가 어떻게 진화하는지 봅시다.
[코드]
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// 1단계: 이름 있는 메서드 사용
List<int> evens1 = numbers.FindAll(IsEven);
bool IsEven(int n)
{
return n % 2 == 0;
}
// 2단계: 익명 메서드 사용
List<int> evens2 = numbers.FindAll(
delegate (int n) { return n % 2 == 0; }
);
// 3단계: 람다식 사용 (완전체)
List<int> evens3 = numbers.FindAll(
(int n) => { return n % 2 == 0; }
);
// 4단계: 여기서부터 진짜 마법이 시작됩니다!
// - 실행문이 한 줄이면 { }와 return을 생략할 수 있습니다.
List<int> evens4 = numbers.FindAll((int n) => n % 2 == 0);
// - 컴파일러가 n이 int 타입인 것을 추론할 수 있으므로, 타입 선언을 생략할 수 있습니다.
List<int> evens5 = numbers.FindAll((n) => n % 2 == 0);
// - 매개변수가 하나뿐이라면, ( ) 괄호마저 생략할 수 있습니다.
List<int> evens6 = numbers.FindAll(n => n % 2 == 0);
// string.Join 메서드를 사용하여 결과를 출력합니다.
// 구분자를 사용해서 여러 문자열을 한 줄로 이어줍니다.
Console.WriteLine("evens1: " + string.Join(", ", evens1));
Console.WriteLine("evens2: " + string.Join(", ", evens2));
Console.WriteLine("evens3: " + string.Join(", ", evens3));
Console.WriteLine("evens4: " + string.Join(", ", evens4));
Console.WriteLine("evens5: " + string.Join(", ", evens5));
Console.WriteLine("evens6: " + string.Join(", ", evens6));
[실행 결과]
evens1: 2, 4, 6
evens2: 2, 4, 6
evens3: 2, 4, 6
evens4: 2, 4, 6
evens5: 2, 4, 6
evens6: 2, 4, 6
최종 형태인 n => n % 2 == 0를 보세요. 로직이 극도로 간결합니다.
람다식은 컬렉션 처리와 이벤트 핸들링에서 압도적인 생산성을 보여줍니다.
LINQ(Language-Integrated Query)는 람다식의 최고 파트너입니다.
컬렉션 데이터를 다룰 때 SQL처럼 선언적으로 코드를 작성할 수 있게 해줍니다.
var products = new List<Product> { ... };
// 가격이 10000원 이상인 제품을 찾아, 이름을 기준으로 정렬한 후, 이름만 선택
var expensiveProductNames = products
.Where(p => p.Price >= 10000)
.OrderBy(p => p.Name)
.Select(p => p.Name);
Where, OrderBy, Select같은 LINQ 메서드에 전달되는 조건들이 모두 람다식입니다.
만약 이걸 for문과 if문으로 구현했다면 코드가 훨씬 길고 복잡해졌을 겁니다.
이벤트에 대한 반응을 즉석에서 정의할 때 매우 유용합니다.
// 버튼 클릭 이벤트에 람다로 핸들러를 바로 연결
myButton.Click += (sender, e) =>
{
MessageBox.Show("버튼이 클릭되었습니다!");
// 복잡한 로직도 이 안에 작성 가능
};
별도의 핸들러 메서드를 만들 필요 없이, 이벤트가 선언된 위치에서
처리 로직을 한눈에 볼 수 있어 가독성이 크게 향상됩니다.
여기서 C# 컴파일러는 똑같이 생긴 람다식을 보고,
문맥에 따라 두 가지 다른 결과물을 만들어낼 수 있습니다.
1. 대리자(Delegate): 지금까지 우리가 본 모든 예시입니다. 컴파일러는
람다식를 IL 코드로 변환하여 실행할 수 있는 대리자 인스턴스를 만듭니다.
"어떻게 실행할지"에 대한 '요리법' 그 자체입니다.
Func<int, int> squareDelegate = x => x * x; // x를 제곱하는 '기능'
2. 식 트리: 만약 람다식을 Expression<TDelegate>타입의 변수에 할당하면,
컴파일러는 IL 코드를 생성하는 대신, 람다식의 구조 자체를 분석하여
트리 형태의 데이터 구조(Expression Tree)로 만듭니다.
"어떻게 실행할지"에 대한 '요리법이 적힌 레시피'입니다.
Expression<Func<int, int>> squareExpression = x => x * x; // x를 제곱하는 '설계도'
squareDelegate는 그저 호출해서 결과를 얻는 '기능'이지만,
squareExpression은 x, *, x라는 구조를 가진 '데이터'입니다.
우리는 이 '레시피'를 읽고 분석하거나, 다른 언어(예: SQL)로 번역할 수 있습니다.
람다식은 C# 코드를 간결하고 가독성 높게 만들어주는 강력한 도구입니다.
| 특징 | 설명 |
|---|---|
| 핵심 | 이름 없는 함수를 (입력) => (처리) 형태로 표현 |
| 장점 | 극강의 간결함, 높은 가독성, 코드의 지역성(Locality) |
| 주 사용처 | LINQ, 이벤트 핸들러, 비동기 처리 등 대리자가 사용되는 모든 곳 |
| 두 가지 형태 | 대리자(Delegate)로 컴파일되어 코드로 실행되거나, 식 트리(Expression Tree)로 컴파일되어 데이터로 분석될 수 있음 |