상세 컨텐츠

본문 제목

LINQ: .NET 통합 언어 쿼리

C#/LINQ

by 탑~! 2008. 4. 4. 16:50

본문

LINQ: .NET 통합 언어 쿼리

Don Box, Anders Hejlsberg

January 2007

적용 대상 :
Visual Studio 2008
.Net Framework 3.5

개요 :이 자료에서는 .NET Framework 에 추가된 범용 쿼리 기능에 대해 설명합니다. 범용 쿼리 기능은 관계형 데이터, XML 데이터 뿐만이 아닌 모든 정보 소스에 적용됩니다.이 기능을 .NET LINQ (Language Integrated Query : 통합 언어 쿼리)라고 부릅니다.

목차

.NET LINQ
표준 쿼리 연산자 개요
LINQ 프로젝트를 지원 하는 언어 기능
표준 쿼리 연산자
쿼리 구문
LINQ to SQL: SQL 통합
LINQ to XML: XML 통합
요약

.NET LINQ

개체 지향 (OO) 프로그래밍 기술 진화는 20 년의 세월을 거쳐 이제 안정화 단계에 이르렀습니다. 프로그래머도 클래스, 개체, 메서드와 같은 기능을 잘 다룰 수 있게 되었습니다.이러한 기술 현상이나 차세대 기술에 관심을 가져보면, OO 기술을 사용하여 네이티브에 정의되지 않은 정보 접근이나 통합의 복잡함을 경감하는 것이 앞으로의 큰 과제입니다. OO 기술에 대응하지 않는 정보 중에서 가장 일반적인 정보 소스가 관계형 데이터베이스와 XML 두가지입니다.

.NET Framework 에서는 프로그램 언어나 런타임에 관계형 고유의 기능이나 XML 고유의 기능을 추가하는 것이 아니라, LINQ 프로젝트라는 범용성 높은 접근 방식을 채용했습니다..NET Framework에 범용 쿼리 기능이 추가되어 관계형 데이터나 XML 데이터 뿐만 아니라, 모든 정보 소스에 쿼리 기능이 적용됩니다. 이 기능을 .NET LINQ (Language Integrated Query : 통합 언어 쿼리)라고 부릅니다.

