{ }codelper

배우기 › C#

언어마다 다른 것 · 39

LINQ와 스트림

거르기·바꾸기·묶기·정렬을 반복문 대신 한 줄짜리 파이프라인으로 이어 써요.

이 단원은 Java · C#에서 배워요.
Python은 내포(컴프리헨션)로 같은 일을 하고(Python 단원 참고), JavaScript·TypeScript는 배열 메서드 filter·map·reduce를 이어 쓰고, C++20은 std::views::filter 같은 레인지를 써요.
C와 크레이콘은 반복문으로 해요.

C#의 LINQ와 Java의 스트림은 "고르고, 바꾸고, 묶고, 정렬하고, 합치는" 단계를 메서드로 이어 붙인 파이프라인이에요.
아래 예제는 단어 중 세 글자보다 긴 것만 남겨 길이별로 묶어 출력하고, 남은 글자 수를 모두 더해요.

using System;using System.Linq; class Program{    static void Main()    {        string[] words = Console.In.ReadToEnd()            .Split((char[])null, StringSplitOptions.RemoveEmptyEntries);         var longWords = words            .Where(w => w.Length > 3)            .ToList(); // 여기서 한 번 실행해 결과를 담아 둬요         var lines = longWords            .GroupBy(w => w.Length)            .OrderBy(g => g.Key)            .Select(g => $"{g.Key}: {string.Join(" ", g.Order())}");        foreach (string line in lines) Console.WriteLine(line);         int total = longWords.Sum(w => w.Length);        Console.WriteLine($"total letters: {total}");    }}
stdin
the quick brown fox jumps over the lazy dog quickly
stdout
4: lazy over
5: brown jumps quick
7: quickly
total letters: 30

핵심

  • 목록을 다루는 일은 대부분 거르기·바꾸기·묶기·정렬·합의 조합이에요.
    반복문 대신 그 단계를 차례로 적어요.
  • 단계마다 이름이 짝지어져 있어요.
    거르기 Where/filter, 바꾸기 Select/map처럼요.
  • 파이프라인은 만들 때가 아니라 결과를 꺼낼 때 실행돼요.
  • 결과를 여러 번 쓸 거면 목록으로 담아 둬요(ToList()/toList()).
  • 묶은 결과는 열쇠(키)와 그 무리로 나와요.
    키 순서가 필요하면 직접 정렬해요.

언어별로 쓰는 법

Javaimport java.util.stream.*; — 목록에서 words.stream()을 먼저 꺼내고 filter, map, sorted, Collectors.groupingBy, mapToInt(...).sum(), toList()(또는 collect(Collectors.toList())).
groupingBy는 기본이 HashMap이라 순서가 없어서, 예제는 groupingBy(String::length, TreeMap::new, Collectors.toList())로 키 순서를 맞춰요
C#using System.Linq; — Where(거르기), Select(바꾸기), OrderBy·ThenBy(그 자체로 정렬은 Order()), GroupBy, Sum, ToList().
GroupBy는 Key를 가진 그룹들을 처음 나온 순서대로 줘서 OrderBy(g => g.Key)로 정렬해요.
SQL 같은 질의 구문 from w in words where w.Length > 3 group w by w.Length도 컴파일하면 같은 메서드 호출이에요

자주 하는 실수

LINQ 질의를 만들어 두고 원본을 바꿨어요C#

var q = nums.Where(n => n > 1);
nums.Add(5);
Console.WriteLine(q.Count());

→ q는 결과가 아니라 "어떻게 거를지"만 기억해요(지연 실행). 새로 넣은 5까지 세고, foreach나 Count()를 할 때마다 처음부터 다시 돌아요

한 번 계산한 결과를 여러 번 쓰려면 ToList()로 담아 둬요.

같은 스트림을 두 번 썼어요Java

Stream<String> s = words.stream();
s.forEach(System.out::println);
long n = s.count();

→ IllegalStateException: stream has already been operated upon or closed — 스트림은 최종 연산(forEach, collect, sum 등)을 한 번만 할 수 있어요

다시 쓸 데이터는 목록으로 모아 두고, 필요할 때마다 stream()을 새로 꺼내요.

수를 Stream<Integer>로 다뤘어요Java

int sum = nums.stream().reduce(0, Integer::sum);

→ 수 하나하나가 Integer 객체로 포장(박싱)돼서 느리고, sum() 메서드도 없어요

mapToInt로 IntStream을 만들면 sum()·average()·max()를 바로 써요(다시 객체로는 boxed()).
C#의 IEnumerable<int>는 박싱이 없어요.

언어마다 다른 점
파이프라인은 "꺼내 쓸 때" 실행돼요.
C# 질의는 꺼낼 때마다 다시 돌고 Java 스트림은 한 번 쓰면 끝이니, 결과를 여러 번 쓸 거면 ToList()/toList()로 담아 두세요.

연습 문제