C# 6.0 완벽 가이드/ LINQ 질의 연산자

Contents

개요

  • 표준 질의 연산자들은 다음 세 범주로 나뉜다.
    • 순차열을 입력받고 순차열을 출력하는 연산자(순차열 -> 순차열)
    • 순차열을 입력받고 요소 하나 또는 스칼라값 하나늘 출력하는 연산자
    • 입력 없이 순차열을 출력하는 연산자(생성 메서드)

순차열->순차열

  • 이 범주의 질의 연산자는 하나 이상의 순차열을 입력받고 하나의 순차열을 산출한다. 대부분의 질의 연산자가 이 범주에 속한다. 아래 그림은 이 범주의 질의 연산자 중 순차열의 형태를 바꾸는 것들을 나타낸 것이다.

분류 형식 내용 연산자
필터링(선별) IEnumerable<TSource> -> IEnumerable<TSource> 원래 요소들의 부분집합을 출력한다. Where, Take, TakeWhile, Skip, SkipWhile, Distinct
투영 IEnumerable<TSource> -> IEnumerable<TResult> 주어진 람다 함수를 이용해서 각 요소를 변환한다. SelectMany는 중첩된 순차열을 평평한 순차열로 만든다(평탄화).

LINQ to SQL이나 EF에 대한 Select와 SelectMany는 내부 결합(inner join), 왼쪽 외부 결합(left outer join), 교차 결합(cross join), 비등가 결합(non-equi join)을 수행한다.

Select, SelectMany
결합 IEnumerable<TOuter>, IEnumerable<TInner> -> IEnumerable<TResult>
IEnumerable<TFirst>, IEnumerable<TSecond> -> IEnumerable<TResult>
두 순차열의 요소들을 합친다. Join과 GroupJoin 연산자는 지역 질의에 효율적으로 작동하도록 설계된 것으로, 내부 결합과 왼쪽 외부 결합을 지원한다.

Zip 연산자는 두 순차열을 동시에 열거하면서 각 요소 쌍에 함수를 적용한다. Zip 연산자에서는 두 형식 매개변수의 이름이 TOuter와 TInner가 아니라 TFirst와 TSecond이다.

Join, GroupJoin, Zip
정렬 IEnumerable<TSource> -> IOrderedEnumerable<TSource> 입력 순차열 요소들의 순서를 바꾼다. OrderBy, ThenBy, Reverse
그룹화 IEnumerable<TSource> -> IEnumerable<IGrouping<TKey, TElement>> 입력 순차열의 요소들을 적절히 묶어서 여러 개의 부분 순차열들을 출력한다. GroupBy
집합 연산 IEnumerable<TSource>, IEnumerable<TSource> -> IEnumerable<TSource> 같은 형식의 순차열 두 개를 입력 받아서 합집합, 교집합, 차집합을 출력한다. Concat, Union, Intersect
변환 메서드: 가져오기 IEnumerable -> IEnumerable<TResult> OfType, Cast
변환 메서드: 내보내기 IEnumerable<TSource> -> 배열, 목록, 사전, 조회 객체(lookup), 순차열 ToArray, ToList, ToDictionary, ToLookup, AsEnumerable, AsQueryable

순차열->요소 또는 값

  • 이 범주의 질의 연산자들은 순차열 하나를 입력 받아서 하나의 요소 또는 값을 출력 한다.
분류 형식 내용 연산자
요소 연산자 IEnumerable<TSource> -> TSource 순차열에서 특정 요소 하나를 선택해서 출력한다. First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault, ElementAt, ElementAtOrDefault, DefaultIfEmpty
집계 메서드 IEnumerable<TSource> -> scalar 입력 순차열의 요소들에 대해 특정한 계산을 수행한 후 하나의 스칼라값(보통은 수치 형식)을 돌려준다. Aggregate, Average, Count, LongCount, Sum, Max, Min
한정사(quantifier, 양화사) IEnumerable<TSource> -> bool true 또는 false를 돌려준다. All, Any, Contains, SequenceEqual

 

void->순차열

  • 입력 없이 순차열을 출력한다.
분류 형식 내용 연산자
생성 메서드 void -> IEnumerable<TResult> 간단한 순차열을 출력한다. Empty, Range, Repeat

 

필터링

  • IEnumerable<TSource> -> IEnumerable<TSource>
메서드 설명 해당 SQL 구문
Where 주어진 조건을 만족하는 요소들로만 이루어진 결과 집합을 돌려준다. WHERE
Take 처음 Count개의 요소를 취하고 나머지는 버린다. WHERE ROW_NUMBER()… 또는 TOP n 부분 질의
Skip 처음 Count개의 요소를 무시하고 나머지를 돌려준다. WHRE ROW_NUMBER()… 또는 NOT IN (SELECT TOP n…)
TakeWhile 술어가 거짓이 될 때까지만 요소들을 취한다. (예외 발생)
SkipWhile 술어가 거짓이 될 때까지만 요소들을 무시하고 나머지 요소들을 돌려준다. (예외 발생)
Distinct 중복 요소들을 제거한 순차열을 돌려준다. SELECT DISTINCT…

 

  • 이번 절에서 소개하는 필터링 연산자들은 항상 입력 순차열과 같은 또는 더 적은 수의 요소들을 출력한다. 요소가 더 많아지는 경우는 없다. 또한 출력된 요소들은 입력 순차열에 있는 것과 동일하다. 즉, 이 연산자들은 그 어떤 변환도 수행하지 않는다.

Where

개요

  • Where는 입력 순차열의 요소 중 주어진 술어를 만족하는 요소들을 돌려준다.
    • (기본 예시 생략)
  • 한 질의 안에 where 절이 여러 개 있을 수 있으며 여러 where 절 사이에 let이나 orderby, join 절이 끼어들 수 있다.
from n in names
where n.Length > 3
let u = n.ToUpper()
where u.EndWith("Y")
select u;

// 결과: { "HARRY", "MARY" }

색인화된 필터링

  • Where에 int 형식의 두 번째 매개변수를 받는 술어를 지정할 수 있다. 그 매개변수에는 입력 순차열 안에서의 현재 요소의 위치(색인)가 전달된다. 이는 필터링 판정 시 요소의 위치를 고려해야 할 때 유용하다. 예컨대 다음 질의는 모든 두 번째 요소(색인이 홀수인 요소)를 건너뛴다.
    • LINQ to SQL이나 EF에 이런 색인화된 필터링을 적용하면 예외가 발생한다.
IEnumerable<string> query = names.Where((n, i) => i % 2 == 0);

// 결과: { "Tom", "Harry", "Jay" }

LINQ to SQL과 EF의 SQL LIKE 비교 구문

  • string에 대한 다음 메서드들은 SQL의 LIKE 연산자에 대응된다.
    • Contains, StartsWith, EndsWith
  • 예컨대 c.Name.Contains(“abc”)는 customer.Name LIKE ‘%abc%’으로 바뀐다.
  • Contains로는 지역에서 평가되는 표현식과의 비교만 가능하다. 다른 열과의 비교를 위해서는 SqlMethods.Like 메서드를 사용해야 한다.
... where SqlMethods.Like (c.Description, "%" + c.Name + "%")
  • 또한 SqlMethods.Like를 이용하면 좀 더 복잡한 비교도 수행할 수 있다. (이를테면 LIKE ‘abc%def%’ 등)

LINQ to SQL과 EF의 문자열 순서 비교 (<, >)

  • CompareTo 메서드를 이용하면 string 인스턴스들의 순서를 비교할 수 있다. 이는 SQL의 <, > 연산자에 대응된다.
dataContext.Purchases.Where(p => p.Description.CompareTo("C") < 0)

LINQ to SQL과 EF의 WHERE x IN (…, …, …) 구문

  • LINQ to SQL과 EF에서는 필터 술어 안에서 지역 컬렉션에 대해 Contains 연산자를 적용할 수 있다. 예컨대 다음과 같다.
string[] chosenOnes = { "Tom", "Jay" };

from c in dataContext.Customers
where chosenOnes.Contains (c.Name)
...
  • 이는 SQL의 IN 연산자에 대응된다. 즉, 위의 WHERE 절은 다음과 같은 SQL 문구가 된다.
WHERE customer.Name IN ("Tom", "Jay")
  • 지역 컬렉션이 엔티티들의 배열이나 비스칼라 형식의 배열이면 LINQ to SQL이나 EF가 EXISTS 절을 산출할 수도 있다.

Take와 Skip

  • Take는 처음 n개의 요소를 출력하고 나머지는 버린다. Skip은 처음 n개의 요소를 버리고 나머지를 출력한다.
    • (이하 예시 생략)
  • LINQ to SQL과 EF는 Take와 Skip을 SQL Server 2005의 ROW_NUMBER 함수로 옮긴다. 단, 그 이전 버전의 SQL Server에서는 TOP n 부분 질의로 옮긴다.

TakeWhile과 SkipWhile

  • TakeWhile은 입력 순차열을 열거하면서 주어진 술어가 참인 동안만 요소들을 출력하고 주어진 술어가 거짓이 되면 출력을 마친다.
    • (이하 예시 생략)
  • SkipWhile은 입력 순차열을 열거하면서 주어진 술어가 참인 동안은 요소들을 버리고, 거짓이 되면 나머지 요소들을 출력한다.
    • (이하 예시 생략)
  • TakeWhile과 SkipWhile에 대응되는 SQL 구문은 없다. DB 대상 LINQ 질의에 이들을 사용하면 예외가 발생한다.

Distinct

  • Distinct는 입력 순차열에서 중복 요소들을 제거한 결과를 출력한다. 원한다면 커스텀 비교자를 인수로 지정할 수 있다. 다음은 문자열의 서로 다은 영문자를 돌려주는 예이다.
char[] distinctLetters = "HelloWorld".Distinct().ToArray();
string s = new string(distinctLetters);  // HeloWrd

투영

  • IEnumerable<TSource> -> IEnumerable<TResult>