"통합 언어 쿼리 (LINQ : Language Integrated Query)" 라는 용어가 보여주듯이, 개발자가 주로 사용하는 프로그램 언어 (Visual C#, Visual Basic 등)에 쿼리 기능이 통합됩니다. LINQ를 사용하여 지금까지는 프로그래밍 코드로 밖에 이용할 수 없었던, 리치 메타데이터, 컴파일시 구문 체크, 정적인 형식 지정, IntelliSense 등의 장점을 "쿼리 식" 에서 이용할 수 있게 됩니다.또, LINQ 라고 하는 선언형의 단일 범용 쿼리 기능에 의해, 외부 소스로부터의 정보 뿐만이 아니라, 메모리내의 모든 정보에 대해 쿼리 기능을 이용할 수도 있습니다.

.NET LINQ 에서는 연속된 범용 "표준 쿼리 연산자" 가 정의됩니다.이 표준 쿼리 연산자에 의해,.NET 기반의 프로그램 언어내에서 선언형의 직접적 수법을 사용하고, 통과 (일괄) 연산, 필터 연산 및 프로젝션 연산을 표현할 수 있습니다.표준 쿼리 연산자에서는 IEnumerable<T> 기반의 모든 정보 소스에 쿼리를 적용할 수 있습니다.  LINQ사용으로 타사에서 제공되는 표준 쿼리 연산자세트가 증가합니다.이러한 표준 쿼리 연산자 안에는 대상 도메인이나 대상 기술에 대응하는 새로운 도메인 고유의 연산자도 포함됩니다.한층 더 중요한 점은 타사가 표준 쿼리 연산자를 독자적인 구현에 자유롭게 옮겨놓을 수 있는 것입니다.타사 독자적인 구현에서는 리모트 평가 쿼리의 번역, 최적화 등의 서비스를 추가할 수 있습니다. 이러한 구현에서는 "LINQ 패턴" 표기법으로 준거하며, 표준 쿼리 연산자와 같은 통합 언어와 도구 지원을 이용할 수 있습니다.

이러한 쿼리 아키텍처의 확장성은 LINQ 프로젝트 자체에서도 사용되고 XML데이터와 SQL 데이터의 양쪽 모두로 기능하는 구현을 제공합니다. XML 에 대한 쿼리 연산자 (LINQ to XML)에서는 효율적으로 사용하기 쉬운, 인메모리 XML 기능을 사용하고, 호스트 프로그램 언어에 XPath/XQuery 기능을 제공합니다. 관계형 데이터에 대한 쿼리 연산자 (LINQ to SQL)는 공용 언어 런타임 (CLR)의 형식 시스템에 통합된 SQL 기반의 스키마 정의를 기본으로 구축됩니다.이러한 통합에 의해, 관계형 모델의 표현력이나, 기본으로 되는 스토어에서 직접 행해지는 쿼리 평가의 성능을 유지하면서, 관계형 데이터에 엄밀한 형식 지정이 필요합니다.

표준 쿼리 연산자 개요

LINQ 의 작용을 조사하기 위해, 배열의 내용의 처리에 표준 쿼리 연산자를 사용하는 간단한 C# 3.0 프로그램에서 살펴보겠습니다.

using System;
using System.Linq;
using System.Collections.Generic;

class app {
  static void Main() {
    string[] names = { "Burke", "Connor", "Frank", 
                       "Everett", "Albert", "George", 
                       "Harris", "David" };

    IEnumerable<string> query = from s in names 
                               where s.Length == 5
                               orderby s
                               select s.ToUpper();

    foreach (string item in query)
      Console.WriteLine(item);
  }
}

이 프로그램을 컴파일 해 실행하면, 다음과 같은 출력을 얻을 수 있습니다.

BURKE
DAVID
FRANK
LINQ 의 구조를 이해하려면, 프로그램의 최초의 구문을 자세하게 조사해야 합니다.

IEnumerable<string> query = from s in names 
                           where s.Length == 5
                           orderby s
                           select s.ToUpper();

여기에서는 로컬 변수 query 가 "쿼리 식" 에서 초기화됩니다.쿼리 식은 표준 쿼리 연산자 또는 도메인 고유의 연산자의 몇 개의 쿼리 연산자를 1 개 이상 적용하고, 1 개 이상의 정보 소스에 대해 연산을 실시합니다. 이 식에서는 세가지 표준 쿼리 연산자, Where, OrderBy Select를 사용합니다.

Visual Basic 9.0에도  LINQ 가 지원 됩니다.상기의 구문을 Visual Basic 9.0 으로 기술하면 다음과 같습니다.

Dim query As IEnumerable(Of String) = From s in names _
                                     Where s.Length = 5 _
                   Order By s _
                   Select s.ToUpper()

여기서 보여진 C# 와 Visual Basic 구문은 쿼리 식을 사용합니다. foreach 구문과 같이, 쿼리 식은 수작업 코드 기술 대신에 편리한 선언형의 간단한 표현입니다. 상기 구문은 C# 에서 보여진 다음의 명확한 구문과 같은 의미입니다.

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

이 형식의 쿼리를 "메서드 기반" 쿼리라 부릅니다.Where 연산자, OrderBy 연산자, Select 연산자의 인수를 "Lambda식" 이라 합니다.이것은 대리자(Delegate)에 비슷한 코드 단편(fragment)입니다.표준 쿼리 연산자는 이 Lambda식에 의해 개별 메서드로 정의된 dot notation를 사용하여 연결할 수 있습니다. 동시에, 이러한 메서드가 확장성 있는 쿼리 언어의 기초를 형성합니다.

LINQ 프로젝트를 지원하는 언어 기능

LINQ는 C# 3.0과 Visual Basic 9.0에 새롭게 도입된 몇 가지의 범용 언어 기능에 전면적으로 기초를 두어 구축됩니다.이러한 기능은 단독으로도 효과가 있지만, 이러한 기능을 조합하여 쿼리나 쿼리 가능한 API  정의에 확장성의 높은 방법이 가져옵니다.여기에서는 이러한 언어 기능에 대해 자세하게 조사해 이러한 기능이 선언형의 직접적인 스타일의 쿼리에 어떻게 공헌하는지 설명합니다.

Lambda식과 식 트리(expression tree)

사용자는 많은 쿼리 연산자를 사용하여, 필터 처리, 프로젝션, 키 추출등을 실행하는 함수를 제공할 수 있습니다. 쿼리 기능은 Lambda식의 개념을 기본으로 구축되어 개발자는 이 기능을 사용하여, 이후 평가에 인수로서 건네줄 수 있는 함수를 간단하게 기술할 수 있습니다. Lambda식은 CLR 의 대리자(Delegate)와 비슷하여 대리자(Delegate)형에 의해 정의된 메서드 서명에 따라야 합니다. 상기의 구문을 확장하여, 동등한 기능을  Func 대리자(Delegate)형을 사용하는 보다 명확한 형식으로 변환합니다.

Func<string, bool>   filter  = s => s.Length == 5;
Func<string, string> extract = s => s;
Func<string, string> project = s => s.ToUpper();

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

Lambda식은 C# 2.0의 익명 메서드를 자연스러운 형식으로로 진화시킨 것입니다.예를 들어, 익명 메서드를 사용하면 상기의 예를 다음과 같이 고쳐 쓸 수 있습니다.

Func<string, bool>   filter  = 대리자(Delegate) (string s) {
                                   return s.Length == 5; 
                               };

Func<string, string> extract = 대리자(Delegate) (string s) { 
                                   return s; 
                               };

Func<string, string> project = 대리자(Delegate) (string s) {
                                   return s.ToUpper(); 
                               };

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

일반적으로, 개발자는 이름 메서드, 익명 메서드 또는 Lambda식을 쿼리 연산자와 자유롭게 병용 할 수 있습니다. Lambda식에는 코드의 기술에 가장 직접적으로 복잡한 구문을 이용할 수 있는 장점이 있습니다. 더 중요한 것은 Lambda식은 코드 또는 데이터도 컴파일 할 수 있습니다.그 때문에,  최적화(optimizer), 변환기( translator), 평가기(evaluator) 의해 Lambda식을 실행시에 처리할 수 있습니다.

System.Linq.Expressions 네임 스페이스에서는 잘 알려진 범용형의 Expression<T> 가 정의되어 있습니다.이 형식은 특정의 Lambda식에 기존의 IL 기반의 메서드 본체가 아니고, "식 트리(expression tree)" 가 요구됩니다. 식 트리(expression tree)는 Lambda식의 효율적인 인메모리 데이타 표현으로,  식에 의존하지 않는 명확한 구조를 작성합니다.

컴파일러가 실행 가능한 IL 를 생성하는지, 식 트리(expression tree)를 생성자는 Lambda식의 사용법에 의해 정해집니다.  대리자(Delegate)인 변수, 필드 또는 매개 변수에 Lambda식이 대입되는 경우, 컴파일러는 익명 메서드와 동등의 IL 를 생성합니다. 대리자(Delegate)형T 에 대해,  Expression<T> 인 변수, 필드 또는 매개 변수에 Lambda식이 대입되는 경우, 컴파일러가 식 트리(expression tree)를 생성합니다.

예를 들어, 다음의 두개의 변수 선언을 생각해 봅시다.

Func<int, bool>             f = n => n < 5;
Expression<Func<int, bool>> e = n => n < 5;

변수f 는 대리자(Delegate) 참조로 직접 실행할 수 있습니다.

bool isSmall = f(2); // isSmall 는 True 가 됩니다.

변수e는 식 트리(expression tree) 참조로 직접 실행할 수 없습니다.

bool isSmall = e(2); // compile error, 식은 데이터입니다.

대리자(Delegate)는 실질적으로는 내용이 명확하게 되어 있지 않은 코드이지만, 식 트리(expression tree)는 프로그램내에서 다른 임의의 데이터 구조와 완전히 같이 접근 할 수 있습니다.

Expression<Func<int, bool>> filter = n => n < 5;

BinaryExpression    body  = (BinaryExpression)filter.Body;
ParameterExpression left  = (ParameterExpression)body.Left;
ConstantExpression  right = (ConstantExpression)body.Right;

Console.WriteLine("{0} {1} {2}", 
                  left.Name, body.NodeType, right.Value);

상기의 예는 실행시에 식 트리(expression tree)를 분해하여, 다음의 문자열을 출력합니다.

n LessThan 5

이와 같이 실행시에 식 트리(expression tree)를 데이터로서 취급하는 기능은 플랫폼의 일부인 기반 쿼리의 추상화를 이용하는 타사제품 라이브러리의 에코시스템을 육성하는 경우에 중요합니다. LINQ to SQL 데이터 접근의 구현은 이 기능을 사용하여, 식 트리(expression tree)를 스토어내에서의 평가에 적절한 T-SQL 구문으로 변환합니다.

확장 메서드

Lambda식은 쿼리 아키텍처의 중요한 요소의 1 개입니다.이제 하나의 중요한 요소가 "확장 메서드" 입니다.확장 메서드란, 동적 언어의 커뮤니티에서는 일반적으로 된 "duck typing" 의 유연성과 정적으로 형식 지정되는 언어의 성능과 컴파일시 검증을 조합한 것입니다.타사는 확장 메서드를 사용하고, 새로운 메서드 형식의 퍼블릭 컨트랙트(contract)를 보강하는 것과 동시에 개개의 형식의 작성자는 이러한 메서드의 독자적인 특수한 구현을 제공할 수 있습니다.

확장 메서드는 정적 클래스내에서 정적 메서드로서 정의됩니다.다만, CLR 메타데이터내에[System.Runtime.CompilerServices.Extension] 속성을 붙일 수 있습니다.언어에서는 확장 메서드용의 직접 구문을 준비하는 것을 추천합니다.C# 에서는 확장 메서드가 this 수식자로 나타납니다.이 수식자는 확장 메서드의 최초의 매개 변수에 적용해야 합니다.그럼, 가장 단순한 쿼리 연산자 Where 의 정의를 봅시다.

namespace System.Linq {
  using System;
  using System.Collections.Generic;

  public static class Enumerable {
    public static IEnumerable<T> Where<T>(
             this IEnumerable<T> source,
             Func<T, bool> predicate) {

      foreach (T item in source)
        if (predicate(item))
          yield return item;
    }
  }
}

확장 메서드의 최초의 매개 변수는 어느 형식을 확장하는지를 나타냅니다.상기의 예의 Where 확장 메서드는 형식 IEnumerable<T> 를 확장합니다.Where 은 정적 메서드임으로 다른 모든 정적 메서드와 완전히와 같이 직접 호출할 수 있습니다.

IEnumerable<string> query = Enumerable.Where(names, 
                                          s => s.Length < 6);

그러나, 확장 메서드가 독특한 것은 다음과 같이 인스턴스 구문을 사용해도 호출할 수 있는 점입니다.

IEnumerable<string> query = names.Where(s => s.Length < 6);

확장 메서드는 어느 확장 메서드가 스코프내가 될지를 기본으로, 컴파일시 해결합니다. C# 의 using 구문 또는 Visual Basic의 Import 구문을 사용해 네임 스페이스가 가져오기 되면, 그 네임 스페이스로부터의 정적 클래스에 의해서 정의되는 모든 확장 메서드가 스코프내에 들어갑니다.

표준 쿼리 연산자는 형식 System.Linq.Enumerable의 확장 메서드로서 정의됩니다.표준 쿼리 연산자를 조사해 보면, 몇 안 되는 예외를 제외한 모든 연산자가 IEnumerable<T> 인터페이스에 관련해 정의된 것을 알 수 있습니다.즉, C# 로 다음의 using 구문을 추가하는 것만으로, 모든 IEnumerable<T> 호환의 정보 소스가 표준 쿼리 연산자를 취득합니다.

using System.Linq; // 쿼리 연산자를 이용 가능하게 합니다.

특정의 형식의 표준 쿼리 연산자를 옮겨놓고 싶은 경우, 그 형식의 같은 이름의 메서드를 호환성이 있는 서명으로 독자적으로 정의하는지, 그 형식을 확장하는 확장 메서드를 같은 이름으로 새롭게 정의합니다.표준 쿼리 연산자가 동시에 이용 가능하게 되는 것을 피하고 싶은 경우는 단순하게 System.Linq 가 스코프내에 들어가지 않도록, IEnumerable<T> 에 대해 독자적인 확장 메서드를 기술합니다.

확장 메서드는 해결의 우선 순위가 가장 낮고, 대상의 형식과 그 기본형에 어울리는 조합이 없는 경우에게만 사용됩니다.그 때문에, 사용자 정의형을 사용하고, 표준 연산자보다 우선도가 높은 독자적인 쿼리 연산자를 제공할 수 있습니다.예를 들어, 다음의 사용자 지정 콜렉션을 생각해 봅시다.

public class MySequence : IEnumerable<int> {
  public IEnumerator<int> GetEnumerator() {
    for (int i = 1; i <= 10; i++) 
      yield return i; 
  }

  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator(); 
  }

  public IEnumerable<int> Where(Func<int, bool> filter) {
    for (int i = 1; i <= 10; i++) 
      if (filter(i)) 
        yield return i;
  }
}

