LINQ: .NET Language-Integrated 쿼리

 

돈 박스, 앤더스 헬스버그

2007년 2월

적용 대상:
   Visual Studio Code 이름 "Orcas"
   .Net Framework 3.5

요약: .NET Framework 추가된 범용 쿼리 기능은 관계형 또는 XML 데이터뿐만 아니라 모든 정보 원본에 적용됩니다. 이 기능을 LINQ(.NET Language-Integrated Query)라고 합니다. (인쇄된 페이지 32개)

콘텐츠

.NET Language-Integrated 쿼리
표준 쿼리 연산자를 사용하여 시작
LINQ 프로젝트를 지원하는 언어 기능
더 많은 표준 쿼리 연산자
쿼리 구문
LINQ to SQL: SQL 통합
LINQ to XML: XML 통합
요약

.NET Language-Integrated 쿼리

이십 년 후, 업계는 개체 지향 (OO) 프로그래밍 기술의 진화에 안정적인 지점에 도달했다. 프로그래머는 이제 클래스, 개체 및 메서드와 같은 당연한 기능을 사용합니다. 현재 및 차세대 기술을 살펴보면 프로그래밍 기술의 다음 큰 과제는 OO 기술을 사용하여 고유하게 정의되지 않은 정보에 액세스하고 통합하는 복잡성을 줄이는 것입니다. 비 OO 정보의 가장 일반적인 두 가지 원본은 관계형 데이터베이스와 XML입니다.

LINQ 프로젝트를 통해 프로그래밍 언어 및 런타임에 관계형 또는 XML 관련 기능을 추가하는 대신 보다 일반적인 접근 방식을 취하고 관계형 또는 XML 데이터뿐만 아니라 모든 정보 원본에 적용되는 .NET Framework 범용 쿼리 기능을 추가하고 있습니다. 이 기능을 LINQ(.NET Language-Integrated Query)라고 합니다.

언어 통합 쿼리라는 용어를 사용하여 쿼리가 개발자의 기본 프로그래밍 언어(예: Visual C#, Visual Basic)의 통합 기능임을 나타냅니다. 언어 통합 쿼리를 사용하면 쿼리 식 이 이전에 명령적 코드에서만 사용할 수 있었던 풍부한 메타데이터, 컴파일 시간 구문 검사, 정적 입력 및 IntelliSense를 활용할 수 있습니다. 또한 언어 통합 쿼리를 사용하면 단일 범용 선언적 쿼리 기능을 외부 원본의 정보뿐만 아니라 모든 메모리 내 정보에 적용할 수 있습니다.

.NET Language-Integrated 쿼리는 모든 에서 직접적이면서도 선언적인 방식으로 트래버스, 필터 및 프로젝션 작업을 표현할 수 있는 범용 표준 쿼리 연산 자 집합을 정의합니다. NET 기반 프로그래밍 언어입니다. 표준 쿼리 연산자를 사용하면 모든 IEnumerable T> 기반 정보 원본에 쿼리를 적용할 수 있습니다<. LINQ를 사용하면 타사에서 대상 도메인 또는 기술에 적합한 새 도메인별 연산자를 사용하여 표준 쿼리 연산자 집합을 보강할 수 있습니다. 또한 제3자는 표준 쿼리 연산자를 원격 평가, 쿼리 변환, 최적화 등과 같은 추가 서비스를 제공하는 자체 구현으로 자유롭게 대체할 수 있습니다. LINQ 패턴의 규칙을 준수함으로써 이러한 구현은 표준 쿼리 연산자와 동일한 언어 통합 및 도구 지원을 누릴 수 있습니다.

쿼리 아키텍처의 확장성은 LINQ 프로젝트 자체에서 XML 및 SQL 데이터 둘 다에서 작동하는 구현을 제공하는 데 사용됩니다. XML을 통한 쿼리 연산자(LINQ to XML)는 효율적이고 사용하기 쉬운 메모리 내 XML 기능을 사용하여 호스트 프로그래밍 언어로 XPath/XQuery 기능을 제공합니다. 관계형 데이터(LINQ to SQL)에 대한 쿼리 연산자는 SQL 기반 스키마 정의를 CLR(공용 언어 런타임) 형식 시스템에 통합하는 것을 기반으로 합니다. 이 통합은 관계형 모델의 표현력과 기본 저장소에서 직접 쿼리 평가의 성능을 유지하면서 관계형 데이터에 대한 강력한 입력을 제공합니다.

표준 쿼리 연산자를 사용하여 시작

직장에서 언어 통합 쿼리를 보려면 표준 쿼리 연산자를 사용하여 배열의 내용을 처리하는 간단한 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
To understand how language-integrated query works, we need to dissect the
 first statement of our program.
IEnumerable<string> query = from s in names 
                           where s.Length == 5
                           orderby s
                           select s.ToUpper();

로컬 변수 쿼리쿼리 식을 사용하여 초기화됩니다. 쿼리 식은 표준 쿼리 연산자 또는 도메인별 연산자에서 하나 이상의 쿼리 연산자를 적용하여 하나 이상의 정보 원본에서 작동합니다. 이 식은 세 가지 표준 쿼리 연산자인 Where, OrderBySelect를 사용합니다.

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, OrderBySelect 연산자의 인수를 대리자와 매우 유사한 코드 조각인 람다 식이라고 합니다. 표준 쿼리 연산자를 메서드로 개별적으로 정의하고 점 표기법을 사용하여 함께 묶을 수 있습니다. 이러한 메서드는 함께 확장 가능한 쿼리 언어의 기초를 형성합니다.

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

LINQ는 전적으로 범용 언어 기능을 기반으로 하며, 그 중 일부는 C# 3.0 및 Visual Basic 9.0의 새로운 기능입니다. 이러한 각 기능에는 자체적으로 유틸리티가 있지만 전체적으로 이러한 기능은 쿼리 및 쿼리 가능한 API를 정의하는 확장 가능한 방법을 제공합니다. 이 섹션에서는 이러한 언어 기능과 이러한 기능이 훨씬 더 직접적이고 선언적인 쿼리 스타일에 어떻게 기여하는지 살펴봅니다.

람다 식 및 식 트리

많은 쿼리 연산자를 사용하면 사용자가 필터링, 프로젝션 또는 키 추출을 수행하는 함수를 제공할 수 있습니다. 쿼리 기능은 개발자가 후속 평가를 위해 인수로 전달할 수 있는 함수를 작성하는 편리한 방법을 제공하는 람다 식의 개념을 기반으로 합니다. 람다 식은 CLR 대리자와 유사하며 대리자 형식으로 정의된 메서드 서명을 준수해야 합니다. 이를 설명하기 위해 Func 대리자 형식을 사용하여 위의 문을 동등하지만 더 명시적인 형식으로 확장할 수 있습니다.

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);