메서드 설명 해당 SQL 구문
Select 주어진 람다식을 이용해서 각 입력 요소를 변환한다. SELECT
SelectMany 주어진 람다식을 이용해서 각 입력 요소를 변환하고 그 결과로 생긴 부분 순차열들을 평탄화하고 연결한다. INNER JOIN, LEFT OUTER JOIN, CROSS JOIN

개요

  • Select는 항상 입력 순차열과 같은 개수의 요소들을 돌려준다. 단, 각 요소를 람다 함수를 이용해서 임의의 방식으로 변환할 수 있다.
  • 다음은 컴퓨터에 설치된 모든 글꼴(font) 이름을 선택하는 예이다.
IEnuemrable<string> query = from f in FontFamily.Families 
  select f.Name;

foreach (string name in query) Console.WriteLine(name);
  • 이 예에서 select 절은 FontFamily 객체를 해당 글꼴 이름으로 변환한다. 다음은 이 질의를 람다식을 이용해서 다시 작성한 것이다.
IEnumerable<string> query = FontFamily.Families.Select(f => f.Name);
  • 다음 예처럼 Select를 익명 형식으로의 투영에 사용하는 경우도 많다.
var query =
  from f in FontFamily.Families
  select new { f.Name, LineSpacing = f.GetLineSpacing(FontStyle.Bold) };
  • 질의 구문에서는 하나의 질의가 반드시 select나 group절로 끝나야 한다는 요구조건을 만족하기 위해 아무런 변환도 수행하지 않는 투영을 사용하기도 한다. 다음은 취소선(strikeout)을 지원하는 글꼴들을 선택하는 예이다.
IEnumerable<FontFamily> query = 
  from f in FontFamily.Families
  where f.IsStyleAvailable(FontStyle.Strikeout)
  select f;

foreach (FontFamily ff in query)
  Console.WriteLine(ff.Name);
  • 질의 구문을 유창한 구문으로 옮길 때 컴파일러는 이런 투영을 그냥 무시한다.

색인화된 투영

  • 하나의 select 절에 부분 질의를 내포해서 객체 계통구조를 형성하는 것이 가능하다.
    • 다음 예는 D:\source의 하위 디렉터리들을 서술하는 컬렉션을 돌려주는데, 그 컬렉션의 각 항목은 해당 하위 디렉터리에 있는 파일들을 서술하는 또 다른 컬렉션이다.
DirectoryInfo[] dirs = new DirectoryInfo(@"d:\source").GetDirectories();

var query = 
  from d in dirs
  where (d.Attributes & FileAttributes.System) == 0
  select new
  {
    DirectoryName = d.FullName,
    Created = d.CreateTime,
    Files = from f in d.GetFiles()
               where (f.Attributes & FileAttributes.Hidden)  == 0
               select new { FileName = f.Name, f.Length, }
  };

foreach (var dirFiles in query)
{
  Console.WriteLine("Directory: " + dirFiles.DirectoryName);
  foreach (var file in dirFiles.Files)
    Console.WriteLine (" " + file.FileName + " Len: " + file.Length);
}
  • 이 질의의 안쪽 부분을 상관 부분 질의(correlated subquery)라고 부른다. 상관 부분 질의는 바깥쪽 질의(외부 질의)의 객체를 참조하는 부분 질의이다. 지금 예에서는 부분 질의가 외부 질의의 d(현재 열거 중인 디렉터리)를 참조한다.
  • Select 안의 부분 질의에서 한 객체 계통구조를 다른 객체 계통구조로 투영하거나 관계형 객체 모형을 계통적(계통구조 형태의) 객체 모형으로 투영하는 것도 가능하다.
  • 지역 질의의 Select 에 부분 질의가 있으면 실행이 이중으로 지연된다. 지금 예에서는 안쪽 foreach 문이 열거되어야 비로소 파일들이 선별 또는 투영된다.

LINQ to SQL과 EF의 부분 질의와 결합

  • LINQ to SQL과 EF에서도 부분 질의 투영이 잘 작동한다. 이 경우 SQL 스타일의 결합(join)을 수행하는 목적으로 부분 질의를 활용할 수 있다.
var query =
  from c in dataContext.Customers
  select new 
  {
    c.Name,
    Purchases = from p in dataContext.Purchases
                        where p.CustomerID == c.ID && p.Price > 1000
                        select new { p.Description, p.Price }
  }

foreach (var namePurchases in query)
{
  Console.WriteLine("Customer: " + namePurchases.Name);
  foreach (var purchaseDetail in namePurchases.Purchases)
    Console.WriteLine(" - $$$: " + purchasesDetail.Price);
}
  • 이런 스타일의 질의는 해석식 질의에 아주 적합하다. 외부 질의와 부분 질의가 한 단위로 처리되므로 불필요한 통신 왕복이 발생하지 않는다. 그러나 지역 질의에서는 이런 방식이 비효율적이다. 최종 결과에 포함되는 요소가 몇 개 되지 않더라도, 그 요소들을 산출하려면 외부 순차열 요소들과 내부 순차열 요소들의 모든 조합을 열거해야 하기 때문이다. 지역 질의에서는 Join이나 GroupJoin을 사용하는 것이 더 낫다.
  • 이 질의는 서로 다른 두 컬렉션의 요소들을 묶어서 출력한다. 따라서 이를 SQL의 결합(join) 연산으로 볼 수 있다. 단 통상적인 데이터베이스 결합 연산과 달리 이 질의는 출력 순차열을 하나의 2차원 결과 집합으로 평탄화 하지 않는다. 즉, 관계형 자료를 계통적 자료로 투영하는 것이지 평평한 구조의 자료로 투영하는 것이 아니다.
  • 다음은 이 질의를 Customer 엔티티에 대한 Purchases 연관 속성을 이용해서 좀 더 간단하게 표기한 것이다.
var query =
  from c in dataContext.Customers
  select new 
  {
    c.Name,
    Purchases = from p in c.Purchases
                        where p.CustomerID == c.ID && p.Price > 1000
                        select new { p.Description, p.Price }
  }
  • 두 질의 모두 바깥쪽 열거에서 고객의 구매 레코드와는 무관하게 모든 고객을 얻게 된다는 점에서 SQL의 왼쪽 외부 결합(left outer join)에 해당한다고 할 수 있다.
    • 내부 결합(inner join)을 흉내내려면 구매 컬렉션에 필터 조건을 추가해야 한다.
var query =
  from c in dataContext.Customers
  where c.Purchases.Any(p => p.Price > 1000)
  select new 
  {
    c.Name,
    Purchases = from p in c.Purchases
                        where p.CustomerID == c.ID && p.Price > 1000
                        select new { p.Description, p.Price }
  }
  • 그런데 같은 술어(price > 1000)가 두 번 나온다는 점에서 코드가 다소 지저분하다. let 절을 이용하면 중복 코드를 제거할 수 있다.
var query =
  from c in dataContext.Customers
  let highValueP = from p in c.Purchases
                            where p.Price > 1000
                            select new { p.Description, p.Price }
  where highValueP.Any()
  select new { c.Name, Purchases = highValueP };
  • 이런 스타일의 질의는 유연하다. 예컨대 Any를 Count로 바꾸면 고가 구매가 2건 이상인 고객들만 조회할 수 있다.
var query =
  from c in dataContext.Customers
  let highValueP = from p in c.Purchases
                            where p.Price > 1000
                            select new { p.Description, p.Price }
  where highValueP.Count() >= 2
  select new { c.Name, Purchases = highValueP };

구체 형식으로의 투영

  • 익명 형식으로의 투영은 중간 결과를 얻는데는 유용하지만, 결과를 클라이언트에 전달하는데는 그리 유용하지 않다. 익명 형식은 오직 메서드 안의 지역 변수로만 존재할 수 있기 때문이다. 대안은 DataSet류 클래스나 커스텀 업무 엔티티 클래스 같은 구체적인 형식으로 투영하는 것이다.
    • 커스텀 업무 엔티티 클래스(business entity)는 프로그래머가 몇몇 속성을 직접 작성해서 만든 엔티티 클래스로, LINQ to SQL에서 [Table] 특성을 부여한 클래스나 EF의 엔티티 클래스와 비슷하되 저수준(데이터베이스 관련) 세부사항을 숨기는 것을 목적으로 한다.
    • 예컨대 업무 엔티티 클래스에서는 외래 키 필드들을 배제할 수 있다.
IQueryable<CustomerEntity> query =
  from c in dataContext.Customers
  select new CustomerEntity
  {
    Name = c.Name,
    Purchases = (from p in c.Purchases
                        where p.Price > 1000
                        select new PurchaseEntity { Description = p.Description, Value = p.Price }).ToList()
  };
  • 지금까지는 Join이나 SelectMany 문을 사용할 필요가 없었다. 이는 아래 그림에 나온 자료의 계통적 형태를 계속 유지했기 때문이다. 전통적인 SQL 접근방식에서는 테이블들을 평탄화해서 2차원 결과 집합으로 만들어야 했지만, LINQ에서는 굳이 그럴 필요가 없는 경우가 많다.

SelectMany

개요

  • SelectMany는 부분 순차열들을 하나의 평평한 순차열로 연결해서 출력한다. Select의 출력 순차열은 입력 순차열과 같은 개수의 요소들로 구성된다. 반면 SelectMany의 출력 순차열은 요소가 0..n개(0개 이상, n개 이하)이다. 그 0..n개의 요소들은 람다식이 만들어 낸 부분 순차열 또는 자식 순차열에서 온 것이다.
  • SelectMany를 이용하면 자식 순차열들을 확장(전개)하거나, 중첩된 컬렉션들을 평평하게 만들거나, 두 컬렉션을 평평한 출력 순차열로 결합할 수 있다. 컨베이어 벨트에 비유하자면 SelectMany는 여러 원자재를 하나의 컨베이어 벨트로 투입하는 깔때기라 할 수 있다.
    • SelectMany에서 입력 순차열의 각 요소는 원자재의 유입을 촉발한다. SelectMany의 selector 표현식은 입력 순차열의 요소마다 하나의 자식 순차열을 출력해야 한다. SelectMany의 최종 결과는 입력 요소마다 산출된 자식 순차열들을 모두 연결해서 하나의 순차열로 만든 것이다.
  • 다음과 같은 이름의 배열이 있다고 하자.
