람다식과 함수형 스타일
이름 없는 짧은 메서드로 코드를 줄입니다.
이름을 붙일 만큼 크지 않은 메서드
3.4에서 메서드를 값처럼 넘길 수 있다는 것을 보았습니다. 그런데 조건 하나를 넘기려고 메서드를 따로 선언하는 것은 번거롭습니다.
// 이 한 줄을 넘기려고 static bool IsCheap(Product p) => p.Price <= 3000; // 여기까지 와야 합니다. 이름도 지어야 하고, 어디에 둘지도 정해야 합니다. products.Where(IsCheap);
람다식은 그것을 사용하는 자리에서 바로 적게 해 줍니다. 이름이 없고, 선언할 곳을 찾을 필요도 없습니다.
products.Where(p => p.Price <= 3000);
=> 를 사이에 두고 왼쪽이 받을 것, 오른쪽이 할
일입니다. 3.3의 LINQ가 읽기 좋았던 것도
이 문법 덕분이었습니다.
그리고 람다에는 바깥의 변수를 붙잡는 성질이 있습니다. 편리하지만 동작이 짐작과 다를 때가 있어, 이 단원의 뒤쪽 절반을 거기에 할애합니다.
여러 모양으로 적기
아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.
// 매개 변수 하나 — 괄호를 생략할 수 있습니다 Func<int, int> twice = x => x * 2; // 둘 이상이면 괄호가 필요합니다 Func<int, int, int> add = (a, b) => a + b; // 받는 것이 없으면 빈 괄호입니다 Func<string> hello = () => "안녕하세요"; // 여러 줄이면 중괄호와 return 을 적습니다 Func<int, string> grade = score => { if (score >= 90) return "A"; if (score >= 80) return "B"; return "C"; }; Console.WriteLine(twice(21)); Console.WriteLine(add(3, 4)); Console.WriteLine(hello()); Console.WriteLine(grade(85));
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
매개 변수의 형식을 적지 않았습니다.
Func<int, int> 에 담기니까
x 가 int 라는 것을 컴파일러가
압니다. 필요하면 (int x) => x * 2 처럼 적어도 됩니다.
이름을 붙일 때와 무엇이 다른가
static bool IsCheap(Product p) => p.Price <= 3000; // 여러 곳에서 다시 사용합니다 products.Where(IsCheap); orders.Where(IsCheap); // 이름이 곧 설명입니다 // 따로 테스트할 수도 있습니다
// 선언할 곳을 찾지 않아도 됩니다 products.Where(p => p.Price <= 3000); // 조건이 그 자리에 보입니다 orders .Where(o => o.Total > 50000) .OrderBy(o => o.Date); // 한 번만 사용할 것에 알맞습니다
기준은 "다시 사용하는가" 와 "이름이 필요한가" 입니다. 한 번만 사용하고 조건이 한눈에 들어오면 람다가 낫고, 여러 곳에서 사용하거나 조건 자체가 복잡해 이름으로 설명해야 한다면 메서드로 빼는 편이 낫습니다. 람다가 세 줄을 넘어가면 대개 이름을 붙일 때입니다.
바깥 변수를 붙잡습니다
람다는 자기 바깥에 있는 변수를 그대로 사용할 수 있습니다. 이것을 클로저라고 합니다. 값을 복사해 두는 것이 아니라 변수 자체를 붙잡습니다.
var limit = 3000; Func<int, bool> cheap = price => price <= limit; Console.WriteLine(cheap(2500)); // True limit = 1000; // 람다를 만든 뒤에 바꿨습니다 Console.WriteLine(cheap(2500)); // False — 바뀐 값을 봅니다
람다를 만든 시점의 값이 아니라 부르는 시점의 값을 봅니다. 이것을 모르면 짐작과 다른 결과가 나옵니다.
반복문에서 특히 조심하세요
람다를 목록에 모아 두었다가 나중에 부르는 코드에서 문제가 드러납니다.
for 와 foreach 의
결과가 다릅니다.
var list = new List<Func<int>>(); for (var i = 0; i < 3; i++) list.Add(() => i); // 3, 3, 3 // i 는 하나뿐이고 반복이 끝나면 3 입니다
var list = new List<Func<int>>(); foreach (var n in new[] { 0, 1, 2 }) list.Add(() => n); // 0, 1, 2 // n 은 회마다 새로 만들어집니다
for 의 i 는
반복 전체에 하나뿐입니다. 세 람다가 같은 변수를 붙잡았고, 부를 때는
이미 반복이 끝나 3 이 되어 있습니다.
foreach 는 회마다 변수를 새로 만들도록
C# 5.0에서 고쳐졌습니다 — 그 전에는 이쪽도 2, 2, 2 였고,
가장 많이 신고된 함정이었습니다.
for 에서 회마다 다른 값을 붙잡으려면
안에서 복사합니다.
for (var i = 0; i < 3; i++) { var copy = i; // 회마다 새로 만들어집니다 list.Add(() => copy); } // 0, 1, 2
아예 붙잡지 못하게 막기
static 을 붙이면 바깥 변수를 붙잡을 수
없습니다(C# 9.0). 실수로 붙잡는 것을 컴파일러가 막아 줍니다.
var limit = 3000; Func<int, bool> ok = price => price <= limit; // 붙잡습니다 Func<int, bool> no = static price => price <= limit; // CS8820 // 정적 익명 함수는 'limit'에 대한 참조를 포함할 수 없습니다 Func<int, int> square = static x => x * x; // 받은 것만 사용합니다
붙잡을 것이 없으면 람다를 만들 때마다 객체를 새로 만들지 않아도 되므로 조금 가볍기도 합니다. 다만 주된 값어치는 성능이 아니라 의도를 못박는 것입니다 — "이 계산은 받은 것에만 기댄다" 를 컴파일러가 지켜 줍니다.
함수형 스타일이란
람다를 이어 붙여 값을 바꿔 나가는 방식으로 코드를 적는 것을 함수형 스타일이라고 합니다. 핵심은 문법이 아니라 바깥을 건드리지 않는 것입니다.
var total = 0; products.ForEach(p => { total += p.Price; // 바깥을 고칩니다 }); // total 이 어디서 바뀌는지 찾아다녀야 합니다
var total = products .Where(p => p.Usable) .Sum(p => p.Price); // total 은 여기서 한 번 정해지고 바뀌지 않습니다
오른쪽 람다들은 받은 것만 보고 값을 돌려줍니다. 바깥을 고치지 않으니 어디서 값이 바뀌었는지 찾을 일이 없고, 순서를 바꾸거나 나중에 병렬로 돌려도 결과가 같습니다. 3.3의 연산들이 전부 이렇게 만들어져 있습니다.
모든 코드를 이렇게 적으라는 뜻은 아닙니다. 값을 계산해 내는 자리에는 잘 맞고, 화면에 그리거나 파일에 담는 것처럼 바깥을 바꾸는 것이 목적인 자리는 그대로 적는 편이 낫습니다.
이 문법, 몇 버전부터 사용할 수 있나요
- 3.0람다식 — 그 자리에서 적는 이름 없는 메서드
- 5.0foreach 의 반복 변수를 회마다 새로 만들기
- 9.0static 람다 — 바깥 변수를 붙잡지 못하게
- 10.0자연 형식 — var 에 람다를 담기
C# 5.0의 변화는 문법이 아니라 동작을 고친 드문 경우입니다.
foreach 의 반복 변수를 회마다 새로 만들도록 바꿔
위의 함정 하나를 없앴습니다. 이미 나가 있는 코드의 결과가 달라지는 일이라
쉽게 결정하지 못했지만, 그 동작을 일부러 기대하고 적은 코드가 거의
없었기에 고칠 수 있었습니다. C# 10.0부터는
var f = (int x) => x * 2; 처럼 형식을 적지 않고
담을 수도 있습니다.
직접 해보기
Func<int, bool> 을 담는 사전을 만들어 이름으로 조건을
찾아 사용해보세요. "짝수"·"양수"·"두 자리"
셋을 담고, 각각으로 걸러낸 결과를 출력합니다.
아래 코드는 0초 뒤·1초 뒤·2초 뒤
를 출력하려던 것인데 세 번 모두 같은 값이 나옵니다. 까닭을
설명하고 고쳐보세요.
var jobs = new List<Action>(); for (var sec = 0; sec < 3; sec++) jobs.Add(() => Console.WriteLine($"{sec}초 뒤")); foreach (var job in jobs) job();
- 람다식은 사용하는 자리에서 바로 적는 이름 없는 메서드입니다.
=>왼쪽이 받을 것, 오른쪽이 할 일입니다. - 한 번만 사용하면 람다, 여러 곳에서 사용하거나 이름이 필요하면 메서드로 뺍니다.
- 바깥 변수를 값이 아니라 변수 자체로 붙잡습니다. 만든 시점이 아니라 부르는 시점의 값을 봅니다.
for의 반복 변수는 하나뿐이라 함께 붙잡힙니다. 안에서 복사하거나foreach를 사용하세요.static람다는 붙잡지 못하게 막습니다. 받은 것만 보고 값을 돌려주는 코드가 다루기 쉽습니다.
C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.