람다 식은 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);

일반적으로 개발자는 쿼리 연산자에서 명명된 메서드, 익명 메서드 또는 람다 식을 자유롭게 사용할 수 있습니다. 람다 식은 작성에 가장 직접적이고 압축된 구문을 제공하는 이점이 있습니다. 더 중요한 것은 람다 식을 코드 또는 데이터로 컴파일할 수 있으므로 최적화 프로그램, 번역기 및 계산기에서 런타임에 람다 식을 처리할 수 있습니다.

네임스페이스 System.Linq.Expressions는 기존의 IL 기반 메서드 본문이 아닌 지정된 람다 식에 식 트리가 필요했음을 나타내는 고유 제네릭 형식인 Expression<T>를 정의합니다. 식 트리는 람다 식의 효율적인 메모리 내 데이터 표현이며 식의 구조를 투명하고 명시적으로 만듭니다.

컴파일러가 실행 파일 IL 또는 식 트리를 내보할지 여부를 결정하는 것은 람다 식이 사용되는 방식에 따라 결정됩니다. 형식이 대리자인 변수, 필드 또는 매개 변수에 람다 식이 할당되면 컴파일러는 익명 메서드와 동일한 IL을 내보낸다. 람다 식이 일부 대리자 형식 T에 대해 Expression<T> 형식인 변수, 필드 또는 매개 변수에 할당되면 컴파일러는 대신 식 트리를 내보낸다.

예를 들어 다음 두 변수 선언을 고려합니다.

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

변수 f는 직접 실행 가능한 대리자를 참조합니다.

bool isSmall = f(2); // isSmall is now true

변수 e 는 직접 실행 가능하지 않은 식 트리에 대한 참조입니다.

bool isSmall = e(2); // compile error, expressions == data

효과적으로 불투명한 코드인 대리자와 달리 프로그램의 다른 데이터 구조와 마찬가지로 식 트리와 상호 작용할 수 있습니다.

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);

위의 예제에서는 런타임에 식 트리를 분해하고 다음 문자열을 출력합니다.

n LessThan 5

런타임 시 식을 데이터로 처리하는 이 기능은 플랫폼의 일부인 기본 쿼리 추상화 를 활용하는 타사 라이브러리의 에코시스템을 사용하도록 설정하는 데 중요합니다. LINQ to SQL 데이터 액세스 구현은 이 기능을 활용하여 식 트리를 저장소의 평가에 적합한 T-SQL 문으로 변환합니다.

확장 메서드

람다 식은 쿼리 아키텍처의 한 가지 중요한 부분입니다. 확장 메서드 는 또 다른 메서드입니다. 확장 메서드는 동적 언어에서 널리 사용되는 "duck typing"의 유연성과 정적 형식 언어의 성능 및 컴파일 시간 유효성 검사를 결합합니다. 확장 메서드를 사용하면 제3자가 새 메서드를 사용하여 형식의 공개 계약을 보강하는 동시에 개별 형식 작성자가 해당 메서드의 고유한 특수 구현을 제공할 수 있습니다.

확장 메서드는 정적 클래스에서 정적 메서드로 정의되지만 CLR 메타데이터에서 [System.Runtime.CompilerServices.Extension] 특성으로 표시됩니다. 언어는 확장 메서드에 대한 직접 구문을 제공하는 것이 좋습니다. C#에서 확장 메서드는 확장 메서드의 첫 번째 매개 변수에 적용해야 하는 한정자에 의해 표시됩니다. 가장 간단한 쿼리 연산자의 정의를 살펴보겠습니다 . 여기서는 다음과 같습니다.

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);