string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" };
  • 목표는 이 이름들을 구성하는 모든 단어가 평평하게 나열된 컬렉션을 얻는 것이다.
    • 이 과제에서는 입력 요소를 가변적인 개수의 출력 요소들에 대응시켜야 한다. SelectMany는 이런 종류의 작업에 아주 적합하다. 각 입력 요소를 자식 순차열로 변환하는 selector 표현식만 지정해 주면 된다. 그런 용도로 적합한 것이 string.Split 메서드이다.
    • 다음은 이 메서드를 이용한 SeleceMany 질의와 그 결과이다.
IEnumerable<string> query = fullNames.SelectMany(name => name.Split());

foreach(string name in query)
  Console.WriteLine(name + "|");  // 결과는 Anne|Williams|John|Fred|Smith|Sue|Green|
  • 질의 구문도 SelectMany의 기능을 지원한다. 질의에 생성기(generator)를 추가하면, 다시 말해 또 다른 from 절을 두면 된다.
    • 질의 구문에서 from 키워드는 두 가지 의미로 쓰인다. 질의의 제일 처음 부분에 나오는 from 은 기본 범위 변수와 입력 순차열을 도입하는 역할을 한다.  그 외의 모든 장소에 있는 from 은 SelectMany로 번역된다.
    • 다음은 앞의 질의를 질의 구문으로 표현한 것이다.
IEnumerable<string> query = 
  from fullName in fullNames
  from name in fullNames.Split()  // SelectMany로 번역 됨
  select name;
  • 추가 생성기가 새로운 범위 변수를 도입함을 주목하기 바란다. 지금 예에서는 name이 바로 그것이다. 기존 범위 변수도 여전히 유효하며, 두 변수 모두 이후의 표현식에서 접근할 수 있다.

다수의 범위 변수

  • 앞의 예제에서 변수 name과 fullName은 둘 다 질의의 끝까지 유효한 범위에 있다. 이처럼 변수들의 범위가 연장된다는 점은 유창한 구문에 비한 질의 구문의 결정적인 장점이다.
IEnumerable<string> query = 
  from fullName in fullNames
  from name in fullNames.Split()  // SelectMany로 번역 됨
  select name + 의 성명은 " + fullName;

// 결과
// Anne 의 섬영은 Anne Williams
// Williams의 성명은 Anne Williams
// John의 성명은 John Fred Smith
// ...
  • 최종 투영 문구에서 두 변수 모두에 접근할 수 있는 것은 내부적으로 컴파일러가 마법을 부린 덕분이다. 이것이 얼마나 편리한 특혜인지는 같은 질의를 유창한 구문으로 옮겨보면 실감할 수 있다. 만일 투영 앞에 where 절이나 orderby 절을 삽입한다면 유창한 구문으로 번역하기가 더욱 어려워진다.
  • 문제는 SelectMany가 자식 순차열들을 연결해서 평평하게 한 순차열을 출력한다는 것이다. 그 과정에서 출력 요소들이 비롯된 원래의 ‘외부’ 요소(fullName)은 사라진다. 해결책은 임시적인 익명 형식을 이용해서 각 자식 요소가 외부 요소를 ‘달고 다니게’ 만드는 것이다.
IEnumerable<string> query = 
  from fullName in fullNames
  from x in fullNames.Split().Select(name => new { name, fullName })
  orderby x.fullName, x.name
  select x.name + "의 성명은 " + x.fullName;
  • 이전과 다른 점은 각 자식 요소(name)를 외부 요소(fullName)와 함께 하나의 익명 형식으로 감싼다는 것 뿐이다. 이는 let 절이 환원되는 방식과 비슷하다. 다음은 이 질의를 유창한 구문으로 옮긴 최종 버전이다.
IEnumerable<string> query = fullNames
  .SelectMany(fName => fName.Split().Select(name => new { name, fName }))
  .OrderBy(x => x.fName)
  .ThenBy(x => x.name)
  .Select(x => x.name + "의 성명은 " + x.fName);

질의 구문으로 생각하기

  • 범위 변수가 여러 개인 질의는 질의 구문을 이용해서 작성하는 것이 여러모로 유리하다. 그런 경우 질의 구문은 질의를 구체적으로 작성할 때뿐만 아니라, 애초에 질의를 머릿속에서 구상하고 고찰하는데도 도움이 된다.
  • 추가 생성기를 작성하는 패턴은 기본적으로 두가지다. 첫 번째 패턴은 부분 순차열들의 확장과 평탄화이다. 추가 생성기 안에서 기존 범위 변수에 대해 속성 또는 메서드를 호출하는 것이 이 패턴에 해당한다. 실제 앞의 예제에서 이 패턴을 사용했다.
from fullName in fullNames
from name in fullNames.Split()
  • 이에 의해 전체 이름이 단어들의 배열로 확장된다. DB 대상 LINQ 질의에서는 자식 연관 관계 속성을 확장하는 것이 이 패턴에 해당한다. 다음 질의는 모든 고객을 해당 구매 레코드와 함께 나열한다.
IEnumerable<string> query = 
  from c in dataContext.Customers
  from p in c.Purchases          
  select c.Name + "의 구매 상품: " + p.Description;
  • 이 질의는 각 고객을 구매 레코드들의 부분 순차열로 확장한다.
  • 둘째 패턴은 데카르트 곱(cartesian product) 또는 교차 결합(cross join)을 수행하는 것, 다시 말해 한 순차열의 모든 요소를 각각 다른 순차열의 모든 요소와 대응 시키는 것이다. 범위 변수와 무관한 selector 표현식을 돌려주는 추가 생성기를 도입하는 것이 이 패턴에 해당한다.
int[] numbers = { 1, 2, 3 };
string[] letters = { "a", "b" };

IEnumerable<string> query = 
  from n in numbers
  from l in letters
  select n.ToString() + l;

// 결과: { "1a", "1b", "2a", "2b", "3a", "3b" }
  • 이런 스타일의 질의는 SelectMany 스타일 결합의 기초가 된다.

SelectMany 스타일의 결합

  • SelectMany를 이용해서 두 순차열을 결합하는 간단한 방법은 교차 결합의 결과를 필터링하는 것이다.
    • 예컨대 모든 선수가 다른 모든 선수와 한 번씩 시합을 한다고 하자. 다음은 모든 대진 조합을 출력하는 질의의 첫 버전이다.
string[] players = { "Tom", "Jay", "Mary" };

IEnumerable<string> query =
  from name1 in players
  from name2 in players
  select name1 + " vs " + name2;

// 결과 { "Tom vs Tom", "Tom vs Jay", "Tom vs Mary", "Jay vs Tom" ... }
  • 그런데 이 질의는 교차 결합이므로 자신과의 시합들과 두 선수의 순서만 다른 시합들이 중복되어 나온다. 제대로된 결과를 얻으려면 다음처럼 필터를 추가해야 한다.
string[] players = { "Tom", "Jay", "Mary" };

IEnumerable<string> query =
  from name1 in players
  from name2 in players
  where name1.CompareTo(name2) < 0
  orderby name1, name2
  select name1 + " vs " + name2;

// 결과 { "Jay vs Mary", "Jay vs Tom", "Mary vs Tom" }
  • 이 경우 필터 술어는 결합 조건(join condition)으로 작용한다. 결합 조건에 상등 연산자가 쓰이지 않았다는 점에서, 이 질의를 비동가 질의(non-equi join)라고 불러도 좋을 것이다.

LINQ to SQL과 EF의 SelectMany

  • LINQ to SQL과 EF의 SelectMany로 교차 결합, 비등가 결합, 내부 결합, 왼쪽 외부 결합을 수행할 수 있다. Select와 마찬가지로 SelectMany는 미리 정의된 연관 관계뿐만 아니라 임시적인 관계도 지원한다. Select와의 차이점은 SelectMany는 계통적 형태의 결과 집합이 아니라 평평한 결과 집합을 돌려준다는 것이다.
  • DB 대상 LINQ의 교차 결합을 작성하는 방법은 이미 앞 절에서 보았다. 다음 질의는 모든 고객과 모든 구매 레코드의 모든 쌍을 나열한다 (교차 결합)
var query = 
  from c in dataContext.Customers
  from p in dataContext.Purchases
  select c.Name + "의 구매 상품(아마도): " + p.Description;
  • 그런데 실제 응용에서는 각 고객을 고객 자신의 구매 레코드들에만 대응시키는 것이 일반적이다. 그렇게 하려면 결합용 술어가 있는 where 절을 추가하면 된다. 그러면 다음과 같은 표준적인 SQL 스타일 등가 결합 질의가 만들어 진다.
var query = 
  from c in dataContext.Customers
  from p in dataContext.Purchases
  where c.ID == p.CustomerID
  select c.Name + "의 구매 상품(아마도): " + p.Description;
  • 엔티티들의 관계에 대한 연관 속성이 엔티티 클래스에 존재한다면, 교차 결합 결과를 필터링하는 질의 대신 자식 컬렉션들을 확장하는 형태의 질의로도 같은 결과를 얻을 수 있다.
var query = 
  from c in dataContext.Customers
  from p in c.Purchases
  select new { c.Name, p.Description };
  • EF의 엔티티는 외래 키를 노출하지 않으므로, 알려진 관계들을 활용하려면 반드시 연관 속성을 사용해야 한다. 앞에서 한 것처럼 직접 결합을 수행할 수는 없다.
  • 이 방식의 장점은 결합 조건에 해당하는 술어를 생략할 수 있다는 점이다. 교차 결합을 필터링하는 대신 자식 컬렉션들을 확장하고 평탄화 했기 때문이다. 그러나 궁극적으로 두 질의는 모두 동일한 SQL 문으로 환원된다.
  • 확장/평탄화 질의에 추가적인 필터링을 위해 where 절을 도입하는 것도 가능하다.
    • 예컨대 이름이 ‘T’로 시작하는 고객들만 조회하고 싶다면 다음과 같이 필터를 추가하면 된다.
