Quick Reference
키로 묶으면 GroupBy, 두 시퀀스를 키로 연결하면 Join, 중첩된 컬렉션을 한 줄 흐름으로 만들면 SelectMany, 결과 필드만 다시 구성하면 Select를 사용합니다. GroupBy는 IGrouping<TKey, TSource>의 흐름을, Join은 매칭된 쌍의 흐름을 만듭니다.
var byTeam = players.GroupBy(player => player.TeamId);
var playerTeams = players.Join(
teams,
player => player.TeamId,
team => team.Id,
(player, team) => new PlayerTeam(player.Name, team.Name));
var memberNames = teams.SelectMany(team => team.Members, (team, member) =>
new { Team = team.Name, Member = member.Name });- 조인에 매칭이 없는 왼쪽 항목까지 한 줄 결과로 남기려면
GroupJoin뒤에DefaultIfEmpty()를 붙입니다. - 쿼리를 API 경계 밖으로 보낼 때는 익명 타입 대신
record나class로 projection합니다. - LINQ to Objects와
IQueryable공급자는 같은 모양의 식도 실행 위치와 번역 가능 범위가 다를 수 있습니다.
GroupBy의 결과 읽기
GroupBy의 각 결과는 Key와 그 키에 속한 원소 흐름을 가진 IGrouping<TKey, TSource>입니다. 그룹별 통계는 그룹을 순회해 만듭니다.
var summaries = players
.GroupBy(player => player.TeamId)
.Select(group => new TeamSummary(
TeamId: group.Key,
MemberCount: group.Count(),
BestScore: group.Max(player => player.Score)));
foreach (var group in players.GroupBy(player => player.TeamId))
{
Console.WriteLine($"team: {group.Key}");
foreach (var player in group)
Console.WriteLine(player.Name);
}
public record TeamSummary(int TeamId, int MemberCount, int BestScore);LINQ to Objects의 GroupBy는 지연 실행이지만 첫 그룹을 만들기 전에 입력 시퀀스를 모두 읽어 그룹을 구성해야 합니다. 이미 만들어진 한 IGrouping을 Count()하고 다시 순회한다고 원본 시퀀스를 다시 읽는 것은 아닙니다. 다만 GroupBy 결과 전체를 다시 열거하면 쿼리 자체가 다시 실행될 수 있으므로, 같은 그룹 결과를 여러 번 쓸 때는 ToList()나 ToLookup()을 선택합니다.
var lookup = players.ToLookup(player => player.TeamId);
IEnumerable<Player> redTeam = lookup[1]; // 없는 키도 빈 시퀀스Join과 GroupJoin
Join은 양쪽 키가 같은 원소만 내보내는 inner join입니다. 한 키가 여러 번 나타나면 가능한 매칭 쌍이 모두 나옵니다. 문자열 키의 비교 규칙이 중요하면 비교자를 받는 오버로드를 사용합니다.
var matches = players.Join(
teams,
player => player.TeamId,
team => team.Id,
(player, team) => new PlayerTeam(player.Name, team.Name));
public record PlayerTeam(string PlayerName, string TeamName);GroupJoin은 각 왼쪽 원소에 대한 오른쪽 매칭 그룹을 남깁니다. 그 자체는 평탄한 left outer join이 아닙니다. DefaultIfEmpty와 SelectMany를 조합하면 매칭이 없는 왼쪽 원소도 한 행으로 만들 수 있습니다.
var allPlayers = players
.GroupJoin(
teams,
player => player.TeamId,
team => team.Id,
(player, teamGroup) => new { player, teamGroup })
.SelectMany(
pair => pair.teamGroup.DefaultIfEmpty(),
(pair, team) => new
{
Player = pair.player.Name,
Team = team?.Name ?? "unassigned",
});SelectMany와 projection
Select는 원소 하나를 결과 하나로 바꾸고, SelectMany는 원소 하나에서 나온 여러 원소를 한 시퀀스로 펼칩니다. 카테시안 곱처럼 결과 수가 빠르게 늘어날 수 있으므로 입력 크기와 필터 위치를 확인합니다.
var activeMemberNames = teams
.Where(team => team.IsActive)
.SelectMany(team => team.Members)
.Where(member => member.IsActive)
.Select(member => member.Name)
.ToList();익명 타입은 같은 어셈블리 안의 지역 처리에는 편하지만 public 반환 형식으로 선언할 수 없습니다. 컨트롤러 응답, 메시지, 서비스 경계의 projection은 이름 있는 record 또는 class로 만듭니다. 데이터베이스 공급자에 IQueryable<T>로 실행한다면 지원되지 않는 .NET 메서드를 중간에 넣어 메모리 평가가 일어나지 않는지도 확인합니다.
자주 틀리는 부분
GroupJoin만으로 left outer join의 평탄한 행이 만들어지지 않습니다.DefaultIfEmpty가 필요합니다.GroupBy는 반환 형식이Dictionary가 아닙니다. 키로 반복 조회할 결과면ToLookup또는ToDictionary의 중복 키 정책을 의식해 선택합니다.Join은 키가 중복된 입력에서 한 행만 고르는 API가 아닙니다. 모든 매칭 조합이 결과에 나옵니다.SelectMany의 결과 크기는 중첩 항목 수의 합 또는 조합 수만큼 커질 수 있습니다. 큰 입력에서는 먼저 필터링합니다.
참고 링크
3 sources