그러나 확장 메서드를 고유하게 만드는 것은 instance 구문을 사용하여 호출할 수도 있다는 것입니다.

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

확장 메서드는 scope 확장 메서드에 따라 컴파일 시간에 확인됩니다. C#의 using 문 또는 Visual Basic의 Import 문으로 네임스페이스를 가져오면 해당 네임스페이스의 정적 클래스에 의해 정의된 모든 확장 메서드가 scope 가져옵니다.

표준 쿼리 연산자는 System.Linq.Enumerable 형식의 확장 메서드로 정의됩니다. 표준 쿼리 연산자를 검사할 때 몇 개의 쿼리 연산자를 제외한 모든 연산자가 IEnumerable T> 인터페이스 측면에서 정의되어 있음을 알 수 있습니다<. 즉, 모든 IEnumerable<T> 호환 정보 원본은 C#에서 다음 using 문을 추가하여 표준 쿼리 연산자를 가져옵니다.

using System.Linq; // makes query operators visible

특정 형식의 표준 쿼리 연산자를 바꾸려는 사용자는 특정 형식에 대해 동일한 이름의 메서드를 호환되는 서명으로 정의하거나 특정 형식을 확장하는 새로운 동일한 이름의 확장 메서드를 정의할 수 있습니다. 표준 쿼리 연산자를 완전히 피하려는 사용자는 단순히 System.Linq를 scope 배치하고 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;
  }
}

이 클래스 정의를 고려할 때 다음 프로그램은 확장 메서드보다 우선적으로 instance 메서드가 우선하므로 확장 메서드가 아닌 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;
}

OfTypeIEnumerable<T> 기반 원본뿐만 아니라 .NET Framework 버전 1.0에 있는 매개 변수가 없는 IEnumerable 인터페이스에 대해 작성된 원본도 허용합니다. OfType 연산자를 사용하면 다음과 같이 사용자가 표준 쿼리 연산자를 클래식 .NET 컬렉션에 적용할 수 있습니다.

// "classic" cannot be used directly with query operators
IEnumerable classic = new OlderCollectionType();

// "modern" can be used directly with query operators
IEnumerable<object> modern = classic.OfType<object>();

이 예제에서 변수 modern클래식과 동일한 값 시퀀스를 생성합니다. 그러나 해당 형식은 표준 쿼리 연산자를 포함하여 최신 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의 사용은 foreach 문을 사용하거나 기본 GetEnumeratorMoveNext 메서드를 수동으로 사용하여 쿼리가 반복될 때까지 쿼리가 실제로 평가되지 않는다는 흥미로운 이점이 있습니다. 이 지연된 평가를 사용하면 잠재적으로 다른 결과를 생성할 때마다 여러 번 평가할 수 있는 IEnumerable<T> 기반 값으로 쿼리를 유지할 수 있습니다.

많은 애플리케이션에서 이것이 원하는 동작입니다. 쿼리 평가 결과를 캐시하려는 애플리케이션의 경우 쿼리를 즉시 평가하도록 하고 쿼리 평가 결과를 포함하는 목록<T> 또는 배열을 반환하는 두 연산자 ToListToArray가 제공됩니다.

지연된 쿼리 평가의 작동 방식을 확인하려면 배열에 대해 간단한 쿼리를 실행하는 이 프로그램을 고려합니다.

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents a query
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');

// evaluate the query
foreach (string item in ayes) 
  Console.WriteLine(item);

// modify the original information source
names[0] = "Bob";

// evaluate the query again, this time no "Allen"
foreach (string item in ayes) 
    Console.WriteLine(item);

쿼리는 변수 반복될 때마다 평가됩니다. 캐시된 결과 복사본이 필요함을 나타내기 위해 다음과 같이 ToList 또는 ToArray 연산자를 쿼리에 추가하면 됩니다.

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents the result
// of an immediate query evaluation
string[] ayes = names.Where(s => s[0] == 'A').ToArray();

// iterate over the cached query results
foreach (string item in ayes) 
    Console.WriteLine(item);

// modifying the original source has no effect on ayes
names[0] = "Bob";

// iterate over result again, which still contains "Allen"
foreach (string item in ayes)
    Console.WriteLine(item);

ToArrayToList 모두 즉시 쿼리 평가를 강제로 수행합니다. 싱글톤 값을 반환하는 표준 쿼리 연산자도 마찬가지입니다(예: First, ElementAt, Sum, Average, All, Any).

IQueryable<T> 인터페이스