var query = 
  from c in dataContext.Customers
  where c.Name.StartWith("T")
  from p in c.Purchases
  select new { c.Name, p.Description };
  • DB 대상 LINQ의 질의에서 where 절을 한 줄 아래로 옮겨도 차이가 생기지 않는다. 그러나 지역 질의에서는 where 절을 한 줄 아래로 옮기면 효율성이 떨어진다. 지역 질의에서는 필터링을 결합 전에 수행하는 것이 바람직하다.
  • 또 다른 from 절을 이용해서 새로운 테이블을 도입할 수도 있다. 예컨대 구매 레코드마다 구매 항목 정보를 담은 자식 행들이 있다고 하자. 다음은 고객별 구매 정보와 구매 항목 상세 정보를 담은 평ㄴ평한 형태의 순차열을 돌려주는 질의이다.
var query = 
  from c in dataContext.Customers
  from p in c.Purchases
  from pi in p.PurchasesItems
  select new { c.Name, p.Description, pi.DetailLine };
  • 각 from 절은 새로운 자식 테이블을 도입한다. 부모 테이블의 자료를 포함시킬 때에는 from 절을 추가할 필요가 없다. 그냥 해당 연관 속성에 접근하면 된다.
    • 예컨대 고객마다 전담 영업사원이 있다고 할 때 다음은 고객 이름과 해당 영업사원 이름의 쌍들을 출력하는 질의이다.
var query = 
  from c in dataContext.Customers
  select new { Name = c.Name, SalesPerson = c.SalesPerson.Name };
  • 이 경우에는 평탄화할 부분 컬렉션들이 없으므로 SelectMany를 사용할 필요가 없다. 부모 연관 속성은 하나의 항목을 돌려준다.

SelectMany를 이용한 외부 결합

  • Select 부분 질의가 왼쪽 외부 결합에 해당하는 결과를 돌려준다는 점은 이미 앞에서 이야기했다.
var query = 
  from c in dataContext.Customers
  select new { 
    c.Name,
    Purchases = from p in c.Purchases
                       where p.Price > 1000
                       select new { p.Description, p.Price }
  };
  • 이 질의의 결과에는 모든 외부 요소(고객 정보)가 포함된다. 즉, 구매 기록이 하나도 없는 고객들까지 모두 포함되는 것이다. 이 질의를 SelectMany 스타일로 다시 작성하면 계통적인 결과 집합이 아니라 하나의 평평한 컬렉션을 얻게 된다.
var query = 
  from c in dataContext.Customers
  from p in c.Purchases
  where p.Price > 1000
  select new { c.Name, p.Description, p.Price };
  • 질의를 다시 작성하는 과정에서 결합의 종류가 왼쪽 외부 결합이 아니라 내부 결합으로 바뀌었다. 즉, 이제는 고가 구매 기록이 있는 고객들만 결과에 포함된다.
    • 왼쪽 외부 결합에서 이런 평평한 결과를 얻으려면 반드시 내부 순차열에 대해 DefaultIfEmpty 질의 연산자를 적용해야 한다. DefaultIfEmpty 메서드는 만일 입력 순차열에 요소가 하나도 없으면 널 요소가 하나 있는 순차열을 돌려준다.
    • 다음은 DefaultIfEmpty를 적용한 질의이다.
var query = 
  from c in dataContext.Customers
  from p in c.Purchases.DefaultIfEmpty()
  select new { c.Name, p.Description, Price = (decimal?) p.Price };
  • LINQ to SQL과 EF에서는 이 질의가 완벽하게 작동해서 구매 기록이 없는 고객들까지 포함해서 모든 고객을 돌려준다. 그런데 이 질의를 지역 질의로 실행하면 예외가 발생할 수 있다. 만일 p가 널이면 p.Description과 p.Price가 NullReferenceException을 던진다.
    • (예외처리 코드 생략)
  • 이제 가격 필터를 다시 도입하자. 이전처럼 그냥 where 절을 추가해서는 안된다. 그렇게 하면 DefaultIfEmpty 이후에 필터가 적용될 것이기 때문이다. 제대로 된 해결책은 부분 질의를 이용해서 where 절을 DefaultIfEmpty 앞에 두는 것이다.
var query = 
  from c in dataContext.Customers
  from p in c.Purchases.Where(p => p.Price > 1000).DefaultIfEmpty()
  select new { 
    c.Name, 
    Descript = p == null ? null : p.Description, 
    Price = p == null ? (decimal?) null : p.Price 
  };
  • LINQ to SQL과 EF는 이를 왼쪽 외부 결합으로 바꾼다. 왼쪽 외부 결합을 수행하고 싶을 때는 이 질의의 패턴을 따르는 것이 효과적이다.
  • SQL에서 외부 결합을 작성하는데 익숙한 독자라면 이런 스타일의 질의를 작성할 때 더 간단한 형태의 Select 부분 질의 대신 어색한, 그러나 SQL 경험자에게는 친숙한 ‘평평한 컬렉션’ 접근방식 쪽으로 마음이 쏠릴 가능성이 있다. 그러나 외부 결합 스타일의 질의에는 Select 부분 질의가 돌려주는 계통적 결과 집합이 더 나은 경우가 많다. 그런 방식에서는 추가적인 널들을 다룰 필요가 없기 때문이다.

결합

메서드 설명 해당 SQL 구문
Join 두 컬렉션의 요소들을 주어진 조회 전략(lookup strategy)을 이용해서 짝짓고, 그 결과를 평평한 형태로 출력한다. INNER JOIN
GroupJoin Join과 같되, 결과 집합이 계통적이다. INNER JOIN, LEFT OUTER JOIN
Zip 두 순차열의 요소들을 차례로 짝지으면서 (마치 지퍼를 채우듯이) 각 요소 쌍에 함수를 적용한다. (예외 발생)

Join과 GroupJoin

개요

  • Join과 GroupJoin은 두 입력 순차열을 하나의 출력 순차열로 합친다. Join은 평평한 순차열을 출력하고, GroupJoin은 계통적 순차열을 출력한다.
  • Join과 GroupJoin의 용도는 Select 및 SelectMany의 용도와 비슷하다. Join과 GroupJoin의 장점은 클라이언트 메모리 안에 있는 지역 컬렉션에 대해 효율적으로 실행된다는 점이다.
    • 이는 이 연산자들이 내부 순차열을 먼저 키 있는 조회 객체(keyed lookup)에 적재하기 때문이다. 그래서 모든 요소를 되풀이해서 열거할 필요가 없다.
    • 단점은 내부 결합과 왼쪽 외부 결합만 지원한다는 점이다.
    • 교차 결합이나 비등가 결합을 수행하려면 Select, SelectMany를 사용해야 한다.
    • LINQ to SQL과 EF 질의에서는 Select, SelectMany보다 Join, GroupJoin이 더 나은 점이 사실상 없다.
  • 아래 표는 이 두 결합 전략들 사이의 차이점을 요약한 것이다.
전략 결과 형태 지역 질의 효율성 내부 결합 왼쪽 외부 결합 교차 결합 비등가 결합
Select + SelectMany 평평함 나쁨 지원 지원 지원 지원
Select + Select 중첩됨 나쁨 지원 지원 지원 지원
Join 평평함 좋음 지원
GroupJoin 중첩됨 좋음 지원 지원
GroupJoin + SelectMany 평평함 좋음 지원 지원

 

Join

  • Join 연산자는 내부 결합을 수행해서 평평한 순차열을 출력한다.
    • EF에서는 외래 키 필드가 숨겨지므로 자연스러운 관계들에 대해 결합을 직접 수행할 수 없다.
  • Join이 지역 질의에 좋긴 하지만, 그 작동 방식을 설명하기에는 LINQ to SQL을 예로 드는 것이 더 간단하다. 다음은 연관 속성을 사용하지 않고 모든 고객과 그 구매 정보를 나열하는 질의이다.
IQueryable<string> query =
  from c in dataContext.Customers
  join p in dataContext.Purchases on c.ID equals p.CustomerID
  select c.Name + "의 구매 상품: " + p.Description

// 결과
// Tom의 구매 상품: Bike
// Dick의 구매 상품: Phone
// ...
  • SelectMany에 비한 Join의 장점을 보여주려면 이를 지역 질의로 바꾸어야 한다.
Customer[] customers = dataContext.Customers.ToArray();
Purchases[] purchases = dataContext.Purchases.ToArray();

var slowQuery = 
  from c in customers
  from p in purchases where c.ID == p.CustomerID
  select c.Name + "의 구매 상품: " + p.Descripion;

var fastQuery = 
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  select c.Name + "의 구매 상품: " + p.Descripion;
  • 두 질의는 같은 결과를 내지만, Join을 사용한 질의가 훨씬 빠르다. 이는 해당 Enumerable 구현이 내부 컬렉션을 키 있는 조회 객체에 미리 적재해 두기 때문이다.
  • 다음은 질의 구문의 join 절의 일반적인 형태이다.
join 내부-변수 in 내부-순차열 on 외부-키-선택자 equals 내부-키-선택자
  • LINQ의 결합 연산자들은 외부 순차열과 내부 순차열을 구분한다. 구문상으로,
    • 외부 순차열은 입력 순차열이다. 지금 예에서는 customers.
    • 내부 순차열은 join 절이 새로 도입하는 컬렉션이다. 지금 예에서는 purchases.
  • Join은 내부 결합을 수행한다. 즉, 구매 기록이 없는 고객은 출력에 포함되지 않는다. 내부 결합에서는 질의의 내부 순차열과 외부 순차열을 맞바꾸어도 같은 결과가 나온다.