이 클래스 정의를 전제로 하면, 다음의 프로그램은 MySequence.Where 구현을 확장 메서드로서가 아니고, 확장 메서드보다 우선도의 높은 인스턴스 메서드로서 사용합니다.

MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
    Console.WriteLine(item);

OfType 연산자는 표준 연산자이지만, IEnumerable<T> 기반의 정보 소스를 확장하지 않는 얼마 안되는 연산자의 하나입니다. OfType 쿼리 연산자를 봅시다.

public static IEnumerable<T> OfType<T>(this IEnumerable source) {
  foreach (object item in source) 
    if (item is T) 
      yield return (T)item;
}

OfType  IEnumerable<T> 기반의 소스 뿐만이 아니고,.NET Framework 버전 1.0 에 존재하고 있던, 매개 변수 화 되지 않은IEnumerable 인터페이스에 대해 기술된 소스도 받습니다. OfType 연산자를 사용하여, 다음과 같이 기존의 .NET 콜렉션에 표준 쿼리 연산자를 적용할 수 있습니다.

// "classic" 는 쿼리 연산자와 직접 병용 할 수 없습니다.
IEnumerable classic = new OlderCollectionType();

// "modern" 는 쿼리 연산자와 직접 병용 할 수 있습니다.
IEnumerable<object> modern = classic.OfType<object>();

이 예에서는 변수modern은 classic 과 같은 값의 순서를 생성합니다.다만, 그 형식은 최신의 IEnumerable<T> 코드와 호환성이 있어, 표준 쿼리 연산자를 포함합니다.

OfType 연산자에서는 형식을 기본으로 소스로부터의 값을 필터 처리하기 위해, 정보 소스가 새로워진 경우에서도 도움이 됩니다. OfType 은 새로운 순서를 작성할 경우에, 형식 인수와 호환성이 없는 멤버를 단순하게 원의 순서로부터 제외합니다.이종 배열로부터 문자열을 추출하는 다음의 간단한 프로그램을 생각합니다.

object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();

foreach 구문으로 justStrings 변수를 열거하면, 두개의 문자열 "Hello" 와 "World" 의 순서로 취득됩니다.

쿼리 지연 평가

예리한 독자라면, 표준Where 연산자가 C# 2.0 으로 도입된 yield 구조를 사용해 구현되어 있다는 것에 알고 있을 것입니다. 이 구현 수법은 값의 순서를 돌려주는 표준 연산자 모두에게 공통되어 사용됩니다. yield를 사용하는 장점은 foreach 구문을 사용하거나 기본이 된 GetEnumerator 메서드와 MoveNext 메서드를 수동으로 사용하여, 실제로 반복 처리를 할 때까지 쿼리가 평가되지 않는 점에 있습니다.이러한 지연 평가에서는 쿼리를 여러 차례 평가할 수 있는 IEnumerable<T> 기반의 값으로 해서 보관 유지할 수 있기 위해, 매회 다른 결과가 생성될 가능성이 있습니다.

많은 응용 프로그램에서는 이 동작은 전혀 문제가 되지 않습니다.다만, 쿼리 평가의 결과를 캐시 하는 응용 프로그램에서는 쿼리의 즉시 평가를 실행할 수 있는 두개의 연산자 ToListToArray가 있습니다.어느 쪽이나 쿼리 평가의 결과를 돌려주지만, 전자는 List<T>를 돌려주어, 후자는 배열을 반환합니다.

쿼리 지연 평가의 동작을 조사하기 위해, 배열에 대해 간단한 쿼리를 실행하는 다음의 프로그램을 생각해 봅시다.

// 문자열을 몇개인가 보관 유지하는 변수를 선언합니다.
string[] names = { "Allen", "Arthur", "Bennett" };

// 쿼리를 나타내는 변수를 선언합니다.
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');

// 쿼리를 평가합니다.
foreach (string item in ayes) 
  Console.WriteLine(item);

// 원래의 정보 소스를 변경합니다.
names[0] = "Bob";

// 쿼리를 재차 평가합니다.이 시점에서는 "Allen" 가 포함되어 있지 않습니다.
foreach (string item in ayes) 
    Console.WriteLine(item);

변수ayes의 반복 처리를 할 때마다, 쿼리가 평가됩니다.결과를 캐시 한 카피가 필요한 일을 나타내는 경우는 다음과 같이 쿼리의 마지막에 ToList 연산자 또는 ToArray 연산자를 부가할 뿐입니다.

// 문자열을 몇개인가 보관 유지하는 변수를 선언합니다.
string[] names = { "Allen", "Arthur", "Bennett" };

// 쿼리의 즉시 평가의 결과를 나타낸다
// 변수를 선언합니다.
string[] ayes = names.Where(s => s[0] == 'A').ToArray();

// 캐시 한 쿼리 결과를 반복 처리 합니다.
foreach (string item in ayes) 
    Console.WriteLine(item);

// 원래의 소스를 변경해도 ayes 에는 영향을 주지 않습니다.
names[0] = "Bob";

// 결과를 재차 반복 처리 합니다.여전히 "Allen" 가 포함됩니다.
foreach (string item in ayes)
    Console.WriteLine(item);

ToArray ToList 는 어느쪽이나 쿼리의 즉시 평가를 실행합니다.같이 단일치를 돌려주는 표준 쿼리 연산자 (First,ElementAt,Sum,Average,All,Any 등)에서도 쿼리 즉시 평가를 합니다.

IQueryable<T> 인터페이스

일반적으로 LINQ to SQL 등 식 트리(expression tree)를 사용해 쿼리 기능을 구현 하는 데이터 소스에서도, 같은 지연 실행 모델이 요구됩니다.이러한 데이터 소스에는 IQueryable<T> 인터페이스를 구현 하는 것으로 장점이 얻을 수 있는 경우가 있습니다.이 인터페이스에서는 LINQ 패턴에 필요한 모든 쿼리 연산자가 식 트리(expression tree)를 사용해 구현되기 때문입니다.각 IQueryable<T> 에서,"쿼리의 실행에 필요한 코드" 가 식 트리(expression tree)의 형식에서 표현됩니다.모든 지연 쿼리 연산자는 식 트리(expression tree)에 그 쿼리 연산자를 호출하는 표기를 추가한, 새로운 IQueryable<T> 를 반환합니다.그 결과, 쿼리가 평가되는 시점에 이르면, IQueryable<T> 가 열거되기 위해, 데이터 소스는 전쿼리를 1 개의 배치로 표현하는 식 트리(expression tree)를 처리할 수 있습니다.예를 들어, 쿼리 연산자를 많이 호출하는 것에 의해서 얻을 수 있는 복잡한 LINQ to SQL 쿼리에서도, 단 1 개의 SQL 쿼리로서 데이터베이스에 송신되게 됩니다.

IQueryable<T> 인터페이스를 구현 하는 것으로써, 이러한 지연 기능의 재이용을 구현 하는 데이터 소스의 장점은 분명합니다.한편, 쿼리를 기술하는 클라이언트에 있어서는 리모트 정보 소스에 대해 공통의 형식을 소지한다고 하는 큰 장점이 있습니다.이것에 의해, 다른 데이터 소스에 대해 사용할 수 있는 Polymorphic Systems인 쿼리를 기술 가능할 뿐만 아니라, 도메인을 걸쳐 송신되는 쿼리를 기술할 수 있을 가능성이 초래됩니다.

복합치의 초기화

값의 순서로부터 단순하게 멤버를 필터 선택해 꺼내는 쿼리에 필요한 처리는 모두, Lambda식과 확장 메서드로 제공됩니다.대부분의 쿼리 식에서는 이러한 멤버에 대한 프로젝션도 실행하여, 오리지널 시퀀스 값이나  다른 멤버의 순서에 효과적으로 변환합니다.LINQ 에서는 이러한 변환을 지원 하기 위해,"개체 초기화자" 라고 하는 새로운 구조에 의해, 구조화 된 형식의 새로운 인스턴스를 작성합니다.

public class Person {
  string name;
  int age;
  bool canCode;

  public string Name {
    get { return name; } set { name = value; }
  }

  public int Age {
    get { return age; } set { age = value; }
  }

  public bool CanCode {
    get { return canCode; } set { canCode = value; }
  }
}

개체 초기화자를 사용하면, 형식의 퍼블릭 필드와 속성을 기본으로, 쉽게 값을 구성할 수 있습니다.예를 들어, Person 형식의 새로운 값을 작성하는 경우, 다음의 구문을 기술할 수 있습니다.

Person value = new Person {
    Name = "Chris Smith", Age = 31, CanCode = false
};