일반적으로 LINQ to SQL 같은 식 트리를 사용하여 쿼리 기능을 구현하는 데이터 원본에 대해 동일한 지연된 실행 모델이 필요합니다. 이러한 데이터 원본은 식 트리를 사용하여 LINQ 패턴에 필요한 모든 쿼리 연산자가 구현되는 IQueryable<T> 인터페이스를 구현하는 이점을 얻을 수 있습니다. 각 IQueryable<T> 에는 식 트리의 형태로 "쿼리를 실행하는 데 필요한 코드"의 표현이 있습니다. 지연된 모든 쿼리 연산자는 해당 쿼리 연산자에 대한 호출의 표현으로 해당 식 트리를 보강하는 새 IQueryable<T> 를 반환합니다. 따라서 쿼리를 평가할 시간이 되면 일반적으로 IQueryable<T> 가 열거되기 때문에 데이터 원본은 전체 쿼리를 나타내는 식 트리를 하나의 일괄 처리로 처리할 수 있습니다. 예를 들어 쿼리 연산자를 여러 번 호출하여 얻은 복잡한 LINQ to SQL 쿼리로 인해 단일 SQL 쿼리만 데이터베이스로 전송될 수 있습니다.

IQueryable<T> 인터페이스를 구현하여 이 지연 기능을 다시 사용하는 데이터 원본 구현자의 이점은 분명합니다. 반면에 쿼리를 작성하는 클라이언트에게는 원격 정보 원본에 대한 공통 형식을 갖는 것이 큰 장점입니다. 다양한 데이터 원본에 대해 사용할 수 있는 다형 쿼리를 작성할 수 있을 뿐만 아니라 도메인 간에 실행되는 쿼리를 작성할 수도 있습니다.

복합 값 초기화

람다 식 및 확장 메서드는 단순히 값 시퀀스에서 멤버를 필터링하는 쿼리에 필요한 모든 것을 제공합니다. 또한 대부분의 쿼리 식은 해당 멤버에 대한 프로젝션을 수행하여 원래 시퀀스의 멤버를 원래 값과 형식이 다를 수 있는 멤버로 효과적으로 변환합니다. 이러한 변환 작성을 지원하기 위해 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;

개체 이니셜라이저는 식만 허용되는 컨텍스트(예: 람다 식 및 식 트리 내)에서 새로운 구조화된 값을 생성할 수 있으므로 언어 통합 쿼리에 중요한 기능입니다. 예를 들어 입력 시퀀스의 각 값에 대해 새 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 { // note the omission of type name
    Name = "Brian Smith", Age = 31, CanCode = false
};

변수 v1v2 는 모두 CLR 형식에 Name,AgeCanCode라는 세 가지 공용 속성이 있는 메모리 내 개체를 가리킵니다. 변수는 v2익명 형식의 instance 참조한다는 점에서 다릅니다. CLR 용어로 익명 형식은 다른 형식과 다르지 않습니다. 익명 형식을 특별하게 만드는 것은 프로그래밍 언어에 의미 있는 이름이 없다는 것입니다. 익명 형식의 인스턴스를 만드는 유일한 방법은 위에 표시된 구문을 사용하는 것입니다.

변수가 익명 형식의 인스턴스를 참조할 수 있지만 여전히 정적 입력의 이점을 누릴 수 있도록 C#에서는 암시적으로 형식화된 지역 변수를 도입합니다. var 키워드(keyword) 지역 변수 선언의 형식 이름 대신 사용할 수 있습니다. 예를 들어 이 법적 C# 3.0 프로그램을 고려합니다.

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

var 키워드(keyword) 변수를 초기화하는 데 사용되는 식의 정적 형식에서 변수 형식을 유추하도록 컴파일러에 지시합니다. 이 예제에서 s, nb 의 형식은 각각 string, intbool입니다. 이 프로그램은 다음과 같습니다.

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

var 키워드(keyword) 형식에 의미 있는 이름이 있는 변수의 경우 편리하지만 익명 형식의 인스턴스를 참조하는 변수는 필수입니다.

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

위의 예제에서 변수 은 다음 의사-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() { ... }
}

무명 형식은 어셈블리 경계 간에 공유할 수 없습니다. 그러나 컴파일러는 각 어셈블리 내에서 지정된 속성 이름/형식 쌍 시퀀스에 대해 익명 형식이 하나 이상 있는지 확인합니다.

익명 형식은 프로젝션에서 기존 구조적 값의 하나 이상의 멤버를 선택하는 데 자주 사용되므로 익명 형식의 초기화에서 다른 값의 필드 또는 속성을 참조하기만 하면 됩니다. 이렇게 하면 이름, 형식 및 값이 모두 참조된 속성 또는 필드에서 복사되는 속성을 새 익명 형식으로 가져옵니다.

instance 경우 다른 값의 속성을 결합하여 새 구조화된 값을 만드는 이 예제를 고려해 보세요.

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 }
};

두 경우 모두 커플 변수는 bobjane에서 NameAge 속성의 자체 복사본을 가져옵니다.

익명 형식은 쿼리의 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 연산자입니다.

OrderByOrderByDescending 연산자는 모든 정보 원본에 적용할 수 있으며 사용자가 결과를 정렬하는 데 사용되는 값을 생성하는 키 추출 함수를 제공할 수 있습니다. OrderByOrderByDescending 은 키에 부분 순서를 적용하는 데 사용할 수 있는 선택적 비교 함수도 허용합니다. 기본 예제를 살펴보겠습니다.

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

// unity sort
var s1 = names.OrderBy(s => s); 
var s2 = names.OrderByDescending(s => s);