var query = 
  from p in purchases  // 이제는 p가 외부
  join c in customers on p.CustomerID equals c.ID  // 이제는 c가 내부
  ...
  • 하나의 질의에 여러 개의 join 절을 둘 수 있다. 예컨대 각 구매에 하나 이상의 구매 항목 정보가 존재한다면, 구매 항목들을 다음과 같이 결합할 수 있다.
var query = 
  from c in customers
  join p in purchases on c.ID equals P.CustomerID  // 첫 결합
  join pi in purchaseItems on p.ID equals pi.PurchaseID  // 둘째 결합
  ...
  • 이 경우 purchases는 첫 결합에서는 내부 순차열, 둘째 결합에서는 외부 순차열로 작용한다. 다음과 같은 내포된 foreach 문들로도 같은 결과를 얻을 수 있다. 단 더 비효율적이다.
foreach (Customer c in customers)
  foreach (Purchase p in purchases)
    if (c.ID == p.CustomerID)
      foreach (PurchaseItem pi in purchaseItems)
        if (p.ID == pi.PurchaseID)
          Console.WriteLine(c.Name + "," + p.Price + "," + pi.Detail);
  • SelectMany 스타일의 질의에서와 마찬가지로 질의 구문에서는 이전 결합의 변수들이 여전히 유효 범위에 있다. 또한 join 절들 사이에 where 절이나 let 절들을 끼워 넣는 것도 허용된다.

다중 키 결합

  • 다음 예처럼 익명 형식을 이용하면 여러 개의 키로 결합을 수행할 수 있다.
from x in sequenceX
join y in sequenceY on new { K1 = x.Prop1, K2 = x.Prop2 }
                          equals new { K1 = y.Prop3, K2 = y.Prop4 }
...
  • 이런 질의가 작동하려면 두 익명 형식의 구조가 동일해야 한다. 그러면 컴파일러가 둘을 동일한 내부 형식으로 구현하며, 따라서 결합 키들이 호환된다.

유창한 구문의 결합

  • 다음과 같은 질의 구문 결합 질의를 생각해 보자.
var query = 
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  select new { c.Name, p.Description, p.Price };
  • 이를 유창한 구문으로 바꾸면 다음과 같은 형태가 된다.
customers.Join (  // 외부 순차열
  purchases,  // 내부 순차열
  c => c.ID,  // 외부 키 선택자
  p => p.CustomerID,  // 내부 키 선택자
  (c, p) => new { c.Name, p.Description, p.Price }  // 결과 선택자
);
  • 질의 끝의 결과 선택자 표현식은 출력 순차열의 각 요소를 생성한다. 그런데 투영 전에 또 다른 절이, 예컨대 다음과 같이 orderby 절이 있는 질의를 유창한 구문으로 옮기려면
var query = 
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  orderby p.Price
  select new { c.Name, p.Description, p.Price };
  • 다음처럼 임시 익명 형식을 도입할 필요가 있다. 그래야 그 이후의 결합에서 c와 p가 여전히 유효한 범위 안에 있게 된다.
customers.Join (  // 외부 순차열
  purchases,  // 내부 순차열
  c => c.ID,  // 외부 키 선택자
  p => p.CustomerID,  // 내부 키 선택자
  (c, p) => new { c, p } ) // 결과 선택자
  .OrderBy (x => x.p.Price)
  .Select (x => x.c.Name + "의 구매 상품: " + x.p.Description);
  • 대체로 결합을 수행할 때는 코드가 덜 장황한 질의 구문이 더 낫다.

GroupJoin

  • GroupJoin은 Join과 같은 일을 하되, 평평한 결과가 아니라 외부 요소마다 자식 순차열이 있는 형태의 계통적인 결과를 낸다. 또한 GroupJoin은 왼쪽 외부 결합을 지원한다.
  • GroupJoin의 질의 구문은 Join 구문에 into 절이 붙는 형태이다.
IEnumerable<IEnumerable<Purchase>> query = 
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  into custPurchases
  select custPurchases;  // custPurchases는 하나의 순차열
  • into 절은 join 절 바로 다음에 있을 때만 GroupJoin으로 해석된다. select나 group절 다음의 into는 질의 연속(query continuation)을 뜻한다. into 키워드의 이 두 용법은 그 의미가 아주 다르다. 단, 둘 다 새로운 범위 변수를 도입한다는 공통점이 있다.
  • 이 질의의 결과는 순차열들의 순차열이다. 다음은 그러한 계통적 순차열을 열거하는 예이다.
foreach (IEnumerable<Purchase> purchaseSequence in query)
  foreach (Purchase p in purchaseSequence)
    Console.WriteLine(p.Description);
  • 그런데 이 결과 순차열을 그리 유용하지 않다. purchasesSequence의 구매 레코드들이 어떤 고객에 속한 것인지가 빠져 있기 때문이다. 일반적으로는 다음 예처럼 고객 이름을 함께 묶어서 출력하는 것이 더 유용하다.
IEnumerable<IEnumerable<Purchase>> query = 
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  into custPurchases
  select new { CustName = c.Name, custPurchases };
  • GroupJoin은 기본적으로 왼쪽 외부 결합에 해당하는 일을 수행한다. 내부 결합을 원한다면, 즉 구매 기록이 없는 고객들을 제외하려면 custPurchases에 대해 필터를 적용해야 한다.
var query =
  from c in customers join p in purchases on c.ID equals p.CustomerID
  into custPurchases
  where custPurchases.Any()
  select ...
  • GroupJoin에 해당하는 질의의 into 다음에 있는 절들은 개별 자식 요소들이 아니라 자식 요소들의 부분 순차열에 대해 작동한다. 이 때문에 개별 구매 레코드를 걸러내려면 결합 전에 Where를 호출해야 한다.
var query =
  from c in customers 
  join p in purchases.Where(p2 => p2.Price > 1000)
  on c.ID equals p.CustomerID
  into custPurchases ...

평평한 외부 결합

  • 외부 결합을 수행하되, 평평한 결과 집합을 얻고 싶다면 어떻게 해야 할까? 외부 결합을 지원하는 것은 GroupJoin이고, 평평한 결과 집합을 제공하는 것은 Join이다. 둘을 섞는 방법은 우선 GroupJoin을 실행하고 각 자식 순차열에 대해 DefaultIfEmpty를 호출하고, 최종적으로 그 결과에 대해 SelectMany를 적용하는 것이다.
var query =
  from c in customers 
  join p in purchases on c.ID equals p.CustomerID
  into custPurchases
  from cp in custPurchases.DefaultIfEmpty()
  select new 
  {
    CustName = c.Name,
    Price = cp = null ? (decimal?) null : cp.Price
  }
  • DefaultIfEmpty는 자식 순차열이 비어 있으면 널 요소 하나로 된 순차열을 돌려준다. 두 번째 from 절은 SelectMany로 번역된다. 이 질의에서 SelectMany는 모든 구매 레코드 자식 순차열을 확장하고 평탄화해서 구매 레코드 요소들로 이루어진 하나의 순차열을 출력한다.

조회 객체를 이용한 결합

  • Enumerable의 Join과 GroupJoin 메서드는 두 단계로 작동한다.
    • 첫째로 이 메서드들은 내부 순차열으르 하나의 조회 객체(lookup)에 적재한다.
    • 둘째로 이들은 그 조회 객체를 이용해서 외부 순차열을 질의한다.
  • 조회 객체는 키를 이용해서 개별 순차열에 접근할 수 있는 키-순차열 쌍들의 순차열이다. 이를 하나의 키에 여러 개의 값(요소)을 대응시킬 수 있는 순차열들의 사전이라고 생각해도 좋다 (그래서 중복사전(multidictionary)이라고 부르기도 한다)
    • 조회 객체는 읽기 전용이며 다음과 같은 인터페이스로 정의된다.
public interface ILookup<TKey, TElement> : IEnumerable<IGrouping<TKey, TElement>>, IEnumerable
{
  int Count { get; }
  bool Contains (TKey key);
  IEnumerable<TElement> this [TKey key] { get; }
}
  • 순차열을 출력하는 다른 여러 질의 연산자들처럼, 결합 연산자들은 지연된 실행 의미론을 따른다. 따라서 조회 객체는 출력 순차열을 열거하기 시작해야 비로소 구축된다. 그리고 그 시점에서 조회 객체 전체가 구축된다.
  • 지역 컬렉션을 다룰 때는 결합 연산자를 사용하는 대신 조회 객체를 직접 구축해서 질의하는 접근방식도 가능하다. 이러한 접근방식의 장점은 다음 두 가지 이다.
    • 같은 조회 객체에 대해 여러 가지 질의를 적용할 수 있다. 또한 통상적인 명령식(imperative) 코드를 이용해서 결과를 열거할 수도 있다.
    • 조회 객체를 직접 질의해 보면 Join과 GroupJoin의 작동 방식을 이해하는데 크게 도움이 된다.
  • 질의 객체를 생성하는 한 가지 방법은 확장 메서드 ToLookup을 호출하는 것이다. 다음 예는 CustomerID를 키로 사용해서 모든 구매 레코드를 질의 객체에 적재한다.
ILookup<int?, Purchase> puchLookup = purchases.ToLookup(p => p.CustomerID, p => p);
  • 첫 인수는 키를 선택한다. 둘째 인수는 조회 객체에 ‘값’들로써 적재되는 요소들을 선택한다.
  • 조회 객체에서 값을 조회하는 것은 보통의 사전에서 값을 조회하는 것과 비슷하다. 단, 이 경우 인덱서는 키에 부합하는 하나의 항목이 아니라 그런 항목들의 순차열을 돌려준다. 다음은 ID가 1인 고객의 모든 구매 정보를 열거하는 예이다.
foreach (Purchase p in purchLookup[1])
  Console.WriteLine(p.Description);
  • 일단 조회 객체를 마련했다면, Join/GroupJoin 질의만큼이나 효율적으로 실행되는 SelectMany/Select 질의를 작성할 수 있다. 조회 객체에 대한 SelectMany 질의는 Join 질의와 동등하다.
from c in customers
from p in purchLookup [c.ID]
select new { c.Name, p.Description, p.Price };