이 구문의 의미는 다음의 연속된 구문과 같습니다.

Person value = new Person();
value.Name = "Chris Smith";
value.Age = 31;
value.CanCode = false;

개체 초기화자에서는 (Lambda식내 또는 시리즈 기록되지 않은 일이나 식 트리(expression tree)내 등) 식만이 허가되는 문맥으로 새롭게 구조화 된 값을 작성할 수 있기 위해, LINQ 에서는 중요한 기능입니다.예를 들어, 입력 순서값마다 새롭운 Person 값을 작성하는 다음의 쿼리 식을 생각해 주세요.

IEnumerable<Person> query = names.Select(s => new Person {
    Name = s, Age = 21, CanCode = s.Length == 5
});

개체 초기화 구문은 구조화 된 값의 배열을 초기화하는 경우에도 편리합니다.예를 들어, 개별의 개체 초기화자를 사용해 초기화되는 다음의 배열 변수를 생각해 주세요.

static Person[] people = {
  new Person { Name="Allen Frances", Age=11, CanCode=false },
  new Person { Name="Burke Madison", Age=50, CanCode=true },
  new Person { Name="Connor Morgan", Age=59, CanCode=false },
  new Person { Name="David Charles", Age=33, CanCode=true },
  new Person { Name="Everett Frank", Age=16, CanCode=true },
};

구조화 된 값과 형식

LINQ 프로젝트에서는 상태와 동작의 양쪽 모두를 겸비한 완전한 개체가 아니고, 주로 구조화 된 값에 정적인 "형상" 을 제공하는 것을 목적으로 해 어떠한  존재하는 데이터 중심의 프로그래밍 스타일을 지원 합니다.이러한 논리적 결론에 이르는 전제를 생각하면, 많은 경우 개발자의 관심사항의 모든 것은 값구조에 있어, 그 형상에 이름 첨부의  필요한 경우가 거의 없습니다. 이것이 "익명형" 의 도입과 연결됩니다. 익명형에서는 새로운 구조를 "인 라인" 으로 정의할 수 있어 정의시에 초기화할 수 있습니다.

C# 의 익명형의 구문은 형식의 이름을 생략 하는 점을 제외하면, 개체 초기화 구문과 같습니다.예를 들어, 다음의 두개의 구문을 생각해 보겠습니다.

object v1 = new Person {
    Name = "Brian Smith", Age = 31, CanCode = false
};

object v2 = new { // 형명이 생략 되고 있는 점에 주의해 주세요.
    Name = "Brian Smith", Age = 31, CanCode = false
};

변수v1 v2는 어느쪽이나 3 개의 퍼블릭 속성 Name, Age, CanCode를 갖춘 CLR 형의 인메모리 개체를 말합니다.두개의 변수의 차이는v2 가 "익명형" 의 인스턴스로 불리는 점입니다.CLR 의 용어로는 익명형과 다른 형식에는 차이가 없습니다.익명형이 특수한 것은 프로그램 언어내에서 의미가 있는 이름이 붙이지 않을 수 없는 것입니다.익명형의 인스턴스를 작성하는 유일한 방법은 상기의 구문을 사용하는 것입니다.

정적인 형식 지정의 장점을 살리면서, 변수를 익명형의 인스턴스로 할 수 있도록, C# 에서는 "암시에 형식 지정되는 로컬 변수" 를 도입했습니다.로컬 변수 선언으로 형명 대신에,var 키워드를 사용할 수 있습니다.예를 들어, 다음의 정규 C# 3.0 프로그램을 생각합니다.

var s = "Bob";
var n = 32;
var b = true;

var 키워드는 변수의 초기화에 사용하는 식의 정적인 형식으로부터 변수의 형식을 추측하도록 컴파일러에 지시합니다.이 예에서는s,n b 형식은 각각string,int bool 입니다.이 프로그램은 다음의 프로그램과 같습니다.

string s = "Bob";
int    n = 32;
bool   b = true;

var 키워드는 의미가 있는 이름을 가지는 형식의 변수에 편리하지만, 익명형의 인스턴스를 나타내는 변수에도 불가결합니다.

var value = new { 
  Name = " Brian Smith", Age = 31, CanCode = false
};

상기의 예에서는 변수 value 는 익명형으로, 다음 C# 코드와 같은 정의가 됩니다.

internal class ??? {
  string _Name;
  int    _Age;
  bool   _CanCode;

  public string Name { 
    get { return _Name; } set { _Name = value; }
  }

  public int Age{ 
    get { return _Age; } set { _Age = value; }
  }

  public bool CanCode { 
    get { return _CanCode; } set { _CanCode = value; }
  }

  public bool Equals(object obj) { ... }

  public bool GetHashCode() { ... }
}

어셈블리가 다른 경우, 익명형은 공유할 수 없습니다.다만, 각 어셈블리내의 속성의 이름과 형식 조의 특정 순서에 최대 1 개의 익명형이 존재하는 것이 컴파일러에 의해 확인됩니다.

익명형은 기존의 구조화 된 값의 멤버를 1 개 이상 선택하기 위해, 프로젝션내에서 사용되는 것이 많기 때문에, 익명형의 초기화시에, 다른 값으로부터의 필드나 속성을 간단하게 참조할 수 있게 되어 있습니다.이러한 참조에 의해, 새로운 익명형에는 참조처의 속성이나 필드로부터 이름, 형식 및 값이 모두 카피된 1 개의 속성이 포함되게 됩니다.

예를 들어, 다른 값으로부터의 속성을 조합해 구조화 된 값을 새롭게 작성하는 이하의 예를 생각해 보겠습니다.

var bob = new Person { Name = "Bob", Age = 51, CanCode = true };
var jane = new { Age = 29, FirstName = "Jane" };

var couple = new {
    Husband = new { bob.Name, bob.Age },
    Wife = new { Name = jane.FirstName, jane.Age }
};

int    ha = couple.Husband.Age; // ha == 51
string wn = couple.Wife.Name;   // wn == "Jane"

상기의 필드나 속성에의 참조는 단지, 이하의 명확한 형식의 기술에 대한 간이 구문입니다.

var couple = new {
    Husband = new { Name = bob.Name, Age = bob.Age },
    Wife = new { Name = jane.FirstName, Age = jane.Age }
};

어느 쪽의 경우도, couple 변수는 bob jane 에서 Name 속성과 Age 속성의 독자적인 카피를 취득합니다.

익명형은 쿼리의 select 구로 가장 빈번히 사용됩니다.예를 들어, 다음의 쿼리를 생각해 봅시다.

var query = people.Select(p => new { 
               p.Name, BadCoder = p.Age == 11
           });

foreach (var item in query) 
  Console.WriteLine("{0} is a {1} coder", 
                     item.Name,
                     item.BadCoder ? "bad" : "good");

이 예에서는Person 형식에 대해 새롭게 프로젝션을 작성합니다.정적인 형식의 장점을 살려, 코드 처리에 필요한 형상에 정확하게 일치시킬 수 있습니다.

표준 쿼리 연산자의 상세

많은 연산자에는 상기로 설명한 기본 쿼리 기능에 추가하여, 순서의 조작이나 쿼리의 구성에 도움이 되는 수법이 있어 사용자는 표준 쿼리 연산자의 간이 프레임워크 안의 결과를 제어할 수 있습니다.

늘어놓아 바꾸어와 그룹화

일반적으로, 쿼리를 평가한 결과는 기본으로 되는 정보 소스에 본래 갖춰지고 있는 순서로 작성된 연속된 값이 됩니다.이와 같이 작성된 값의 순서를 개발자가 명확하게 제어할 수 있도록 하려면 , 순서를 제어하기 위한 표준 쿼리 연산자를 정의합니다.이러한 연산자 중에서도 가장 기본적인 것이 OrderBy 연산자입니다.

OrderBy 연산자와 OrderByDescending 연산자는 임의의 정보 소스에 적용할 수 있고 결과를 정렬하여 사용하는 값을 작성하는 키 추출 함수는 사용자를 지정할 수 있습니다. OrderBy 연산자와 OrderByDescending 연산자는 키에 부분적인 순서를 설정하기 위해 사용할 수 있는 생략 가능한 비교 함수도 받습니다. 다음은 기본적인 예입니다.

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

// 단일이 늘어놓아 대체
var s1 = names.OrderBy(s => s); 
var s2 = names.OrderByDescending(s => s);

// 길이에 의한 늘어놓아 대체
var s3 = names.OrderBy(s => s.Length); 
var s4 = names.OrderByDescending(s => s.Length);

최초의 두개의 쿼리 식은 문자열 비교를 기본으로 정보 소스의 멤버를 늘어놓아 바꾼 새로운 순서를 작성합니다.다음의 두개의 쿼리 식은 각 문자열의 길이를 기본으로 정보 소스의 멤버를 늘어놓아 바꾼 새로운 순서를 작성합니다.