// sort by length
var s3 = names.OrderBy(s => s.Length); 
var s4 = names.OrderByDescending(s => s.Length);

처음 두 쿼리 식은 문자열 비교를 기반으로 원본의 멤버 정렬을 기반으로 하는 새 시퀀스를 생성합니다. 두 번째 두 쿼리는 각 문자열의 길이에 따라 원본의 멤버 정렬을 기반으로 하는 새 시퀀스를 생성합니다.

여러 정렬 조건을 허용하려면 OrderBy와 OrderByDescending 모두 일반 IEnumerable<T가 아닌 OrderedSequence T>를 반환합니다.>< 두 연산자는 OrderedSequence<T>에서만 정의됩니다. 즉, ThenByThenByDescending 은 추가(하위) 정렬 조건을 적용합니다. 그런 다음By/ThenByDescendingOrderedSequence<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 는 단순히 시퀀스를 열거하고 동일한 값을 역순으로 생성합니다. OrderBy와 달리 Reverse는 순서를 결정할 때 실제 값 자체를 고려하지 않고 기본 원본에서 값이 생성되는 순서에만 의존합니다.

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"};

// group by length
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

la Select, GroupBy 를 사용하면 그룹의 멤버를 채우는 데 사용되는 프로젝션 함수를 제공할 수 있습니다.

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

// group by length
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

참고 이 예제에서 프로젝스된 형식은 원본과 같을 필요가 없습니다. 이 경우 문자열 시퀀스에서 문자로 정수 그룹화가 생성되었습니다.

집계 연산자

값 시퀀스를 단일 값으로 집계하기 위해 여러 표준 쿼리 연산자가 정의됩니다. 가장 일반적인 집계 연산자는 다음과 같이 정의된 집계입니다.

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가 람다 식을 호출할 때마다 시퀀스의 멤버와 집계된 값을 모두 전달합니다(초기 값은 Aggregate에 대한 시드 매개 변수임). 람다 식의 결과는 이전 집계 값을 대체하고 Aggregate 는 람다 식의 최종 결과를 반환합니다.

예를 들어 이 프로그램은 집계 를 사용하여 문자열 배열에 대한 총 문자 수를 누적합니다.

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

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

범용 집계 연산자 외에도 표준 쿼리 연산자에는 범용 Count 연산자와 이러한 일반적인 집계 작업을 간소화하는 4개의 숫자 집계 연산자(최소, 최대, 합계평균)도 포함됩니다. 숫자 집계 함수는 시퀀스의 멤버를 숫자 형식으로 프로젝션하는 함수가 제공되는 한 숫자 형식의 시퀀스(예: 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

참고 두 번째 Sum 문은 집계를 사용하는 이전 예제와 동일합니다.

선택 및 선택관리

Select 연산자를 사용하려면 변환 함수가 원본 시퀀스의 각 값에 대해 하나의 값을 생성해야 합니다. 변환 함수가 시퀀스 자체의 값을 반환하는 경우 하위 시퀀스를 수동으로 트래버스하는 것은 소비자에게 달려 있습니다. 예를 들어 기존 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.

쿼리가 병합된 토큰 시퀀스를 반환하고 중간 문자열[] 을 소비자에게 노출하지 않는 것이 좋습니다. 이를 위해 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에 전달된 람다 식에서 중첩된 쿼리는 n 다른 원본에 적용되지만 scope 외부 원본에서 전달된 매개 변수가 있습니다. 따라서 사람들. 여기서 는 각 n에 대해 한 번 호출되며, 결과 시퀀스는 최종 출력에 대해 SelectMany 에 의해 평면화됩니다. 결과는 이름 배열에 이름이 표시되는 모든 사람의 시퀀스입니다.

조인 연산자

개체 지향 프로그램에서 서로 관련된 개체는 일반적으로 탐색하기 쉬운 개체 참조와 연결됩니다. 일반적으로 데이터 항목은 ID 또는 엔터티를 고유하게 식별할 수 있는 다른 데이터와 함께 기호적으로 서로를 "가리킬" 수밖에 없는 외부 정보 원본에도 마찬가지입니다. 조인의 개념은 시퀀스의 요소를 다른 시퀀스에서 "일치"하는 요소와 함께 가져오는 작업을 의미합니다.

SelectMany의 이전 예제에서는 실제로 문자열을 해당 문자열인 이름의 사용자와 일치시키는 작업을 수행합니다. 그러나 이러한 특정 목적을 위해 SelectMany 접근 방식은 매우 효율적이지 않습니다. 이름의 각 요소와 모든 요소에 대해 사람들의 모든 요소를 반복합니다. 이 시나리오의 모든 정보(두 정보 원본과 일치하는 "키")를 하나의 메서드 호출에서 함께 가져오면 Join 연산자는 훨씬 더 나은 작업을 수행할 수 있습니다.

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

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

이것은 약간 한 입이지만 조각이 어떻게 결합되는지 확인합니다. Join 메서드는 "외부" 데이터 원본 이름에 대해 호출됩니다. 첫 번째 인수는 "내부" 데이터 원본인 people입니다. 두 번째 및 세 번째 인수는 각각 외부 및 내부 소스의 요소에서 키를 추출하는 람다 식입니다. 이러한 키는 Join 메서드가 요소를 일치시킬 때 사용하는 키입니다. 여기서는 이름 자체가 사람들의 Name 속성과 일치하도록 합니다. 그런 다음 최종 람다 식은 결과 시퀀스의 요소를 생성합니다. 이 식은 일치하는 요소 np의 각 쌍으로 호출되며 결과를 셰이핑하는 데 사용됩니다. 이 경우 n 을 삭제하고 p를 반환하도록 선택합니다. 최종 결과는 이름이이름 목록에 있는 사람의Person 요소 목록입니다.

인의 더 강력한 사촌은 GroupJoin 연산자입니다. GroupJoin 은 결과 셰이핑 람다 식이 사용되는 방식에서 Join 과 다릅니다. 외부 및 내부 요소의 각 개별 쌍으로 호출되는 대신 각 외부 요소에 대해 한 번만 호출되고 해당 외부 요소와 일치하는 모든 내부 요소의 시퀀스를 사용하여 호출됩니다. 이를 구체적으로 만들려면 다음을 수행합니다.

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() }
);

