LINQ 는 새로운 문법이 아니다
Esc

    LINQ 는 새로운 문법이 아니다

    –C#

    점수 목록에서 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 가 돈다고요.