// 결과
// Tom Bike 500
// Dick Phone 300
// ...
  • 여기에 DefaultIfEmpty 호출을 추가하면 외부 결합 질의가 된다.
from c in customers
from p in purchLookup [c.ID].DefaultIfEmpty()
select new { 
  c.Name, 
  Descript = p == null ? null : p.Description, 
  Price = p == null ? (decimal?) null : p.Price 
};
  • 다음과 같이 투영 안에서 조회 객체를 읽는 것은 GroupJoin에 해당한다.
from c in customers
select new { 
  CustName = c.Name, 
  CustPurchases = purchLookup[c.ID]
};

Zip 연산자

  • Zip 연산자는 .NET Framework 4.0에 추가되었다. 이 질의 연산자는 두 순차열의 요소들을 한 단계씩 훑으면서 (마치 지퍼를 채우듯이) 각 요소 쌍에 함수를 적용한 결과를 담은 순차열을 돌려준다.
  • 예컨대 다음 예는
int[] numbers = { 3, 5, 7 };
string[] words = { "삼", "오", "칠", "해당 없음" };
IEnumerable<string> zip = numbers.Zip(words, (n, w) => n + "=" + w);
  • 다음과 같은 요소들로 이루어진 순차열을 산출한다.
3=삼
5=오
7=칠
  • 두 입력 순차열의 길이가 다른 경우 여분의 요소들은 무시된다. EF와 L2S는 Zip을 지원하지 않는다.

순서 결정

  • IEnumerable<TSource> -> IOrderedEnumerable<TSource>
메서드 설명 해당 SQL 구문
OrderBy, ThenBy 순차열을 오름차순으로 정렬한다. ORDER BY …
OrderByDescending, ThenByDescending 순차열을 내림차순으로 정렬한다. ORDER BY … DESC
Reverse 입력 순차열과 순서가 반대인 순차열을 돌려준다. (예외 발생)

 

OrderBy, OrderByDescending, ThenBy, ThenByDescending

개요

  • OrderBy는 입력 순차열을 정렬한 버전을 출력한다. keySelector 표현식은 정렬시 두 요소의 순서를 비교하는데 쓰이는 정렬 키(sorting key)를 돌려준다. 다음 질의는 이름들을 길이순으로 정렬한다.
IEnumerable<string> query = names.OrderBy(s => s.Length);
  • 정렬 키가 같은 요소들의 순서는 정의되지 않는다. 그런 순서를 지정하려면 ThenBy 연산자를 추가해야 한다.
IEnumerable<string> query = names.OrderBy(s => s.Length).ThenBy(s => s);
  • ThenBy는 그 앞의 정렬에서 정렬 키가 같은 요소들의 순서만 바꾼다. ThenBy 연산자를 여러 개 이을 수도 있다. 다음은 이름들을 먼저 길이순으로 정렬하고, 그런 다음 둘째 문자를 기준으로 정렬하고 마지막으로 첫 문자를 기준으로 정렬하는 예이다.
IEnumerable<string> query = names.OrderBy(s => s.Length).ThenBy(s => s[1]).ThenBy(s => s[0]);
  • 이를 질의 구문으로 표현하면 다음과 같다.
var query = 
  from s in names
  orderby s.Length, s[1], s[0]
  select s;
  • LINQ는 이들과 같은 일을 수행하되 ‘역순’의 결과를 돌려주는 두 연산자를 제공한다. 바로 OrderByDescending과 ThenByDescending이다.
    • 다음 DB 대상 질의는 구매 레코드들을 가격의 내림차순으로 정렬하되, 가격이 같은 구매 레코드들은 알파벳순으로 정렬한다.
var query = dataContext.Purchases.OrderByDescending(p => p.Price).ThenBy(p => p.Description);
  • 이를 질의 구문으로 표현하면 다음과 같다.
var query = 
  from p in dataContext
  orderby p.Price desceding, p.Description
  select p;

비교자와 콜레이션

  • 지역 질의에서 정렬 기준은 키 선택자 객체 자체의 기본 IComparable 구현이 결정한다. 다른 정렬 기준을 사용하고 싶다면 IComparer 파생 형식의 비교자 객체를 지정하면 된다.
    • 다음은 이름들의 대소문자 구분 없이 정렬하는 예이다.
IEnumerable<string> query = names.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase);
  • 질의 구문에서는 비교자를 지정할 수 없다. 또한 LINQ to SQL과 EF에서는 어떤 구문이든 비교자 지정을 지원하지 않는다. 데이터베이스 질의에서 정렬 기준은 해당 열의 콜레이션(collation) 설정이 결정한다.
    • 영문 대소문자를 비교하는 콜레이션이 설정된 열에 대해 대소문자를 구분하지 않는 정렬을 수행하는 한 가지 방법은 다음처럼 키 선택자에서 ToUpper를 호출하는 것이다.
from p in dataContext.Purchases
orderby p.Description.ToUpper()
select p;

IOrderedEnumerable과 IOrderedQueryable

  • 순서 질의 연산자들은 특정한 IEnumerable<T> 파생 형식들을 돌려준다. Enumerable의 질의 연산자들은 IOrderedEnumerable<TSource>를 돌려주고 Queryable의 질의 연산자들은 IOrderedQueryable<TSource>를 돌려준다. 이 파생 형식들에서는 기존 순서를 아예 대체하는 것이 아니라 일부만 다듬는 ThenBy 연산자를 뒤에 붙일 수 있다.
  • 이 파생 형식들이 정의하는 추가적인 멤버들은 공용으로 노출되지 않으므로 겉으로 보기에 이 형식들은 보통의 순차열과 다를 바가 없다. 그러나 질의를 점진적으로 구축해 보면 보통의 순차열과 다른 점이 드러난다.
IOrderedEnumerable<string> query1 = names.OrderBy(s => s.Length);
IOrderedEnumerable<string> query2 = query1.ThenBy(s => s);
  • 만일 query1을 IEnumerable<string> 형식으로 선언했다면 둘째 줄이 컴파일 되지 않는다. 확장 메서드 ThenBy는 IOrderedEnumerable<string> 형식의 인수를 받기 때문이다. 이런 차이점을 신경 쓰고 싶지 않다면 질의 변수의 형식을 암묵적으로 지정하면 된다.
    • 그러나 암묵적 형식 지정 때문에 문제가 생길 수도 있다. 다음 예는 컴파일 되지 않는다.
var query = names.OrderBy(s => s.Length);
query = query.Where(n => n.Length > 3);  // 컴파일 시점 오류
  • 컴파일러는 OrderBy의 출력 순차열 형식에 기초해서 query의 형식이 IOrderedEnumerable<string>이라고 추론한다. 그러나 그 다음 줄의 Where는 보통의 IEnumerable<string>을 돌려주며, 그것을 다시 query에 배정할 수는 없다.
    • 해결책은 명시적으로 형식을 지정하거나, 아니면 OrderBy 다음에 AsEnumerable을 호출하는 것이다.
    • 해석식 질의에서는 AsEnumerable 대신 AsQueryable을 호출해야 한다.

그룹화

  • IEnumerable<TSource> -> IEnumerable<IGrouping<TKey, TElement>>
메서드 설명 해당 SQL 구문
GroupBy 입력 순차열의 요소들을 여러 그룹으로 분류한 부분 순차열들을 출력한다. GROUP BY

 

개요

  • GroupBy는 평평한 입력 순차열을 그룹들의 순차열로 조직화한다.
    • 예컨대 다음은 모든 파일을 확장자별로 조직화하는 질의이다.
string[] files = Directory.GetFiles("c:\\temp");
IEnumerable<IGroupping<string, string>> query = files.GroupBy(file => Path.GetExtension(file));
  • Enumerable.GroupBy는 입력 요소들을 키별로 임시적인 목록들의 사전에 집어 넣는다. 결과적으로 키가 같은 요소들은 모두 사전의 같은 목록에 추가된다. 그런 다음 그 사전의 목록들을 그룹화 객체(grouping)들의 순차열로 변환해서 출력한다.
    • 그룹화 객체는 Key 속성이 있는, 다음과 같은 형식의 순차열이다.
public interface IGrouping<TKey, TElement> : IEnumerable<TElement>, IEnumerable
{
  TKey Key { get; }  // Key는 순차열의 모든 원소에 공통이다.
}
  • 기본적으로 GroupBy는 입력 순차열 요소들을 변환 없이 각 그룹화에 집어넣는다. 변환을 원한다면 요소 선택자(elementSelector)를 지정하면 된다.
    • 다음은 각 입력 요소를 영문 대문자로 변환하는 예이다.
files.GroupBy(file => Path.GetExtension(file), file => file.ToUpper());
  • 요소 선택자는 키 선택자(keySelector 인수)와는 독립적이다. 지금 예에서 각 그룹화의 Key는 여전히 원래의 대소문자 구성을 유지한다.
  • 각 부분 컬렉션(그룹화)의 요소들이 키들의 알파벳순으로 정렬되어 있지는 않음을 주목하기 바란다. GroupBy는 그룹화만 수행할 뿐, 정렬은 수행하지 않는다.
    • 사실 GroupBy는 원래의 순서를 유지한다. 정렬을 원한다면 OrderBy 연산자를 추가해야 한다.
files.GroupBy(file => Path.GetExtension(file), file => file.ToUpper()).OrderBy(grouping => grouping.Key);
  • GroupBy에 해당하는 질의 구문은 다음과 같다. GroupBy 절의 구문과 거의 직접적으로 대응된다.
group 요소-표현식 by 키-표현식
  • 다음은 앞의 예를 질의 구문으로 표현한 것이다.
from file in files
group file.ToUpper() by Path.GetExtension(file);
  • select 처럼 group도 질의를 끝내는 역할을 한다. 단, 질의 연속(query continuation)절을 추가하는 것도 가능하다.
from file in files
group file.ToUpper() by Path.GetExtension (file) into grouping
orderby grouping.Key
select grouping;
  • group by 질의에서는 이러한 질의 연속이 유용한 경우가 많다. 다음 질의는 파일이 다섯 개 미만인 그룹들을 출력에서 제외한다.