이 호출은 해당 이름을 가진 사용자 수와 쌍으로 시작한 이름의 시퀀스를 생성합니다. 따라서 GroupJoin 연산자를 사용하면 외부 요소의 전체 "일치 집합"에 대한 결과를 기반으로 할 수 있습니다.

쿼리 구문

C#의 기존 foreach 문은 .NET Frameworks IEnumerable/IEnumerator 메서드를 반복하기 위한 선언적 구문을 제공합니다. foreach 문은 엄격하게 선택 사항이지만 매우 편리하고 인기있는 언어 메커니즘으로 입증되었습니다.

이 전례를 기반으로 쿼리 식은 가장 일반적인 쿼리 연산자(Where, Join, GroupJoin, Select, SelectMany, GroupBy, OrderBy, ThenBy, OrderByDescending, ThenByDescending, ThenByDescendingCast)에 대한 선언적 구문을 사용하여 쿼리를 간소화합니다.

먼저 다음을 사용하여 이 문서를 시작한 간단한 쿼리를 살펴보겠습니다.

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 문과 마찬가지로 쿼리 식은 더 간결하고 읽기 쉽지만 완전히 선택 사항입니다. 쿼리 식으로 작성할 수 있는 모든 식에는 점 표기법을 사용하는 해당 구문이 있습니다(자세한 내용은 아니지만).

먼저 쿼리 식의 기본 구조를 살펴보겠습니다. C#의 모든 구문 쿼리 식은 from 절로 시작하고 select 또는 group 절로 끝납니다. from 절의 초기 다음에는 0개 이상의 from, let, where, joinorderby 절이 뒤따를 수 있습니다. 각 from 절은 시퀀스에 범위 변수를 도입하는 생성기입니다. 각 let 절은 식의 결과에 이름을 지정합니다. 및 각 where 절은 결과에서 항목을 제외하는 필터입니다. 모든 인 절은 새 데이터 원본과 이전 절의 결과를 상호 연결합니다. 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 keyExprquery-continuation ::= intoitemName 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;

컴파일러는 이러한 쿼리 식을 다음과 같은 명시적 점 표기법을 사용하여 작성된 것처럼 처리합니다.

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
                   });

쿼리 식은 특정 이름의 메서드 호출로 기계적으로 변환됩니다. 따라서 선택된 정확한 쿼리 연산자 구현은 쿼리되는 변수의 형식과 scope 있는 확장 메서드 모두에 따라 달라집니다.

지금까지 표시된 쿼리 식은 하나의 생성기만 사용했습니다. 둘 이상의 생성기를 사용하는 경우 각 후속 생성기는 선행 작업의 컨텍스트에서 평가됩니다. 예를 들어 다음과 같이 쿼리를 약간 수정하는 것이 좋습니다.

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

위의 쿼리 식은 이 점 표기법 식으로 확장됩니다.

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 는 외부 및 내부 시퀀스의 요소를 기반으로 결과를 생성하는 데 사용되는 추가 람다 식을 사용합니다. 이 람다 식에서 두 범위 변수는 무명 형식으로 수집됩니다. 컴파일러는 변수 이름 $1 을 발명하여 후속 람다 식에서 익명 형식을 나타냅니다.

특별한 종류의 생성기는 지정된 키에 따라 이전 절의 요소와 일치하는 다른 소스의 요소를 도입하는 join 절입니다. 인 절은 일치하는 요소를 하나씩 생성할 수 있지만 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() };

당연히 이 쿼리는 이전에 본 쿼리로 직접 확장됩니다.

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

한 쿼리의 결과를 후속 쿼리에서 생성기로 처리하는 것이 유용한 경우가 많습니다. 이를 지원하기 위해 쿼리 식은 를 into 키워드(keyword) 사용하여 select 또는 group 절 뒤에 새 쿼리 식을 스플라이스합니다. 이를 쿼리 연속이라고 합니다.

