LINQ 는 새로운 문법이 아니다
점수 목록에서 60점 이상만 골라 높은 순으로 이름을 뽑는 코드입니다.
var 결과 = new List<string>();
foreach (var s in 학생들)
{
if (s.점수 >= 60)
결과.Add(s.이름);
}
결과.Sort();
한 줄로 쓸 수 있습니다.
var 결과 = 학생들.Where(s => s.점수 >= 60)
.OrderByDescending(s => s.점수)
.Select(s => s.이름);
새로 배울 것이 있어 보이지만 앞에서 다 나온 것들입니다.
Where와Select는 확장 메서드이고, 안은yield return이고, 넘기는 것은 람다입니다.
자주 쓰는 것만 추리면
| 하는 일 | 메서드 |
|---|---|
| 거르기 | Where |
| 모양 바꾸기 | Select |
| 줄 세우기 | OrderBy OrderByDescending ThenBy |
| 하나 꺼내기 | First FirstOrDefault Single |
| 개수와 합 | Count Sum Max Min Average |
| 확인 | Any All Contains |
| 묶기 | GroupBy ToDictionary |
| 굳히기 | ToList ToArray |
FirstOrDefault 와 First 의 차이는 알아 둘 만합니다.
First 는 없으면 예외를 던지고 FirstOrDefault 는 기본값을 돌려줍니다. 참조 타입이면 null 입니다.
없을 수도 있는 자리라면 FirstOrDefault 를 쓰고 null 검사를 합니다.
전부 확장 메서드다
List<T> 를 열어 봐도 Where 는 없습니다. System.Linq.Enumerable 이라는 static 클래스에 있습니다.
// 실제 모양에 가깝게 줄이면
public static IEnumerable<T> Where<T>(this IEnumerable<T> 원본, Func<T, bool> 조건)
앞에서 본 확장 메서드 그대로입니다. 첫 매개변수 앞에 this 가 있고, 담는 클래스가 static 입니다.
그래서 using System.Linq; 가 필요합니다. 이걸 안 적으면 Where 가 자동완성에 안 나옵니다.
확장 메서드는 네임스페이스를 using 해야 보인다고 한 그 규칙입니다.
그리고 받는 타입이 IEnumerable<T> 입니다. 앞에서 foreach 로 돌 수 있는 것이 그것이었습니다.
배열이든 List 든 Dictionary 든 우리가 yield 로 만든 것이든 전부 이 자리에 들어갑니다.
안은 yield 다
Where 를 직접 써 보면 이렇게 생겼습니다.
public static IEnumerable<T> 내Where<T>(this IEnumerable<T> 원본, Func<T, bool> 조건)
{
foreach (T x in 원본)
{
if (조건(x))
yield return x;
}
}
열 줄이 안 됩니다. 그리고 yield 니까 부른다고 실행되지 않습니다.
var q = 학생들.Where(s => s.점수 >= 60); // 아무도 안 걸렀습니다
foreach (var s in q) { ... } // 여기서 처음 돕니다
이게 LINQ 의 동작 전부를 설명합니다.
한 명씩 파이프를 통과한다
Where 다음에 Select 를 이으면 “전부 거르고 나서 전부 바꾸는” 것이 아닙니다.
한 명이 끝까지 갔다가 다음 사람이 출발합니다.
중간 리스트가 하나도 안 생깁니다. Where 결과를 담는 배열도, Select 결과를 담는 배열도 없습니다.
yield 라서 그렇습니다.
지연 실행이 만드는 함정 두 가지
하나. 돌 때마다 다시 계산합니다.
var 합격 = 학생들.Where(s => s.점수 >= 60);
Console.WriteLine(합격.Count()); // 한 번 돕니다
foreach (var s in 합격) { ... } // 또 돕니다
Console.WriteLine(합격.Count()); // 또 돕니다
세 번 썼으니 세 번 걸렀습니다. 두 번 이상 쓸 것이면 굳혀 둡니다.
var 합격 = 학생들.Where(s => s.점수 >= 60).ToList();
ToList 나 ToArray, Count, First, Sum 처럼 값을 하나 돌려주는 것들이 그 자리에서 전부 돕니다.
Where, Select, OrderBy 처럼 IEnumerable 을 돌려주는 것들은 안 돕니다. 이 둘을 가르면 헷갈릴 일이 줄어듭니다.
둘. 원본이 바뀌면 결과도 바뀝니다.
var 큰수 = 목록.Where(n => n > 10);
목록.Add(99);
foreach (var n in 큰수) // 99 가 들어 있습니다
거를 때 목록을 복사한 것이 아니라 돌 때 원본을 봅니다. 위에서 만든 것은 값이 아니라 질문입니다.
OrderBy 만 다르다
Where 와 Select 는 한 명씩 흘려보냅니다. 그런데 OrderBy 는 그럴 수 없습니다.
줄을 세우려면 전부 봐야 첫 번째가 누구인지 알 수 있습니다.
그래서 OrderBy 는 첫 값을 요구받는 순간 원본을 전부 읽어 배열로 만들고 정렬합니다.
할당이 한 번 일어난다는 뜻입니다. 그리고 무한 수열에 OrderBy 를 걸면 영원히 안 끝납니다.
쿼리 구문
같은 것을 SQL 처럼 쓸 수도 있습니다.
var 결과 = from s in 학생들
where s.점수 >= 60
orderby s.점수 descending
select s.이름;
컴파일러가 위의 메서드 호출로 바꿔 씁니다. 완전히 같은 코드입니다.
join 이나 여러 from 이 겹칠 때는 이쪽이 읽기 쉽고, 짧은 것은 메서드 쪽이 낫습니다. 취향껏 고르면 됩니다.
게임 루프에서는 조심한다
LINQ 는 짧아 보이지만 공짜가 아닙니다.
void Update()
{
var 가까운적 = 적들.Where(e => e.거리 < 10f)
.OrderBy(e => e.거리)
.FirstOrDefault();
}
이 한 줄에서 열거자 객체들과 람다를 담는 객체, OrderBy 의 정렬용 배열이 생깁니다.
한 번은 아무것도 아닙니다. 초에 예순 번 도는 자리에서는 쓰레기가 계속 쌓입니다.
앞에서 박싱을 볼 때와 같은 이야기입니다. 값이 비싸서가 아니라 횟수 때문입니다.
그래서 기준은 이렇게 잡습니다.
- 매 프레임 도는 자리는 손으로 쓴
for가 낫습니다 - 그 밖에는 LINQ 가 낫습니다. 읽기 쉬운 쪽이 대부분 이깁니다
먼저 재 보지도 않고 전부 for 로 바꾸는 것은 손해입니다.
정리
Where,Select는IEnumerable<T>에 붙은 확장 메서드입니다.using System.Linq가 필요합니다- 안은
yield라 부른다고 실행되지 않습니다. 돌아야 돕니다 - 한 명이 파이프를 끝까지 통과한 뒤 다음 사람이 출발합니다. 중간 리스트가 안 생깁니다
- 두 번 이상 쓸 것이면
ToList()로 굳힙니다. 안 그러면 쓸 때마다 다시 계산합니다 OrderBy만은 전부 읽어야 하므로 여기서 할당이 한 번 생깁니다- 매 프레임 도는 자리가 아니면 읽기 쉬운 쪽이 대개 이깁니다
LINQ 한 줄에서 열거자와 람다를 담는 객체가 매번 생긴다고 했습니다. 박싱에서도, 문자열에서도 같은 말을 했습니다. 쌓이면 GC 가 돈다고요.