from file in files
group file.ToUpper() by Path.GetExtension (file) into grouping
where grouping.Count() >= 5
select grouping;
  • group by 다음의 where는 SQL의 HAVING에 해당한다. where는 개별 요소가 아니라 각각의 부분 순차열 자체에 적용된다.
  • 그룹화된 부분 순차열들 자체는 필요하지 않고, 그룹들에 대한 어떤 집계 정보만 얻는 것으로 충분한 때도 있다. 다음으 그러한 예이다.
string[] votes = { "Bush", "Gore", "Gore", "Bush", "Bush" };

IEnumerable<string> query = 
  from vote in votes
  group vote by vote into g
  orderby g.Count() descending
  select g.Key;

string winner = query.First();  // Bush

LINQ to SQL과 EF의 GroupBy

  • 그룹화 질의 연산자들은 데이터베이스 질의에서도 지역 질의와 동일한 방식으로 작동한다. 그러나 엔티티 클래스에 연관 관계 속성들을 설정해 두었다면, 그룹화가 필요한 상황이 적어진다.
    • 예컨대 구매 기록이 2건 이상인 고객들을 선택한다고 할 때, group 절을 추가할 필요가 없다.
from c in dataContext.Customers
where c.Purchases.Count >= 2
select c.Name + "의 구매 수: " + c.Purchases.Count + "건";
  • 그룹화가 필요한 예로 연도별 총 판매액을 구하는 질의이다.
from p in dataContext.Purchases
group p.Price by p.Date.Year into salesByYear
select new { Year = salesByYear.Key, TotalValue = salesByYear.Sum() };
  • LINQ의 그룹화 기능은 SQL의 GROUP BY 보다 강력하다. 예컨대 LINQ에서는 다음처럼 집계(aggregation) 연산자 없이도 모든 상세 정보 행들을 가져올 수 있다.
from p in dataContext.Purchases
group p.Price by p.Date.Year
  • 이 질의는 EF에서도 잘 작동한다. 단 L2S에서는 불필요한 통신 왕복들이 발생한다. 한 가지 손쉬운 우회책은 그룹화 바로 전에 AsEnumerable()을 호출하는 것이다. 그러면 그룹화가 클라이언트에서 일어난다.
    • 필요한 모든 필터링을 그룹화 전에 수행해서, 작업에 꼭 필요한 자료만 서버에서 가져왔다면 이렇게 해도 효율성이 떨어지지는 않는다.
  • 전통적인 SQL과의 또 다른 차이점은 그룹화나 정렬에 쓰이는 변수나 표현식을 투영할 필요가 없다는 점이다.

다중 키 그룹화

  • 여러 키로 이루어진 복합 키로 그룹화를 수행할 수 있다. 다음처럼 익명 형식을 활용하면 된다.
from n in names
group n by new { FirstLetter = n[0], Length = n.Length };

커스텀 상등 비교자

  • 지역 질의에서는 GroupBy에 커스텀 상등 비교자를 지정해서 키 비교 방식을 임의로 변경할 수 있다. 그러나 실제로 상등 비교자를 지정해야 할 일은 별로 없다. 그냥 키 선택자 표현식을 적절히 바꾸는 것으로 충분하기 떄문이다.
    • 예컨대 다음은 대소문자를 구분하지 않고 그룹화를 수행한다.
group name by name.ToUpper()

집합 연산자

  • IEnumerable<TSource>, IEnumerable<TSource> -> IEnumerable<TSource>
메서드 설명 해당 SQL 구문
Concat 두 순차열을 연결한 순차열, 즉 두 순차열의 모든 요소를 담은 순차열을 돌려준다. UNION ALL
Union 두 순차열을 연결하되 중복 요소를 제외한 순차열을 돌려준다. UNION
Intersect 두 순차열 모두에 있는 요소들을 담은 순차열을 돌려준다. WHERE … IN (…)
Except 첫 순차열에만 있고 둘째 순차열에는 없는 요소들을 담은 순차열을 돌려준다. EXCEPT 또는 WHERE … NOT IN (…)

 

Concat과 Union

  • Concat은 첫 순차열에 둘째 순차열을 연결(concatenation)한 순차열, 즉 첫 순차열의 모든 요소 다음에 둘째 순차열의 모든 요소가 있는 순차열을 돌려준다.
  • Union은 그러한 순차열에서 중복된 요소들을 제거한 결과를 돌려준다.
int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };

IEnumerable<int> 
  concat = seq1.Concat(seq2),  // { 1, 2, 3, 3, 4, 5 }
  union = seq1.Union(seq2);  // { 1, 2, 3, 4, 5 }
  • 이 예에서처럼 형식 인수를 명시적으로 지정하는 것은, 두 순차열의 형식이 다르지만 그 요소들은 공통의 기반 형식에서 파생된 형식일 때 유용하다.
    • 예컨대MethodInfo 클래스와 PropertyInfo 클래스에는 MemberInfo라는 공통의 기반 클래스가 있는데, Concat을 호출할 때 그 기반 클래스를 명시적으로 지정함녀 메서드들과 속성들을 하나의 순차열에 담을 수 있다.
MethodInfo[] methods = typeof(string).GetMethods();
PropertyInfo[] props = typeof(string).GetProperties();
IEnumerable<MemberInfo> both = methods.Concat<MemberInfo>(props);
  • 다음 예는 연결 이전에 메서드들을 선별하는 예이다.
MethodInfo[] methods = typeof(string).GetMethods().Where(m => !m.IsSpecialName);
PropertyInfo[] props = typeof(string).GetProperties();
IEnumerable<MemberInfo> both = methods.Concat<MemberInfo>(props);
  • 이 예는 인터페이스 형식 매개변수의 가변성(variance; 공변성과 반변성)에 의존한다. methods는 IEumerable<MethodInfo> 형식인데, 이를 IEnumerable<MemberInfo>로 변환할 수 있어야 질의가 성공한다. 이는 가변성이 생각보다 더 많은 것을 가능하게 한다는 점을 잘 보여주는 예이다.

Intersect와 Except

  • Intersect는 두 순차열 모두에 있는 요소들을 출력한다. Except는 첫 순차열에만 있고 둘째 순차열에는 없는 요소들을 출력한다.
int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };

IEnumerable<int> 
  commonality = seq1.Intersect(seq2),  // { 3 }
  difference1 = seq1.Except(seq2),  // { 1, 2 }
  difference2 = seq2.Except(seq1);  // { 4, 5 }
  • 내부적으로 Enumerable.Except는 첫 컬렉션의 모든 요소를 한 사전에 넣고, 둘째 순차열의 모든 요소를 그 사전에서 제거한다. 이에 해당하는 SQL 구문은 NOT EXISTS 부분 질의 또는 NOT IN 부분 질의이다.
SELECT number FROM numbers1Table
WHERE number NOT IN (SELECT number FROM numbers2Table)

변환 메서드들

  • LINQ는 기본적으로 순차열, 다시 말해 IEnumerable<T> 형식의 컬렉션을 다룬다. 변환 메서드들은 한 형식의 컬렉션을 다른 형식으로 변환한다.
메서드 설명
OfType IEnuemrable을 IEnumerable<T>로 변환한다. 호환되지 않는 형식의 요소들은 폐기한다.
Cast IEnuemrable을 IEnumerable<T>로 변환한다. 호환되지 않는 형식의 요소가 있으면 예외를 던진다.
ToArray IEnuemrable<T>를 T[]로 변환한다.
ToList IEnuemrable<T>를 List<T>로 변환한다.
ToDictionary IEnuemrable<T>를 Dictionary<TKey, TValue>로 변환한다.
ToLookup IEnuemrable<T>를 ILookup<TKey, TElement>로 변환한다.
AsEnumerable IEnuemrable<T>를 하향 캐스팅한다.
AsQueryable IQueryable<T>로 캐스팅 또는 변환한다.

 

OfType과 Cast

  • OfType과 Cast는 비제네릭 IEnumerable 컬렉션을 받고 제네릭 IEnumerable<T> 순차열을 출력한다.
    • (변환 코드 생략)
  • Cast와 OfType의 차이는 입력 순차열에서 호환되지 않는 형식의 요소를 만났을 때 드러난다. 그런 경우 Cast는 예외를 던지지만 OfType은 그런 요소를 그냥 무시한다.
    • (예시 코드 생략)
  • 요소 형식 호환성 규칙은 C#의 is 연산자에 쓰이는 규칙들과 정확히 동일하다. 오직 참조 변환과 언박싱 변환만 고려된다. 실제로 OfType의 내부 구현을 보면 이 점을 확인할 수 있다. Cast의 구현은 이에서 형식 호환성 판정만 뺀 것이다.
    • (내부 코드 생략)
  • 이러한 구현 방식 때문에 수치 형식 변환이나 커스텀 변환에는 Cast를 사용할 수 없다. (그런 용도로는 반드시 Select 연산을 실행해야 한다) 다른 말로 하면 Cast가 C#의 캐스팅 연산자만큼 유연하지는 않다.
    • (이하 생략)
  • OfType과 Cast는 제네릭 입력 순차열의 요소들을 하향 캐스팅할 때도 유용하다. 예컨대 입력 순차열의 형식이 IEnumerable<Fruit>이면 OfType<Apple>은 사과들만 돌려준다.
  • 질의 구문은 Cast를 지원한다. 그냥 범위 변수 앞에 형식을 지정해 주면 된다.

ToArray, ToList, ToDictionary, ToLookup

  • ToArray와 ToList는 주어진 순차열을 배열 또는 제네릭 목록으로 변환한다. 이 두 연산자가 호출되면 입력 순차열이 즉시 열거된다.
  • ToDictionary와 ToLookup은 다음과 같은 인수들을 받는다.