into 키워드(keyword) 그룹 by 절의 결과를 사후 처리하는 데 특히 유용합니다. 예를 들어 다음 프로그램을 고려합니다.

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> 인터페이스를 보강합니다. 개발자는 필요한 메서드를 직접 구현하거나 확장 메서드로 추가하여 쿼리 패턴을 준수하는지 확인하는 한 원하는 형식에서 쿼리 구문을 활용할 수 있습니다.

이 확장성은 SQL 기반 데이터 액세스를 위한 LINQ 패턴을 구현하는 두 개의 LINQ 지원 API, 즉 LINQ to SQL 프로비전하고 XML 데이터에 대한 LINQ 쿼리를 허용하는 LINQ to XML LINQ 프로젝트 자체에서 악용됩니다. 이 두 가지 모두 다음 섹션에 설명되어 있습니다.

LINQ to SQL: SQL 통합

.NET Language-Integrated 쿼리는 로컬 프로그래밍 언어의 구문 또는 컴파일 시간 환경을 벗어나지 않고 관계형 데이터 저장소를 쿼리하는 데 사용할 수 있습니다. 코드 이름이 LINQ to SQL 이 기능은 SQL 스키마 정보를 CLR 메타데이터에 통합하는 기능을 활용합니다. 이 통합은 SQL 테이블 및 뷰 정의를 모든 언어에서 액세스할 수 있는 CLR 형식으로 컴파일합니다.

LINQ to SQL 외부 SQL 데이터에 해당하는 CLR 형식 및 속성을 나타내는 두 가지 핵심 특성인 [Table][Column]을 정의합니다. [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의 nullable 형식(.NET Framework 버전 2.0에 처음 나타난 nullable 형식)에 매핑되고, CLR 형식(예: nvarchar, char, text)과 1:1 대응이 없는 SQL 형식의 경우 원래 SQL 형식이 CLR 메타데이터에 유지되는 예제입니다.

관계형 저장소에 대해 쿼리를 실행하기 위해 LINQ 패턴의 LINQ to SQL 구현은 쿼리를 식 트리 형식에서 SQL 식으로 변환하고 원격 평가에 적합한 DbCommand 개체를 ADO.NET. 예를 들어 다음 간단한 쿼리를 고려합니다.

// establish a query context over ADO.NET sql connection
DataContext context = new DataContext(
     "Initial Catalog=petdb;Integrated Security=sspi");

// grab variables that represent the remote tables that 
// correspond to the Person and Order CLR types
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders   = context.GetTable<Order>();

// build the query
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
            }; 

// execute the query
foreach (var item in query) 
    Console.WriteLine("{0} {1} {2} {3}", 
                      item.Name, item.OrderID, 
                      item.Amount, item.Age);

DataContext 형식은 표준 쿼리 연산자를 SQL로 변환하는 간단한 번역기를 제공합니다. DataContext 는 저장소에 액세스하기 위해 기존 ADO.NET IDbConnection 을 사용하며 설정된 ADO.NET 연결 개체 또는 연결 문자열을 사용하여 초기화할 수 있습니다.

GetTable 메서드는 쿼리 식에서 원격 테이블 또는 뷰를 나타내는 데 사용할 수 있는 IEnumerable 호환 변수를 제공합니다. GetTable에 대한 호출은 데이터베이스와의 상호 작용을 유발하지 않습니다. 대신 쿼리 식을 사용하여 원격 테이블 또는 뷰와 상호 작용할 수 있는 가능성을 나타냅니다. 위의 예제에서는 프로그램이 쿼리 식을 반복할 때까지 쿼리가 저장소로 전송되지 않습니다. 이 경우 C#의 foreach 문을 사용합니다. 프로그램이 쿼리를 처음 반복하면 DataContext 기계는 식 트리를 저장소로 전송되는 다음 SQL 문으로 변환합니다.

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 개발자가 개체 간의 관계를 정의하고 탐색할 수 있는 개체 관계형 매핑 기능을 제공합니다. 매핑을 사용하여 Customer 클래스의 속성으로 Orders를 참조할 수 있으므로 둘을 연결하기 위해 명시적 조인이 필요하지 않습니다. 외부 매핑 파일을 사용하면 매핑을 개체 모델과 구분하여 보다 풍부한 매핑 기능을 사용할 수 있습니다.

LINQ to XML: XML 통합

.NET Language-Integrated LINQ to XML(XML용 쿼리)를 사용하면 표준 쿼리 연산자뿐만 아니라 하위 항목, 상위 항목 및 형제를 통해 XPath와 유사한 탐색을 제공하는 트리별 연산자를 사용하여 XML 데이터를 쿼리할 수 있습니다. 기존 System.Xml 판독기/기록기 인프라와 통합되고 W3C DOM보다 사용하기 쉬운 XML에 대한 효율적인 메모리 내 표현을 제공합니다. XML을 쿼리와 통합하는 대부분의 작업에는 XName, XElementXAttribute의 세 가지 유형이 있습니다.

XName 은 요소 및 특성 이름 모두로 사용되는 네임스페이스 정규화된 식별자(QNames)를 처리하는 사용하기 쉬운 방법을 제공합니다. XName 은 식별자의 효율적인 원자화를 투명하게 처리하고 QName이 필요한 곳마다 기호 또는 일반 문자열을 사용할 수 있도록 합니다.