OrderBy OrderByDescending 은 어느쪽이든 늘어놓고 바꾸어 조건을 복수 지정할 수 있도록, 범용의 IEnumerable<T> 가 아니고, OrderedSequence<T> 를 반환합니다. OrderedSequence<T> 만으로 정의되고 있는 연산자는 ThenBy ThenByDescending 의 두가지 하위를 정렬 조건을 적용합니다. ThenBy/ThenByDescending 자체도 OrderedSequence<T> 를 돌려주기 위해, 임의의 수의ThenBy/ThenByDescending 연산자를 적용할 수 있습니다.

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);

이 예의s1 이 참조하는 쿼리의 평가에서는 다음의 값의 순서가 작성됩니다.

"Burke", "David", "Frank", 
"Albert", "Connor", "George", "Harris", 
"Everett"

표준 쿼리 연산자에는 OrderBy 패밀리의 연산자 이외에, Reverse 연산자도 있습니다.Reverse 는 단지 같은 값의 순서를 열거해, 그것을 역순으로 합니다. Reverse는 OrderBy 과는 달리, 순서의 결정시에 실제의 값 자체는 고려하지 않고, 단지, 기본으로 되는 정보 소스에 의해서 생성된 값의 순서에 의존합니다.

OrderBy 연산자는 값의 순서에 정렬 순서를 설정합니다.표준 쿼리 연산자에는 키 추출 함수에 근거해 값의 순서를 파티션 분할한다GroupBy 연산자도 있습니다.GroupBy 연산자는 검색 한 개개의 키 값마다 IGrouping 값의 순서를 반환합니다. IGrouping은 컨텐츠의 추출에 사용한 키를 추가로 포함한 IEnumerable 입니다.

public interface IGrouping<K, T> : IEnumerable<T> {
  public K Key { get; }
}

GroupBy 연산자의 가장 단순한 적용 예는 다음과 같이 됩니다.

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// 길이로 그룹화
var groups = names.GroupBy(s => s.Length);

foreach (IGrouping<int, string> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (string value in group)
        Console.WriteLine("  {0}", value);
}

이 프로그램을 실행하면, 다음과 같은 출력 결과를 얻을 수 있습니다.

Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank
Strings of length 7
  Everett

GroupBy 에서 Select 와 같은 형식에서, 그룹 멤버의 설정에 사용하는 프로젝션 함수를 제공할 수 있습니다.

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// 길이로 그룹화
var groups = names.GroupBy(s => s.Length, s => s[0]);
foreach (IGrouping<int, char> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (char value in group)
        Console.WriteLine("  {0}", value);
}

이와 같이 변경하는 것으로, 다음과 같은 출력 결과를 얻을 수 있습니다.

Strings of length 6
  A
  C
  G
  H
Strings of length 5
  B
  D
  F
Strings of length 7
  E
   이 예로부터, 프로젝션을 한 형식과 정보 소스의  같은 필요가 없는 것을 알 수 있습니다.이 경우는 정수로부터 문자열의 순서내의 문자에의 그룹화를 실시했습니다.

집계 연산자

값의 순서를 1 개의 값에 집계하기 위한 표준 쿼리 연산자가 몇 개인가 정의됩니다.가장 일반적인 집계 연산자는 Aggregate 에서 다음과 같이 정의됩니다.

public static U Aggregate<T, U>(this IEnumerable<T> source, 
                                U seed, Func<U, T, U> func) {
  U result = seed;

  foreach (T element in source) 
      result = func(result, element);

  return result;
}

Aggregate 연산자는 값의 순서로의 계산의 실행을 쉽게 합니다. Aggregate는 기본으로 되는 순서의 멤버마다 1 회 Lambda식을 호출하는 것에 의해서 기능합니다. Aggregate 에서 Lambda식을 호출할 때마다, 순서로부터의 멤버와 지금까지의 집계치가 양쪽 모두 건네받습니다 (초기치는Aggregate 에의 seed 매개 변수에 근거합니다).Lambda식의 결과가 직전의 집계값에 옮겨지기 위해, Aggregate 에서는 Lambda식 최종 결과가 돌아갑니다.

예를 들어, 다음의 프로그램은 Aggregate를  사용하고, 문자열의 배열내의 총문자수를 누적 계산합니다.

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int count = names.Aggregate(0, (c, s) => c + s.Length);
// count == 46

표준 쿼리 연산자에는 범용의 Aggregate 연산자 이외에도, 일반적인 집계 연산을 쉽게 하는 범용Count 연산자와 4 개의 수치 집계 연산자 (Min,Max,Sum Average)도 있습니다.수치 집계 함수는 수치형 (int,double,decimal 등)의 값의 순서로 기능합니다.또, 순서의 멤버로부터 수치형에의 프로젝션을 실시하는 함수를 준비하면, 임의의 값의 순서로 기능시킬 수 있습니다.

다음의 프로그램에서는 상기의 양쪽 모두의 형식에서 Sum 연산자를 사용하는 예를 나타냅니다.

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int total1 = numbers.Sum();            // total1 == 55
int total2 = names.Sum(s => s.Length); // total2 == 46
   2 번째의Sum 구문은 전의 Aggregate를  사용하는 예와 같게 됩니다.

Select 와 SelectMany 비교

Select 연산자에는 원의 순서내의 값 마다 1 개의 값을 작성하는 변환 함수가 필요합니다.변환 함수가 그 자체가 순서인 값을 돌려주는 경우, 그 서브 순서 전체를 수동으로 처리하는 것은 사용자측의 역할입니다.예를 들어, 기존의 String.Split 메서드를 사용하고, 문자열을 토큰에 분할하는 다음의 프로그램을 생각해 봅시다.

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.Select(s => s.Split(' '));

foreach (string[] line in tokens)
    foreach (string token in line)
        Console.Write("{0}.", token);

이 프로그램을 실행하면, 다음의 텍스트가 출력됩니다.

Albert.was.here.Burke.slept.late.Connor.is.happy.

다만, 사용자측에서 중간의 string[]을 준비하지 않고, 쿼리로부터 토큰을 연결한 순서를 돌려주는 것이 이상적입니다.이것을 실현하려면, Select 연산자 대신에 SelectMany 연산자를 사용합니다. SelectMany 연산자는 Select 연산자와 같게 기능합니다.그 후 SelectMany 연산자에 의해서 배포 되는 순서를 변환 함수로부터 돌려주는 것이 구할 수 있는 점이 다릅니다.상기의 프로그램을 SelectMany를 사용해 고쳐 써 보겠습니다.

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.SelectMany(s => s.Split(' '));

foreach (string token in tokens)
    Console.Write("{0}.", token);

SelectMany 를 사용하면, 통상의 평가의 일환으로서 각 중간 순서가 배포 됩니다.

SelectMany 는 두개의 정보 소스를 조합하는 경우에 이상적입니다.

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.SelectMany(n => 
                     people.Where(p => n.Equals(p.Name))
);

SelectMany 에게 건네지는 Lambda식에서는 다른 소스에 상자가 된 쿼리가 적용됩니다만, 상자가 된 쿼리는 외부 소스로부터 건네받은 n 매개 변수의 스코프내에 있습니다.그 때문에, people.Where 은 최종 출력용으로 SelectMany 에 의해서 배포 된 결과의 순서를 지정하고, 각n 에 대해 1 회 불려 갑니다.결과는 names 배열내에 출현하는 이름을 가지는 모든 사람의 순서가 됩니다.

결합 연산자

개체 지향의 프로그램에서는 서로 관련하는 개체는 통상, 안내가 용이한 개체 참조를 사용해 링크됩니다.일반적으로, 외부 정보 소스의 경우는 같은 일은 들어맞지 않습니다.외부 정보 소스가 많은 데이터 엔트리는 참조처의 엔티티를 일의에 식별할 수 있는 ID 등을 사용하고, 상징적에 서로를 "가리키는" 이외로 선택사항은 없습니다."결합" 이란, 어느A 순서의 요소와 다른 순서로부터의 "대응하는" 요소를 조합하여  1 로  생각합니다.

상기의 SelectMany 의 예로 실제로 가고 있는 것을 정확하게 말하면, 사람의 이름을 이름을 모은 문자열 배열과 조합합니다.그러나, SelectMany 가 채용하는 접근 방식은 이 목적으로는 별로 효과적이 아닙니다.이 접근 방식에서는people 의 요소를 모두 루프 하고, 요소 마다 names 요소와 조합합니다.이 시나리오의 모든 정보 (두개의 정보 소스와 조합에 사용하는 "키")를 다음과 같이 Join 연산자라고 하는 1 개의 메서드 호출에 정리하면, 작업 효율이 큰폭으로 향상합니다.

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.Join(people, n => n, p => p.Name, (n,p) => p);