인수 형식
입력 순차열 IEnumerable<TSource>
키 선택자 TSource => TKey
요소 선택자(선택적) TSource => TElement
비교자(선택적) IEqualityComparer<TKey>

 

  • ToDictionary도 입력 순차열의 즉시 열거를 강제한다. 이 연산자는 결과를 제네릭 Dictionary에 담아서 돌려준다. 키 선택자에는 반드시 입력 순차열의 각 요소에 대해 고유한 값으로 평가되는 표현식을 지정해야 한다. 그렇지 않으면 예외가 발생한다.
  • 반면 조회 객체를 출력하는 ToLookup에서는 다수의 요소가 같은 키에 할당되어도 예외가 발생하지 않는다.

AsEnumerable과 AsQueryable

  • AsEnumerable은 순차열을 IEnumerable<T>로 상향 캐스팅한다. 그러면 컴파일러는 이후의 질의 연산자를 Queryable이 아니라 Enumerable에 있는 메서드에 묶게 된다.
  • AsQueryable은 만일 입력 순차열이 IQueryable<T>를 구현한 형식이면 그 인터페이스로 하향 캐스팅하고 그렇지 않으면 지역 질의를 감싸는 IQueryable<T> 래퍼의 인스턴스를 생성한다.

요소 연산자

  • IEnumerable<TSource> -> TSource
메서드 설명 해당 SQL 구문
First, FirstOrDefault 순차열의 첫 요소(술어가 지정되었으면 그 술어를 만족하는 첫 요소)를 돌려준다. SELECT TOP 1 … ORDER BY …
Last, LastOrDefault 순차열의 마지막 요소(술어가 지정되었으면 그 술어를 만족하는 마지막 요소)를 돌려준다. SELECT TOP 1 … ORDER BY DESC
Single, SingleOrDefault First/FirstOrDefault와 같되 부합하는 요소가 둘 이상이면 예외를 던진다.
ElementAt, ElementAtOrDefault 지정된 위치에 있는 요소들 돌려준다. (예외 발생)
DefaultIfEmpty 입력 순차열에 요소가 하나도 없으면, 값이 default(TSource)인 요소 하나만 있는 순차열을 돌려준다. OUTER JOIN

 

  • OrDefault로 끝나는 메서드들은 입력 순차열이 비었거나 주어진 술어와 부합하는 요소가 없을 때 예외를 던지는 대신 default(TSource) 하나로 된 순차열을 출력한다.
  • default(TSource)는 요소가 참조 형식일 때는 null이고 bool 형식일 때는 false, 수치 형식일 때는 0이다.
  • (이하 각 메서드 설명 생략)

집계 메서드

  • IEnuemrable<TSource> -> scalar
메서드 설명 해당 SQL 구문
Count, LongCount 입력 순차열의 요소 개수(술어가 지정되었으면 그 술어를 만족하는 요소들의 개수)를 돌려준다. COUNT (…)
Min, Max 입력 순차열에서 가장 작은 또는 가장 큰 요소를 돌려준다. MIN (…), MAX (…)
Sum, Average 순차열 요소들의 수치 합 또는 평균을 계산한다. SUM (…), AVG (…)
Aggregate 주어진 커스텀 집계 연산을 수행한다. (예외 발생)

 

  • (이하 Count, Min/Max, Sum/Average 설명 생략)

Aggregate

  • Aggregate는 주어진 커스텀 누계(accumulation) 알고리즘을 순차열에 적용한다. 흔치 않은 집계 연산을 구현하고자 할 때 유용하다.
  • LINQ to SQL과 EF는 Aggregate를 지원하지 않는다. 사실 Aggregate의 용도는 다소 특화되어 있다. 다음은 Aggregate를 이용해서 Sum과 같은 결과를 얻는 예이다.
int[] numbers = { 2, 3, 4 };
int sum = numbers.Aggregate(0, (total, n) => total + n);  // 9
  • Aggregate의 첫 인수는 누계의 초기 값으로 쓰인느 종잣값(seed)이다. 둘째 인수는 입력 순차열의 각 요소로 누계 결과를 갱신하는 표현식이다. 선택적인 셋째 인수를 지정해서 누계 결과를 다른 형태의 최종 결과 값으로 투영할 수도 있다.
  • Aggregate로 풀 수 있는 대부분의 문제는 좀 더 친숙한 구문의 foreach 루프를 이용해서 쉽게 풀 수 있다. Aggregate의 장점은, 크거나 복잡한 집계 문제의 경우 PLINQ를 이용해서 누계를 자동으로 병렬화 할 수 있다는 것이다.

종갓값 없는 집계

  • Aggregate 호출 시 종잣값을 생략할 수도 있다. 그런 경우 입력 순차열의 첫 요소가 암묵적인 종잣값이 되며, 누계는 둘째 요소부터 시작한다. 다음은 앞의 질의를 종잣값 없이 수행하는 예이다.
int[] numbers = { 1, 2, 3 };
int sum = numbers.Aggregate((total, n) => total + n);  // 6
  • 이 질의도 이전과 동일한 결과를 내지만, 실제로 수행하는 계산은 이전과 다르다. 이전 질의는 0+1+2+3을 계산하지만, 이 질의는 1+2+3을 계산한다. 덧셈 대신 곱셈을 적용하면 이 차이가 중요해진다.
  • 종잣값 없는 집계에는 특별한 중복적재 버전들을 사용하지 않고도 누계를 병렬화 할 수 있다는 장점이 있다. 그러나 몇 가지 함정들도 있다.

종잣값 없는 집계의 함정

  • 종잣값 없는 집계 메서드들은 가환적(commutative)이고 결합적(associative)인 대리자, 즉 교환법칙과 결합법칙을 만족하는 대리자와 함께 사용하도록 고안된 것이다. 그렇지 않은 대리자를 사용하면 결과는 직관적이지 않거나 비결정론적이다(PLINQ로 질의를 병렬화한 경우).
  • 예컨대 다음 함수를 생각해 보자.
(total, n) => total + n * n
  • 이 함수는 가환적이지도, 결합적이지도 않다. (1 + 2 * 2 != 2 + 1 * 1) 그럼 이를 이용해서 2, 3, 4의 제곱들의 합을 구해 보자.
int[] numbers = { 2, 3, 4 };
int sum = numbers.Aggregate((total, n) => total + n * n);  // 27
  • 이 질의는 2*2 + 3*3 + 4*4가 아니라 2 + 3*3 + 4*4를 계산한다.
  • 이를 바로 잡는 방법은 여러가지가 있는데, 우선 첫 요소로 0을 포함하는 방법이 있다. 그러나 이는 우아하지 않을 뿐만 아니라 병렬화하면 여전히 잘못된 결과를 낸다.   LINQ는 함수가 결합적이라는 가정을 활용해서 여러 개의 요소를 종잣값들로 선택하기 때문이다.
  • 예컨대 누산 함수가 다음과 같다고 하자.
f(total, n) => total + n * n
  • 그러면 객체 대상 LINQ는 이를 다음과 같이 계산하지만
f(f(f(0, 2), 3), 4)
  • PLINQ는 다음과 같이 계산할 가능성이 있다.
f(f(0,2), f(3,4))
  • 제대로된 해법은 두 가지이다. 첫째는 종잣값 없는 집계를 종잣값 있는 집계로 바꾸는 것이다. 지금 예에서는 0을 종잣값으로 사용하면 된다.
    • 이 방법의 유일한 단점은 PLINQ의 경우 질의가 순차적으로 실행되지 않게 하려면 특별한 중복적재 버전을 사용해야 한다는 것이다.
  • 둘째 해법은 누계 함수가 교환법칙과 결합법칙을 만족하도록 질의의 구조를 개선하는 것이다.
    • 지금 예에서는 다음과 같이 하면 된다.
int sum = numbers.Select(n => n * n).Aggregate((total, n) => total + n);

한정사

  • IEnumerable<TSource> -> bool
메서드 설명 해당 SQL 구문
Contains 만일 주어진 요소가 입력 순차열에 있으면 true를 돌려준다. WHERE … IN (…)
Any 만일 주어진 술어를 만족하는 요소가 하나라도 있으면 true를 돌려준다. WHERE … IN (…)
All 만일 주어진 술어를 모든 요소가 만족하면 true를 돌려준다. WHERE (…)
SequenceEqual 만일 둘째 순차열의 요소들이 입력 순차열과 요소들과 동일하면 true를 돌려준다.

 

  • (이하 메서드 설명 생략)

생성 메서드

  • void -> IEnumerable<TResult>
메서드 설명 해당 SQL 구문
Empty 빈 순차열을 생성한다.
Repeat 같은 요소가 되풀이된 순차열을 생성한다.
Range 정수들의 순차열을 생성한다.

 

Empty

  • Empty는 빈 순차열을 생성한다. 형식 인수만 지정하면 된다.
foreach(string s in Enumerable.Empty<string>())
  Console.WriteLine(s);  // 출력 없음
  • ??와 함께 사용한 Empty는 DefaultIfEmpty의 반대에 해당하는 일을 수행한다. 예컨대 길이가 가변적인 정수 배열들의 배열에 담긴 정수들을 하나의 평평한 목록으로 출력한다고 하자. 다음과 같이 SelectMany를 이용한 질의는 내부 배열 중에 널이 하나라도 있으면 실패한다.
int[][] numbers =
{
  new int[] { 1, 2, 3 },
  new int[] { 4, 5, 6 },
  null  // 이 널 배열 때문에 다음 질의가 실패한다.
}

IEnumerable<int> flat = numbers.SelectMany(innerArray => innerArray);
  • Empty와 ??의 조합을 사용하면 문제가 해결된다.
IEnumerable<int> flat = numbers.SelectMany(innerArray => innerArray ?? Enumerable.Empty<int>());

Range와 Repeat

  • Range는 시작 색인과 개수를 받는다. (둘 다 정수)
foreach(int i in Enumerable.Range(5, 3))
  Console.WriteLine(i + " ");  // 5 6 7
  • Repeat는 되풀이할 요소 하나와 되풀이 횟수를 받는다.
foreach(bool x in Enumerable.Repeat(true, 3))
  Console.WriteLine(x + " ");  // True True True
[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.