XML 요소와 특성은 각각 XElementXAttribute 를 사용하여 표시됩니다. XElementXAttribute 는 일반 생성 구문을 지원하므로 개발자는 자연 구문을 사용하여 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을 생성했습니다. XML 요소는 기존 XmlReader 또는 문자열 리터럴에서 생성할 수도 있습니다.

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>

위의 문에는 Visual Basic으로 직접 변환됩니다. 그러나 Visual Basic 9.0은 VISUAL Basic에서 직접 선언적 XML 구문을 사용하여 쿼리 식을 표현할 수 있는 XML 리터럴의 사용도 지원합니다. 이전 예제는 Visual Basic 문을 사용하여 생성할 수 있습니다.

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

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

지금까지의 예제에서는 언어 통합 쿼리를 사용하여 새 XML 값을 생성하는 방법을 보여 줍니다. 또한 XElementXAttribute 형식은 XML 구조에서 정보 추출을 간소화합니다. XElement 는 쿼리 식을 기존 XPath 축에 적용할 수 있는 접근자 메서드를 제공합니다. 예를 들어 다음 쿼리는 위에 표시된 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") 
    };

XAttributeXElement는 모두 텍스트 값을 기본 형식으로 추출하는 명시적 변환을 지원합니다. 누락된 데이터를 처리하기 위해 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은 XElementElements, AttributeDescendants 접근자 메서드에 대한 직접 언어 지원을 제공하므로 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>이름이 Personx의 Descendants 컬렉션에 있는 모든 항목을 가져오고 식은@AgeAge. The Value 속성이라는 이름의 모든 XAttributes를 찾은 다음 컬렉션의 첫 번째 특성을 가져오고 해당 특성의 Value 속성을 호출합니다.

요약

.NET Language-Integrated 쿼리는 CLR 및 이를 대상으로 하는 언어에 쿼리 기능을 추가합니다. 쿼리 기능은 조건자, 프로젝션 및 키 추출 식을 불투명 실행 코드 또는 다운스트림 처리 또는 변환에 적합한 투명한 메모리 내 데이터로 사용할 수 있도록 람다 식 및 식 트리를 기반으로 합니다. LINQ 프로젝트에서 정의한 표준 쿼리 연산자는 모든 IEnumerable<T> 기반 정보 원본에서 작동하며 관계형 및 XML 데이터가 언어 통합 쿼리의 이점을 얻을 수 있도록 ADO.NET(LINQ to SQL) 및 System.Xml (LINQ to XML)와 통합됩니다.

간단히 말해서 표준 쿼리 연산자

연산자 Description
Where 조건자 함수를 기반으로 하는 제한 연산자
Select/SelectMany 선택기 함수를 기반으로 하는 프로젝션 연산자
Take/Skip/ TakeWhile/SkipWhile 위치 또는 조건자 함수를 기반으로 하는 분할 연산자
조인/그룹조인 키 선택기 함수를 기반으로 하는 조인 연산자
Concat Concatenation 연산자
OrderBy/ThenBy/OrderByDescending/ThenByDescending 선택적 키 선택기 및 비교기 함수에 따라 오름차순 또는 내림차순으로 정렬하는 정렬 연산자
Reverse 시퀀스의 순서를 역방향으로 정렬하는 연산자
GroupBy 선택적 키 선택기 및 비교기 함수를 기반으로 하는 그룹화 연산자
Distinct 중복 제거 연산자 설정
공용 구조체/교차 집합 공용 구조체 또는 교집합을 반환하는 설정 연산자
Except 집합 차이를 반환하는 set 연산자
AsEnumerable IEnumerable<T>로 변환 연산자
ToArray/ToList 배열 또는 List<T>로 변환 연산자
ToDictionary/ToLookup 키 선택기 함수를 기반으로 하는 사전<K, T> 또는 조회<K,T> (다중 사전)로 변환 연산자
OfType/Cast 형식 인수로의 필터링 또는 변환에 따라 연산 자를 IEnumerable<T> 로 변환
SequenceEqual 쌍 요소 같음을 확인하는 같음 연산자
First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault 선택적 조건자 함수를 기반으로 초기/최종/전용 요소를 반환하는 요소 연산자
ElementAt/ElementAtOrDefault 위치에 따라 요소를 반환하는 요소 연산자
DefaultIfEmpty 빈 시퀀스를 기본값 싱글톤 시퀀스로 바꾸는 요소 연산자
범위 범위에서 숫자를 반환하는 생성 연산자
Repeat 지정된 값의 여러 발생을 반환하는 생성 연산자
Empty 빈 시퀀스를 반환하는 생성 연산자
일부/모두 조건자 기능의 실존적 또는 보편적 만족도에 대한 수량자 검사
포함 지정된 요소의 존재 여부를 확인하는 수량자
Count/LongCount 선택적 조건자 함수를 기반으로 요소 계산 집계 연산자
합계/최소/최대/평균 선택적 선택기 함수를 기반으로 하는 집계 연산자
집계 누적 함수 및 선택적 시드를 기반으로 여러 값을 누적하는 집계 연산자