간단한 코드이지만, 각 부분이 어떻게 구성되어 있을까를 봅시다. Join 메서드는 "외부" 데이터 소스 names에 불려 가고 있습니다. 최초의 인수는 "안쪽" 의 데이터 소스 people 입니다. 2 번째와 3 번째의 인수는 외부과 안쪽의 소스의 요소로부터 각각 키를 추출하는 Lambda식입니다.이러한 키는 Join 메서드로 요소의 조합에 사용됩니다.여기에서는 names 자체를 people 의 Name 속성과 조합합니다.마지막 Lambda식에서, 결과가 되는 요소의 순서가 작성됩니다.이 Lambda식은 조합하는 요소n p의 각 조를 지정해 불려 가 결과가 형성됩니다.이 경우는 n 을 파기하여, p를 돌려주는 것을 선택했습니다. 최종 결과는 names 목록에 포함된 Name을 가지는 people Person 요소의 목록이 됩니다.

Join 과 같은 연산자 중에 가장 우수한 것이 GroupJoin 연산자입니다. GroupJoin은 결과를 형성하는 Lambda식의 사용 방법이Join 과는 다릅니다.Lambda식은 외부의 요소와 안쪽의 요소의 각 조 개별적으로 불려 가는 것이 아니라, 외부의 요소와 조합하는 안쪽의 전요소의 순서를 지정하고, 외부의 요소 마다 1 회만 불려 갑니다.구체적으로는 다음과 같습니다.

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.GroupJoin(people, n => n, p => p.Name,                   
                 (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

이 호출에서는 지정한 이름으로부터 시작되어, 그 이름을 가지는 사람의 수로 페어로 한 names 의 순서가 작성됩니다.그 때문에 GroupJoin 연산자에 의해, 외부의 1 개의 요소에 대한 "조합의 전세트" 를 결과의 기초로 할 수 있습니다.

쿼리 구문

C# 의 기존의 foreach 구문에서는.NET Framework의 IEnumerable/IEnumerator 메서드상에서 반복 처리를 행하기 위한 선언형 구문이 제공됩니다.특히 foreach 구문을 사용할 필요는 없지만, 이 구문은 매우 편리하고, 넓게 사용되는 언어 메커니즘인 것이 실증됩니다.

"쿼리 구문" 은 이러한 전례에 근거해,Where,Join,GroupJoin,Select,SelectMany,GroupBy,OrderBy,ThenBy,OrderByDescending,ThenByDescending,Cast 등의 가장 일반적인 쿼리 연산자의 쿼리 식을 선언형의 구문을 사용해 간소화합니다.

우선, 다음의 간단한 쿼리로부터 보고 갑시다.

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

쿼리 식을 사용하고, 완전히 같을 구문을 다음과 같이 고쳐 쓸 수 있습니다.

IEnumerable<string> query = from s in names 
                            where s.Length == 5
                            orderby s
                            select s.ToUpper();

쿼리 식은 C# 의 foreach 구문 같이, 보다 컴팩트하게 되어, 읽어내기 쉬워지고 있지만, 전혀 사용하지 않아도 괜찮습니다.쿼리 식에서 고쳐 쓸 수 있는 모든 식은 dot notation를 사용하는 구문에 대응합니다 (약간, 장황하게는 됩니다).

그럼, 쿼리 식의 기본적인 구조를 보기로 합시다.C# 로의 각 쿼리식의 구문은 f rom 구로 시작되어,select 절 또는 group 절의 어느쪽이든으로 끝납니다.최초의from 절에 이어from, let, where,  join orderby를 0 개 이상 지정할 수 있습니다.각 from 절은 순서를 대상으로 하는 범위 변수를 도입하는 생성기로, 각let 절은 식의 결과에 이름을 주어 각 where 절은 결과로부터 항목을 제외하는 필터입니다.각 join 절은 전의 절의 결과와 새로운 데이터 소스를 서로 관련합니다. orderby 절은 결과 순서를 지정합니다.

query-expression ::= from-clause query-body

query-body ::= 

      query-body-clause* final-query-clause query-continuation?

query-body-clause ::=
  (from-clause 
      | join-clause 
      | let-clause 
      | where-clause 
      | orderby-clause)

from-clause ::= from itemName in srcExpr

join-clause ::= join itemName in srcExpr on keyExpr equals keyExpr 
       (into itemName)?

let-clause ::= let itemName = selExpr

where-clause ::= where predExpr

orderby-clause ::= orderby (keyExpr (ascending | descending)?)*

final-query-clause ::=
  (select-clause | groupby-clause)

select-clause ::= select selExpr

groupby-clause ::= group selExpr by keyExpr

query-continuation ::= into itemName query-body

예를 들어, 다음의 두개의 쿼리 식을 생각해 봅시다.

var query1 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             select new { 
                 p.Name, Senior = p.Age > 30, p.CanCode
             };

var query2 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             group new { 
                p.Name, Senior = p.Age > 30, p.CanCode
             } by p.CanCode;

컴파일러는 이러한 쿼리 식을 이하의 명확한 dot notation를 사용해 기술되었는지와 같이 취급합니다.

var query1 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .Select(p => new { 
                       p.Name, 
                       Senior = p.Age > 30, 
                       p.CanCode
                   });

var query2 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .GroupBy(p => p.CanCode, 
                            p => new {
                                   p.Name, 
                                   Senior = p.Age > 30, 
                                   p.CanCode
});

쿼리 식은 메서드의 호출을 구체적인 이름에 기계적으로 변환합니다.쿼리 연산자의 어느 "구현" 이 선택될지는 쿼리 대상의 변수의 형식과 스코프내의 확장 메서드의 양쪽 모두에 의존합니다.

지금까지 나타내 보여 온 쿼리 식에서는 생성기를  하나만 사용합니다. 생성기를 복수 사용할 때는 후에 계속 되는 생성기는 각각 그 전의 생성기 문맥으로 평가됩니다.예를 들어, 지금까지의 쿼리에 다듬은 후 다음의 쿼리를 생각해 보겠습니다.

var query = from s1 in names 
            where s1.Length == 5
            from s2 in names 
            where s1 == s2
            select s1 + " " + s2;

이 쿼리를 다음의 입력 배열에 대해 실행합니다.

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

결과는 다음과 같이 됩니다.

Burke Burke
Frank Frank
David David

상기의 쿼리 식은 이하의 dot notation에 배포 됩니다.

var query = names.Where(s1 => s1.Length == 5)
                 .SelectMany(s1 => names, (s1,s2) => new {s1,s2})
                 .Where($1 => $1.s1 == $1.s2) 
                 .Select($1 => $1.s1 + " " + $1.s2);
   이 버전의 SelectMany는 외부의 순서와 안쪽의 순서로부터의 요소에 근거하는 결과를 작성하기 위해 사용하는 Lambda식을 추가로 받습니다.이 Lambda식에서는 두개의 범위 변수가 1 개의 익명형에 정리합니다.컴파일러에서는 후속의 Lambda식에서 익명형을 나타내기 위해, 변수명$1이 사용됩니다.

특수한 생성기가  join 절입니다.이 절에 의해, 특정의 키에 따라서 전의 절의 요소와 조합되는 다른 소스의 요소가 도입됩니다.join 절에서는 일치하는 요소가 1 개씩 생성됩니다.다만,into 절이 동시에 지정되어 있는 경우는 일치하는 요소가 그룹으로서 생성됩니다.

var query = from n in names
            join p in people on n equals p.Name into matching
            select new { Name = n, Count = matching.Count() };

당연, 이 쿼리는 상기의 쿼리의 1 에 직접 배포 됩니다.

var query = names.GroupJoin(people, n => n, p => p.Name,                   
            (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

이것은 어느A 쿼리의 결과를 후에 계속 되는 쿼리의 생성기로서 취급하는 경우에 유효합니다.이 기능을 지원하려면 쿼리 식에서 into 키워드를 사용해, select 절 또는 group 절의 뒤에 새로운 쿼리 식을 연결합니다.이것을 "쿼리 계속" 이라고 부릅니다.

특히, group by 절의 결과의 후처리를 실시하는 경우에, into 키워드가 도움이 됩니다.예를 들어, 다음의 프로그램을 생각해 보겠습니다.

var query = from item in names
            orderby item
            group item by item.Length into lengthGroups
            orderby lengthGroups.Key descending
            select lengthGroups;

foreach (var group in query) { 
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (var val in group)
        Console.WriteLine("  {0}", val);
}

이 프로그램의 출력은 다음과 같이 됩니다.

Strings of length 7
  Everett
Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank

여기에서는 C# 로 쿼리 식을 구현 하는 방법에 대해 설명했습니다.다른 언어에서는 추가의 쿼리 연산자가 명확한 구문과 함께 지원 되고 있는 경우도 있으면, 쿼리 식을 전혀 지원 하지 않는 경우도 있습니다.

쿼리 구문이 표준 쿼리 연산자에 고정적으로 연결시킬 수 있지 않은 점에 주의하는 것이 중요합니다.쿼리 구문은 순수하게 구문상의 기능으로, 적절한 이름과 서명으로 기본으로 되는 메서드를 구현 하는 것으로 "쿼리 패턴" 을 채우는 모든 것에 적용됩니다.여기까지에 설명한 표준 쿼리 연산자는 IEnumerable<T> 인터페이스를 추가한 확장 메서드를 사용하는 것으로 이것을 실시합니다.개발자는 쿼리 패턴에 준거하는 한, 필요한 메서드를 직접 구현 하는지, 확장 메서드를 추가하는 것으로, 희망하는 임의의 형식의 쿼리 구문을 개발할 수 있습니다.

LINQ 프로젝트 자체도 이러한 확장성을 살리고, 구개의 "LINQ 대응" 의 API 를 제공합니다.1 는 LINQ to SQL 로 SQL 기반의 데이터 접근의 LINQ 패턴을 구현 합니다.이제 하나는 LINQ to XML 로 XML 데이터로의 LINQ 쿼리를 가능하게 합니다.다음은 두번째에 대한 설명합니다.

LINQ to SQL: SQL 통합

로컬 프로그램 언어에 구문이나 컴파일시의 환경을 준비하지 않아도, .NET LINQ 를 사용해 관계형 데이터 스토어에 쿼리를 발행할 수 있습니다.이 기능 (코드네임 LINQ to SQL)에서는 CLR 메타데이터에의 SQL 스키마 통합을 사용하여 SQL 테이블 정의와 뷰 정의가 임의의 언어로부터 접근 할 수 있는 CLR 형에 컴파일 됩니다.

LINQ to SQL 에서는 핵심이 되는 두개의 속성 [Table] [Column] 이 정의되어 어느 CLR 형과 속성이 외부 SQL 데이터에 대응하는지를 나타냅니다. [Table] 속성은 클래스에 적용할 수 있어 CLR 형을 이름 첨부의 SQL 테이블이나 뷰에 관련짓습니다. [Column] 속성은 임의필드나 속성에 적용할 수 있어 멤버를 이름 첨부 SQL 열에 관련짓습니다.어느 쪽으로 적용한 속성도, SQL 고유의 메타데이터를 보관 유지하기 위해 매개 변수화할 수 있습니다.예를 들어, 다음의 단순한 SQL 스키마 정의를 생각합니다.

create table People (
    Name nvarchar(32) primary key not null, 
    Age int not null, 
    CanCode bit not null
)

create table Orders (
    OrderID nvarchar(32) primary key not null, 
    Customer nvarchar(32) not null, 
    Amount int
)

CLR 에서는 같은 정의가 다음과 같이 됩니다.

[Table(Name="People")]
public class Person {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string Name; 

  [Column]
  public int Age;

  [Column]
  public bool CanCode;
}

[Table(Name="Orders")]
public class Order {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string OrderID; 

  [Column(DbType="nvarchar(32) not null")]        
  public string Customer; 

  [Column]
  public int? Amount; 
}
   이 예로부터, NULL 값을 허용 하는 열은 CLR  NULL 값을 허용 하는 형식에 매핑 되는 것에 주의해 주세요.NULL 값을 허용 하는 형식은 .NET Framework 버전 2.0에서 처음으로 도입되었습니다.그 때문에, SQL 형에 일대일에 대응하는 CLR 형 (nvarchar,char,text 등)이 없는 경우는 Original SQL 형이 CLR 메타데이터에 보관 유지됩니다.

관계형 스토어에 쿼리를 발행하는 경우, LINQ 패턴의 LINQ to SQL 구현에서는 쿼리가 식 트리(expression tree) 형식으로부터 SQL 식과 리모트 평가에 적절한 ADO.NETDbCommand 개체에 변환됩니다.예를 들어, 다음과 같은 간단한 쿼리를 생각해 보겠습니다.

// ADO.NET sql 접속상에 쿼리 문맥을 확립합니다.
DataContext context = new DataContext(
     "Initial Catalog=petdb;Integrated Security=sspi");

// Person 와 Order의 각 CLR 형에 대응한다
// 리모트 테이블을 나타내는 변수를 설정합니다.
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders   = context.GetTable<Order>();

// 쿼리를 빌드 합니다.
var query = from c in custs
            from o in orders
            where o.Customer == c.Name
            select new { 
                       c.Name, 
                       o.OrderID,
                       o.Amount,
                       c.Age
            }; 

// 쿼리를 실행합니다.
foreach (var item in query) 
    Console.WriteLine("{0} {1} {2} {3}", 
                      item.Name, item.OrderID, 
                      item.Amount, item.Age);

DataContext 형식은 표준 쿼리 연산자를 SQL 로 변환하는 간이 트랜스레이터를 제공합니다.DataContext 에서 스토어의 접근에 기존의 ADO.NETIDbConnection 을 사용합니다.이것은 확립 끝난 ADO.NET 접속 개체 또는 접속에 사용할 수 있는 접속 문자열의 어느쪽으로든 초기화할 수 있습니다.

GetTable 메서드에서는 리모트 테이블이나 뷰를 나타내기 위해 쿼리 식 내부에 기록되어 있는 일에서 사용할 수 있는 IEnumerable 호환의 변수가 제공됩니다.GetTable을 호출해도 데이타베이스에 접근 하는 것은 아닙니다.어느 쪽일까하고 말하면, 쿼리 식을 사용해 리모트 테이블이나 뷰에 접근 할 "가능성" 을 나타냅니다. 위의 예에서는 프로그램으로부터 쿼리 식에서 반복 처리를 실시한다 (이 예에서는 C# 의foreach)까지, 쿼리가 스토어에 송신되지 않습니다.프로그램으로부터 쿼리의 최초 반복 처리를 하면, DataContext 에 의해 스토어에 송신되는 이하의 SQL 구문이 식 트리(expression tree)에서 기계적으로 변환됩니다.

SELECT [t0].[Age], [t1].[Amount], 
       [t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]

쿼리 기능을 로컬 프로그램 언어에 짜넣는 것으로, 개발자는 CLR 형과의 관계를 정적으로 확립할 필요없이, 관계형 모델의 전기능을 이용할 수 있는 점에 주의하는 것이 중요합니다.즉, 포괄적인 개체와 관계형 모델과 매핑하여, 이러한 기능을 대망 하는 사용자도 이러한 쿼리의 핵심 기능을 이용할 수 있게 됩니다. LINQ to SQL 에서는 개발자가 개체 사이의 릴레이션 쉽을 정의 및 안내할 수 있는 기능을 갖춘, 개체와 관계형 모델의 매핑 기능이 제공됩니다. 매핑을 사용하면, OrdersCustomer 클래스의 속성으로서 참조할 수 있기 위해, 2 를 서로 연결시키는 명시적인 결합은 필요 없습니다. 매핑 기능을 강화하기 위해, 외부 매핑 파일을 사용하는 것으로써, 개체 모델과 매핑을 분리할 수 있습니다.

LINQ to XML: XML 통합

XML 향해의 .NET LINQ (LINQ to XML)에서는 표준 쿼리 연산자와 선조 노드, 자식 노드, 형제 노드의 XPath 형식의 네비게이션을 제공하는 트리 고유의 연산자를 사용하고, XML 데이터의 쿼리가 가능하게 됩니다.XLinq 에서는 기존의System.Xml 리더/라이터 인프라에 통합되어 W3C DOM 보다 사용하기 쉬운, XML 의 효율적인 인메모리 표현이 제공됩니다. XML 와 쿼리를 통합하는 작업의 대부분을 실시하는 형식으로서 XName, XElement XAttribute 의 세가지가 있습니다.

XName 에서는 요소명과 속성명의 양쪽 모두에 사용되는 네임 스페이스로 수식된 식별자(dentifiers) (QNames)를 취급하는 방법이 제공됩니다. XName에서는 사용자가 의식하지 않지는 식별자(dentifiers)의 원자가 효율적으로 처리되고 QName 를 필요로 하는 임의의 장소에 심볼 또는 일반 문자열 이든 사용할 수 있습니다.

XML 요소와 속성은 각각 XElement XAttribute 를 사용해 나타납니다. XElement XAttribute 에서 표준의 구축 구문이 지원 되기 위해, 개발자는 자연스러운 구문을 사용해 XML 표기를 기술할 수 있습니다.

var e = new XElement("Person", 
                     new XAttribute("CanCode", true),
                     new XElement("Name", "Loren David"),
                     new XElement("Age", 31));

var s = e.ToString();

상기의 구문은 이하의 XML 에 대응합니다.

<Person CanCode="true">
  <Name>Loren David</Name> 
  <Age>31</Age> 
</Person>

XML 표기의 작성에 DOM 기반의 팩토리 패턴이 필요없고, ToString 구현에 의해 텍스트 형식의 XML의 생성에 주목해 주세요.다음과 같이 기존의 XmlReader, 문자열 리터럴에서 XML 요소를 구축할 수 있습니다.

var e2 = XElement.Load(xmlReader);
var e1 = XElement.Parse(
@"<Person CanCode='true'>
  <Name>Loren David</Name>
  <Age>31</Age>
</Person>");

XElement 에서는 기존의 XmlWriter 형식을 사용한 XML 발행도 지원 합니다.

XElement 에는 쿼리 연산자가 효과적으로 결합되기 위해, 개발자는 XML 이외의 정보에 대한 쿼리를 기술할 수 있어 Select 구문에서XElements를 구축하여 결과를 XML로 생성할 수 있습니다.

var query = from p in people 
            where p.CanCode
            select new XElement("Person", 
                                  new XAttribute("Age", p.Age),
                                  p.Name);

상기의 쿼리는 XElements 의 순서를 반환합니다.이런 종류의 쿼리 결과로부터 XElements 구축을 가능하도록, XElement 생성자의 인수에 직접 요소의 순서를 건네줄 수 있습니다.

var x = new XElement("People",
                  from p in people 
                  where p.CanCode
                  select 
                    new XElement("Person", 
                                   new XAttribute("Age", p.Age),
                                   p.Name));

이 XML 표기는 결과적으로 다음과 같은 XML 가 됩니다.

<People>
  <Person Age="11">Allen Frances</Person> 
  <Person Age="59">Connor Morgan</Person> 
</People>

상기의 XML 는 Visual Basic 에 직접 변환됩니다.다만, Visual Basic 9.0 에서는 XML 리터럴의 사용도 지원 됩니다. XML 리터럴에서는 Visual Basic 에서 선언형의 XML 구문을 직접 사용하고, 쿼리 식을 표기할 수 있습니다.상기의 예는 다음의 Visual Basic 구문을 사용해 구축할 수 있습니다.

 Dim x = _
        <People>
             <%= From p In people __
                 Where p.CanCode _

                 Select <Person Age=<%= p.Age %>>p.Name</Person> _
             %>
        </People>

여기까지의 예에서는 LINQ 를 사용해 새롭고 XML 값을 "구축" 하는 방법을 나타냈습니다. XElement 형식과 XAttribute 형식에 의해, XML 구조로부터의 정보의 "추출" 도 쉽게 됩니다. XElement 에는 기존의 XPath 축으로 쿼리 식을 적용할 수 있는 접근자 메서드(accessor methods)가 있습니다.예를 들어, 다음의 쿼리는 상기의 XElement 에서 이름만을 추출합니다.

IEnumerable<string> justNames =
    from e in x.Descendants("Person")
    select e.Value;

//justNames = ["Allen Frances", "Connor Morgan"]

XML 에서 구조화 된 값을 추출하려면, 다음과 같이 Select 구로 단순하게 개체 초기화자를 사용합니다.

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int)e.Attribute("Age") 
};

XAttribute XElement는 어느쪽이나 텍스트 값을 원시적형으로서 추출하기 위해 명시적인 변환을 지원 합니다.발견되지 않은 데이터를 처리하는 경우는 단순하게 NULL 값을 허용하는 형식에 캐스트 할 수 있습니다.

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int?)e.Attribute("Age") ?? 21
};

이 경우는 Age 속성이 발견되지 않았을 때에 디폴트값으로서 21 이 사용됩니다.

Visual Basic 9.0 에서는 XElement접근자 메서드(accessor methods)로서 Elements, Attribute Descendants 가 언어내에서 직접 지원 되기 위해, XML 축 속성이라고 한다, 게다가 컴팩트한 직접 구문을 사용하고, XML 기반의 데이터에 접근 할 수 있습니다.전술의 C# 구문은 이 기능을 사용하면, 다음과 같이 기술할 수 있습니다.

Dim persons = _
      From e In x...<Person> _   
      Select new Person { _
          .Name = e.Value, _
          .Age = IIF(e.@Age, 21) _
} 

Visual Basic 에서는 x...<Person> 이름이 Person x의 자식 콜렉션이 모두 취득된 식e.@Age 이름이 Age XAttributes 가 모두 검색됩니다.Value 속성은 콜렉션내의 최초의 속성을 취득하여, 그 속성의 Value 속성을 호출합니다.

요약

.NET LINQ 에서는 CLR 와 CLR 가 대상으로 하는 언어에 쿼리 기능이 추가됩니다.쿼리 기능은 Lambda식과 식 트리(expression tree)를 기본으로 구축되고 사용자에는 밝혀지지 않는 실행 가능 코드로서 또는 다운 스트림의 처리나 변환에 적절한, 사용자에는 의식되지 않는 인메모리데이타로서 서술어, 프로젝션 및 키 추출식을 사용할 수 있습니다.LINQ 프로젝트로 정의되는 표준 쿼리 연산자는 모든 IEnumerable<T> 기반의 정보 소스로 기능합니다.또, ADO.NET (LINQ to SQL)나 System.Xml (LINQ to XML)에 통합되기 위해, 관계형 데이터나 XML 데이터의 처리에 LINQ의 장점을 살릴 수 있습니다.

표준 쿼리 연산자 목록

연산자 설명
Where 서서술어 함수에 근거하는 제한 연산자
Select/SelectMany 선택 함수에 근거하는 프로젝션 연산자
Take/Skip/ TakeWhile/SkipWhile 위치 결정 함수 또는 서술어 함수에 근거하는 파티션 분할 연산자
Join/GroupJoin 키 선택 함수에 근거하는 결합 연산자
Concat 연결 연산자
OrderBy/ThenBy/OrderByDescending/ThenByDescending 생략 가능한 키 선택 함수와 비교 함수에 근거해 올림 또는 내림차순에 늘어놓는  정렬 연산자
Reverse 순서의 순서를 반전하는 정렬 연산자
GroupBy 생략 가능한 키 선택 함수와 비교 함수에 근거하는 그룹화 연산자
Distinct 중복을 삭제하는 세트 연산자
Union/Intersect 합집합 또는 교집합을 돌려주는 세트 연산자
Except 차집합을 돌려주는 세트 연산자
AsEnumerable IEnumerable<T>변환 연산자
ToArray/ToList 배열 또는 List<T> 변환 연산자
ToDictionary/ToLookup 키 선택 함수에 근거한 Dictionary<K,T> 또는Lookup<K,T> (다중 사전) 변환 연산자
OfType/Cast 필터 선택에 근거한 IEnumerable<T> 에의 변환 연산자 또는 형식 인수 변환
SequenceEqual 대가 된 요소의 등치성을 체크하는 등가 연산자
First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault 생략 가능한 서술어 함수에 근거해 초기 요소, 최종 요소 또는 유일한 요소를 돌려주는 요소 연산자
ElementAt/ElementAtOrDefault 위치에 근거하고 요소를 돌려주는 요소 연산자
DefaultIfEmpty 빈의 순서를 디폴트 값의 단일 순서에 옮겨놓는 요소 연산자
Range 범위내의 수를 돌려주는 생성 연산자
Repeat 특정의 값의 복수의 출현을 돌려주는 생성 연산자
Empty 비어 있는 순서를 돌려주는 생성 연산자
All/Any 서술어 함수의 실존적 또는 보편적 충족도를 체크하는 한정자
Contains 특정 요소의 존재를 체크하는 한정자
Count/LongCount 생략 가능한 서술어 함수에 근거하여 요소를 세는 집계 연산자
Sum/Min/Max/Average 생략 가능한 선택 함수에 근거하는 집계 연산자
Aggregate 누적 함수 및 생략 가능한 배정을 기본으로 복수의 값을 집적하는 집계 연산자


출처 : http://www.microsoft.com/Korea/MSDN/library/bb308959.aspx

'C# > LINQ' 카테고리의 다른 글

LINQPad 를 이용해 보세요  (0) 2011.11.11
Linq to DataSet Group By Sum  (0) 2011.11.11
LINQ 아키텍처  (0) 2010.09.16

관련글 더보기