Udostępnij za pośrednictwem


LINQ: zapytanie Language-Integrated platformy .NET

 

Don Box, Anders Hejlsberg

Luty 2007 r.

Dotyczy:
   nazwa Visual Studio Code "Orcas"
   .Net Framework 3.5

Krótki opis: Obiekty zapytań ogólnego przeznaczenia dodane do .NET Framework mają zastosowanie do wszystkich źródeł informacji, a nie tylko danych relacyjnych lub XML. Ta funkcja nosi nazwę .NET Language-Integrated Query (LINQ). (32 drukowane strony)

Zawartość

Zapytanie platformy .NET Language-Integrated
Wprowadzenie ze standardowymi operatorami zapytań
Funkcje językowe obsługujące projekt LINQ
Więcej standardowych operatorów zapytań
Składnia zapytania
LINQ to SQL: integracja z programem SQL
LINQ to XML: integracja XML
Podsumowanie

Zapytanie platformy .NET Language-Integrated

Po dwóch dekadach przemysł osiągnął stabilny punkt ewolucji technologii programowania zorientowanych na obiekty (OO). Programiści przyjmują teraz przyznane funkcje, takie jak klasy, obiekty i metody. Patrząc na obecną i następną generację technologii, okazało się, że kolejnym dużym wyzwaniem w technologii programowania jest zmniejszenie złożoności uzyskiwania dostępu do informacji, które nie są natywnie zdefiniowane przy użyciu technologii OO. Dwa najbardziej typowe źródła informacji innych niż OO to relacyjne bazy danych i XML.

Zamiast dodawać funkcje specyficzne dla relacyjnych lub XML do naszych języków programowania i środowiska uruchomieniowego, w projekcie LINQ podjęliśmy bardziej ogólne podejście i dodajemy do .NET Framework obiekty zapytań ogólnego przeznaczenia, które mają zastosowanie do wszystkich źródeł informacji, a nie tylko danych relacyjnych lub XML. Ta funkcja nosi nazwę .NET Language-Integrated Query (LINQ).

Używamy terminu zapytanie zintegrowane z językiem , aby wskazać, że zapytanie jest zintegrowaną funkcją podstawowych języków programowania dewelopera (na przykład Visual C#, Visual Basic). Zapytanie zintegrowane z językiem umożliwia wyrażeniem zapytań korzystanie z zaawansowanych metadanych, sprawdzania składni w czasie kompilacji, statycznego pisania i funkcji IntelliSense, które były wcześniej dostępne tylko dla kodu imperatywnego. Zapytanie zintegrowane z językiem umożliwia również zastosowanie pojedynczej funkcji zapytań deklaratywnych ogólnego przeznaczenia do wszystkich informacji w pamięci, a nie tylko informacji ze źródeł zewnętrznych.

Zapytanie platformy .NET Language-Integrated definiuje zestaw standardowych operatorów zapytań ogólnego przeznaczenia, które umożliwiają wyrażanie operacji przechodzenia, filtrowania i projekcji w sposób bezpośredni, ale deklaratywny w dowolnym obiekcie . Język programowania oparty na platformie NET. Standardowe operatory zapytań umożliwiają stosowanie zapytań do dowolnego źródła informacji opartych na protokole> IEnumerable<. LINQ umożliwia innym firmom rozszerzanie zestawu standardowych operatorów zapytań przy użyciu nowych operatorów specyficznych dla domeny, które są odpowiednie dla domeny docelowej lub technologii. Co ważniejsze, inne firmy mogą również zastąpić standardowe operatory zapytań własnymi implementacjami, które zapewniają dodatkowe usługi, takie jak ocena zdalna, tłumaczenie zapytań, optymalizacja itd. Stosując się do konwencji wzorca LINQ, takie implementacje korzystają z tej samej integracji języka i obsługi narzędzi co standardowe operatory zapytań.

Rozszerzalność architektury zapytań jest używana w samym projekcie LINQ w celu zapewnienia implementacji, które działają zarówno nad danymi XML, jak i SQL. Operatory zapytań za pośrednictwem kodu XML (LINQ to XML) używają wydajnej, łatwej w użyciu funkcji XML w pamięci w celu zapewnienia funkcji XPath/XQuery w języku programowania hosta. Operatory zapytań nad danymi relacyjnymi (LINQ to SQL) bazują na integracji definicji schematów opartych na języku SQL w systemie typów środowiska uruchomieniowego języka wspólnego (CLR). Ta integracja zapewnia silne wpisywanie danych relacyjnych przy zachowaniu wyrazistej mocy modelu relacyjnego i wydajności oceny zapytań bezpośrednio w magazynie bazowym.

Wprowadzenie ze standardowymi operatorami zapytań

Aby zobaczyć zapytanie zintegrowane z językiem w pracy, zaczniemy od prostego programu języka C# 3.0, który używa standardowych operatorów zapytań do przetwarzania zawartości tablicy:

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

Jeśli chcesz skompilować i uruchomić ten program, zobaczysz następujące dane wyjściowe:

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

Zapytanie zmiennej lokalnej jest inicjowane za pomocą wyrażenia zapytania. Wyrażenie zapytania działa na jednym lub kilku źródłach informacji przez zastosowanie co najmniej jednego operatora zapytania z standardowych operatorów zapytań lub operatorów specyficznych dla domeny. To wyrażenie używa trzech standardowych operatorów zapytań: Where, OrderBy i Select.

Język Visual Basic 9.0 obsługuje również linQ. Oto poprzednia instrukcja napisana w Visual Basic 9.0:

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

Instrukcje języka C# i Visual Basic pokazane tutaj używają wyrażeń zapytań. Podobnie jak instrukcja foreach , wyrażenia zapytań są wygodnym deklaratywnym skróconym opisem kodu, który można napisać ręcznie. Powyższe instrukcje są semantycznie identyczne z następującą jawną składnią pokazaną w języku C#:

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

Ta forma zapytania jest nazywana zapytaniem opartym na metodzie . Argumenty operatorów Where, OrderBy i Select są nazywane wyrażeniami lambda, które są fragmentami kodu podobnie jak delegaty. Umożliwiają one definiowanie pojedynczych standardowych operatorów zapytań jako metod i łączenie ich przy użyciu notacji kropkowej. Razem te metody stanowią podstawę rozszerzalnego języka zapytań.

Funkcje językowe obsługujące projekt LINQ

LINQ jest oparty wyłącznie na funkcjach języka ogólnego przeznaczenia, z których niektóre są nowe w językach C# 3.0 i Visual Basic 9.0. Każda z tych funkcji ma własne narzędzie, ale zbiorczo te funkcje zapewniają rozszerzalny sposób definiowania zapytań i interfejsów API z możliwością wykonywania zapytań. W tej sekcji zapoznamy się z tymi funkcjami językowymi i tym, jak przyczyniają się one do znacznie bardziej bezpośredniego i deklaratywnego stylu zapytań.

Wyrażenia lambda i drzewa wyrażeń

Wiele operatorów zapytań umożliwia użytkownikowi udostępnienie funkcji wykonującej filtrowanie, projekcję lub wyodrębnianie kluczy. Obiekty zapytań opierają się na koncepcji wyrażeń lambda, które zapewniają deweloperom wygodny sposób pisania funkcji, które mogą być przekazywane jako argumenty do kolejnej oceny. Wyrażenia lambda są podobne do delegatów CLR i muszą być zgodne z sygnaturą metody zdefiniowaną przez typ delegata. Aby to zilustrować, możemy rozwinąć instrukcję powyżej do równoważnej, ale bardziej jawnej postaci przy użyciu typu delegata 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);

Wyrażenia lambda to naturalna ewolucja metod anonimowych w języku C# 2.0. Na przykład mogliśmy napisać poprzedni przykład przy użyciu metod anonimowych, takich jak:

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

Ogólnie rzecz biorąc, deweloper może używać metod nazwanych, metod anonimowych lub wyrażeń lambda z operatorami zapytań. Wyrażenia lambda mają zaletę zapewniania najbardziej bezpośredniej i kompaktowej składni do tworzenia. Co ważniejsze, wyrażenia lambda można kompilować jako kod lub dane, co umożliwia przetwarzanie wyrażeń lambda w czasie wykonywania przez optymalizatorów, tłumaczy i ewaluatorów.

Przestrzeń nazw System.Linq.Expressions definiuje typ ogólny wyróżniający, Expression<T>, który wskazuje, że drzewo wyrażeń jest pożądane dla danego wyrażenia lambda, a nie dla tradycyjnej treści metody opartej na języku IL. Drzewa wyrażeń są wydajnymi reprezentacjami danych w pamięci w wyrażeniach lambda i sprawiają, że struktura wyrażenia jest przezroczysta i jawna.

Określenie, czy kompilator będzie emitować wykonywalny il, czy drzewo wyrażeń jest określane przez sposób użycia wyrażenia lambda. Gdy wyrażenie lambda jest przypisane do zmiennej, pola lub parametru, którego typem jest delegat, kompilator emituje il, który jest identyczny z typem metody anonimowej. Gdy wyrażenie lambda jest przypisane do zmiennej, pola lub parametru, którego typem jest wyrażenie<T dla typu delegata T>, kompilator emituje drzewo wyrażeń.

Rozważmy na przykład następujące dwie deklaracje zmiennych:

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

Zmienna f jest odwołaniem do delegata, który jest bezpośrednio wykonywalny:

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

Zmienna e jest odwołaniem do drzewa wyrażeń, które nie jest bezpośrednio wykonywalne:

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

W przeciwieństwie do delegatów, które są skutecznie nieprzezroczystych kodu, możemy wchodzić w interakcje z drzewem wyrażeń tak samo jak każda inna struktura danych w naszym programie.

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

Powyższy przykład rozkłada drzewo wyrażeń w czasie wykonywania i wyświetla następujący ciąg:

n LessThan 5

Możliwość traktowania wyrażeń jako danych w czasie wykonywania ma kluczowe znaczenie dla umożliwienia ekosystemu bibliotek innych firm korzystających z abstrakcji zapytań podstawowych, które są częścią platformy. Implementacja dostępu do danych LINQ to SQL wykorzystuje tę funkcję do tłumaczenia drzew wyrażeń na instrukcje języka T-SQL odpowiednie do oceny w magazynie.

Metody rozszerzania

Wyrażenia lambda to jeden ważny element architektury zapytań. Metody rozszerzenia są inne. Metody rozszerzeń łączą elastyczność "wpisywania kaczki" popularne w językach dynamicznych z wydajnością i walidacją w czasie kompilacji statycznie typizowane języki. W przypadku metod rozszerzeń osoby trzecie mogą rozszerzyć kontrakt publiczny typu o nowe metody, jednocześnie pozwalając autorom poszczególnych typów na zapewnienie własnej wyspecjalizowanej implementacji tych metod.

Metody rozszerzenia są definiowane w klasach statycznych jako metody statyczne, ale są oznaczone atrybutem [System.Runtime.CompilerServices.Extension] w metadanych CLR. Zachęcamy języki do zapewnienia bezpośredniej składni dla metod rozszerzeń. W języku C# metody rozszerzenia są wskazywane przez ten modyfikator, który należy zastosować do pierwszego parametru metody rozszerzenia. Przyjrzyjmy się definicji najprostszego operatora zapytania , Gdzie:

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

Typ pierwszego parametru metody rozszerzenia wskazuje typ rozszerzenia, do którego ma zastosowanie. W powyższym przykładzie metoda where rozszerzenia rozszerza typ IEnumerable<T>. Ponieważ gdzie jest metodą statyczną, możemy wywołać ją bezpośrednio tak samo jak każda inna metoda statyczna:

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

Jednak to, co sprawia, że metody rozszerzeń są unikatowe, jest to, że można je również wywołać przy użyciu składni wystąpienia:

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

Metody rozszerzeń są rozwiązywane w czasie kompilacji na podstawie metod rozszerzenia w zakresie. Po zaimportowaniu przestrzeni nazw za pomocą instrukcji using w języku C# lub instrukcji Import w języku Visual Basic wszystkie metody rozszerzeń zdefiniowane przez klasy statyczne z tej przestrzeni nazw są wprowadzane do zakresu.

Standardowe operatory zapytań są definiowane jako metody rozszerzenia w typie System.Linq.Enumerable. Podczas badania standardowych operatorów zapytań zauważysz, że wszystkie z nich, ale kilka z nich jest zdefiniowanych pod względem interfejsu IEnumerable<T> . Oznacza to, że każde źródło informacji zgodne z językiem IEnumerable<> pobiera standardowe operatory zapytań, dodając następującą instrukcję using w języku C#:

using System.Linq; // makes query operators visible

Użytkownicy, którzy chcą zastąpić standardowe operatory zapytań dla określonego typu, mogą: zdefiniować własne metody o tej samej nazwie dla określonego typu z zgodnymi podpisami lub zdefiniować nowe metody rozszerzenia o tej samej nazwie, które rozszerzają określony typ. Użytkownicy, którzy chcą całkowicie usunąć standardowe operatory zapytań, nie mogą po prostu umieścić system.Linq w zakresie i napisać własne metody rozszerzenia dla IEnumerable<T>.

Metody rozszerzenia mają najniższy priorytet w zakresie rozpoznawania i są używane tylko wtedy, gdy nie ma odpowiedniego dopasowania dla typu docelowego i jego typów podstawowych. Dzięki temu typy zdefiniowane przez użytkownika mogą udostępniać własne operatory zapytań, które mają pierwszeństwo przed standardowymi operatorami. Rozważmy na przykład następującą kolekcję niestandardową:

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

Biorąc pod uwagę tę definicję klasy, następujący program użyje metody MySequence.Where implementation, a nie metody rozszerzenia, ponieważ metody wystąpienia mają pierwszeństwo przed metodami rozszerzenia:

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

Operator OfType jest jednym z niewielu standardowych operatorów zapytań, które nie rozszerzają źródła informacji opartych na protokole> IEnumerable<. Przyjrzyjmy się operatorowi zapytania OfType :

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

Funkcja OfType akceptuje nie tylko źródła oparte na języku IEnumerable<>, ale także źródła, które są zapisywane względem niesparametryzowanego interfejsu IEnumerable, który był obecny w wersji 1.0 .NET Framework. Operator OfType umożliwia użytkownikom stosowanie standardowych operatorów zapytań do klasycznych kolekcji platformy .NET w następujący sposób:

// "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>();

W tym przykładzie zmienna modern zwraca taką samą sekwencję wartości, jak w przypadku modelu klasycznego. Jednak jego typ jest zgodny z nowoczesnym kodem IEnumerable<T> , w tym standardowymi operatorami zapytań.

Operator OfType jest również przydatny w przypadku nowszych źródeł informacji, ponieważ umożliwia filtrowanie wartości ze źródła na podstawie typu. Podczas tworzenia nowej sekwencji funkcja OfType po prostu pomija elementy członkowskie oryginalnej sekwencji, które nie są zgodne z argumentem typu. Rozważmy ten prosty program, który wyodrębnia ciągi z tablicy heterogenicznej:

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

Podczas wyliczania zmiennej justStrings w instrukcji foreach uzyskamy sekwencję dwóch ciągów: "Hello" i "World".

Ocena zapytania odroczonego

Obserwatorzy mogli zauważyć, że standardowy operator Where jest implementowany przy użyciu konstrukcji wydajności wprowadzonej w języku C# 2.0. Ta technika implementacji jest powszechna dla wszystkich standardowych operatorów, które zwracają sekwencje wartości. Użycie funkcji yield ma interesującą korzyść, która polega na tym, że zapytanie nie jest rzeczywiście oceniane, dopóki nie zostanie przeserowane, albo z instrukcją foreach lub ręcznie przy użyciu podstawowych metod GetEnumerator i MoveNext . Ta odroczona ocena umożliwia zachowywanie zapytań jako wartości opartych na języku IEnumerable<>, które można wielokrotnie oceniać, za każdym razem generując potencjalnie różne wyniki.

W przypadku wielu aplikacji jest to dokładnie zachowanie, które jest wymagane. W przypadku aplikacji, które chcą buforować wyniki oceny zapytania, podano dwa operatory ToList i ToArray, które wymuszają natychmiastową ocenę zapytania i zwracają <listę T> lub tablicę zawierającą wyniki oceny zapytania.

Aby zobaczyć, jak działa ocena zapytania odroczonego, należy wziąć pod uwagę ten program, który uruchamia proste zapytanie względem tablicy:

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

Zapytanie jest oceniane za każdym razem, gdy zmienna ayes jest iterowana. Aby wskazać, że jest potrzebna buforowana kopia wyników, możemy po prostu dołączyć operator ToList lub ToArray do zapytania w następujący sposób:

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

Zarówno ToArray, jak i ToList wymuszają natychmiastową ocenę zapytania. To samo dotyczy standardowych operatorów zapytań, które zwracają pojedyncze wartości (na przykład: First, ElementAt, Sum, Average, All, Any).

Interfejs IQueryable<T>

Ten sam model odroczonego wykonywania jest zwykle wymagany w przypadku źródeł danych, które implementują funkcje zapytania przy użyciu drzew wyrażeń, takich jak LINQ to SQL. Te źródła danych mogą korzystać z implementacji interfejsu IQueryable<T> , dla którego wszystkie operatory zapytań wymagane przez wzorzec LINQ są implementowane przy użyciu drzew wyrażeń. Każdy element IQueryable<T> ma reprezentację "kodu potrzebnego do uruchomienia zapytania" w postaci drzewa wyrażeń. Wszystkie operatory zapytań odroczonych zwracają nowy język IQueryable<T> , który rozszerza to drzewo wyrażeń z reprezentacją wywołania tego operatora zapytania. W związku z tym, gdy staje się czas na ocenę zapytania, zazwyczaj dlatego, że zapytanie IQueryable<T> jest wyliczane, źródło danych może przetworzyć drzewo wyrażeń reprezentujące całe zapytanie w jednej partii. Na przykład skomplikowane zapytanie LINQ to SQL uzyskane przez wiele wywołań operatorów zapytań może spowodować wysłanie tylko jednego zapytania SQL do bazy danych.

Korzyść dla implementatorów źródeł danych ponownego używania tej funkcji odroczenia przez zaimplementowanie interfejsu IQueryable<T> jest oczywista. Z drugiej strony dla klientów, którzy piszą zapytania, jest to świetna zaleta wspólnego typu dla zdalnych źródeł informacji. Nie tylko umożliwia im pisanie zapytań polimorficznych, które mogą być używane względem różnych źródeł danych, ale także otwiera możliwość pisania zapytań, które przechodzą między domenami.

Inicjowanie wartości złożonych

Wyrażenia lambda i metody rozszerzenia zapewniają nam wszystko, czego potrzebujemy w przypadku zapytań, które po prostu filtrować elementy członkowskie z sekwencji wartości. Większość wyrażeń zapytań wykonuje również projekcję na tych elementach członkowskich, efektywnie przekształcając elementy członkowskie oryginalnej sekwencji na elementy członkowskie, których wartość i typ mogą różnić się od oryginalnego. Aby obsługiwać pisanie tych przekształceń, LINQ opiera się na nowej konstrukcji nazywanej inicjatorami obiektów w celu utworzenia nowych wystąpień typów strukturalnych. W pozostałej części tego dokumentu przyjęto założenie, że zdefiniowano następujący typ:

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

Inicjatory obiektów umożliwiają łatwe konstruowanie wartości na podstawie pól publicznych i właściwości typu. Aby na przykład utworzyć nową wartość typu Person, możemy napisać następującą instrukcję:

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

Semantycznie ta instrukcja jest równoważna następującej sekwencji instrukcji:

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

Inicjatory obiektów są ważną funkcją zapytania zintegrowanego z językiem, ponieważ umożliwiają konstruowanie nowych wartości strukturalnych w kontekstach, w których dozwolone są tylko wyrażenia (na przykład w wyrażeniach lambda i drzewach wyrażeń). Rozważmy na przykład to wyrażenie zapytania, które tworzy nową wartość Person dla każdej wartości w sekwencji danych wejściowych:

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

Składnia inicjowania obiektów jest również wygodna do inicjowania tablic wartości strukturalnych. Rozważmy na przykład tę zmienną tablicową zainicjowaną przy użyciu inicjatorów poszczególnych obiektów:

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

Wartości strukturalne i typy

Projekt LINQ obsługuje styl programowania skoncentrowany na danych, w którym niektóre typy istnieją przede wszystkim w celu zapewnienia statycznego "kształtu" nad wartością ustrukturyzowaną, a nie pełnowymiarowego obiektu o stanie i zachowaniu. Biorąc to założenie do logicznego wniosku, często zdarza się, że wszyscy deweloper dbają o strukturę wartości, a potrzeba nazwanego typu dla tego kształtu jest mało używana. Prowadzi to do wprowadzenia typów anonimowych , które umożliwiają definiowanie nowych struktur "wbudowanych" przy użyciu ich inicjowania.

W języku C# składnia typów anonimowych jest podobna do składni inicjowania obiektu, z tą różnicą, że nazwa typu zostanie pominięta. Rozważmy na przykład następujące dwie instrukcje:

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

Zmienne w wersji 1 i 2 wskazują obiekt w pamięci, którego typ CLR ma trzy właściwości publiczne Name, Age i CanCode. Zmienne różnią się tym, że wersja 2 odnosi się do wystąpienia typu anonimowego. W terminach CLR typy anonimowe nie różnią się od innych typów. To, co sprawia, że typy anonimowe są specjalne, jest to, że nie mają znaczącej nazwy w języku programowania. Jedynym sposobem tworzenia wystąpień typu anonimowego jest użycie składni pokazanej powyżej.

Aby umożliwić zmienne odwoływania się do wystąpień typów anonimowych, ale nadal korzystają ze statycznego pisania, język C# wprowadza niejawnie wpisane zmienne lokalne: słowo kluczowe var może być używane zamiast nazwy typu dla lokalnych deklaracji zmiennych. Rozważmy na przykład ten legalny program C# 3.0:

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

Słowo kluczowe var nakazuje kompilatorowi wnioskowanie typu zmiennej ze statycznego typu wyrażenia użytego do zainicjowania zmiennej. W tym przykładzie typy s, n i b są odpowiednio ciągami, int i bool. Ten program jest identyczny z następującymi elementami:

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

Słowo kluczowe var jest wygodą dla zmiennych, których typy mają znaczące nazwy, ale jest to konieczność dla zmiennych odwołujących się do wystąpień typów anonimowych.

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

W powyższym przykładzie wartość zmiennej jest typu anonimowego, którego definicja jest równoważna następującemu pseudo-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() { ... }
}

Typy anonimowe nie mogą być współużytkowane przez granice zestawów; Jednak kompilator zapewnia, że istnieje co najwyżej jeden anonimowy typ dla danej sekwencji par nazw/typów właściwości w każdym zestawie.

Ponieważ typy anonimowe są często używane w projekcjach do wybierania co najmniej jednego elementu członkowskiego istniejącej wartości ustrukturyzowanej, możemy po prostu odwoływać się do pól lub właściwości z innej wartości w inicjowaniu typu anonimowego. Powoduje to pobranie nowej anonimowej właściwości, której nazwa, typ i wartość są kopiowane z przywoływanej właściwości lub pola.

Rozważmy na przykład ten przykład, który tworzy nową wartość ustrukturyzowaną, łącząc właściwości z innych wartości:

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"

Odwoływanie się do pól lub właściwości pokazanych powyżej jest po prostu wygodną składnią do pisania następującego bardziej jawnego formularza:

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

W obu przypadkach zmienna pary pobiera własną kopię właściwości Name i Age z bob i jane.

Typy anonimowe są najczęściej używane w klauzuli select zapytania. Rozważmy na przykład następujące zapytanie:

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

W tym przykładzie udało nam się utworzyć nową projekcję dla typu Osoba , która dokładnie pasuje do kształtu potrzebnego do naszego kodu przetwarzania, ale nadal daje nam korzyści z typu statycznego.

Więcej standardowych operatorów zapytań

W oparciu o podstawowe obiekty zapytań opisane powyżej, wiele operatorów zapewnia przydatne sposoby manipulowania sekwencjami i komponowania zapytań, dając użytkownikowi wysoki stopień kontroli nad wynikiem w wygodnej strukturze standardowych operatorów zapytań.

Sortowanie i grupowanie

Ogólnie rzecz biorąc, ocena zapytania powoduje sekwencję wartości, które są generowane w określonej kolejności, która jest wewnętrzna w źródłowych źródłach informacji. Aby zapewnić deweloperom jawną kontrolę nad kolejnością tworzenia tych wartości, standardowe operatory zapytań są definiowane do kontrolowania kolejności. Najbardziej podstawowym z tych operatorów jest operator OrderBy .

Operatory OrderBy i OrderByDescending można zastosować do dowolnego źródła informacji i umożliwić użytkownikowi udostępnienie funkcji wyodrębniania kluczy, która generuje wartość używaną do sortowania wyników. Klasy OrderBy i OrderByDescending akceptują również opcjonalną funkcję porównania, która może służyć do narzucenia częściowego porządku nad kluczami. Przyjrzyjmy się podstawowego przykładowi:

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

Pierwsze dwa wyrażenia zapytania tworzą nowe sekwencje, które są oparte na sortowaniu elementów członkowskich źródła na podstawie porównania ciągów. Drugie dwa zapytania tworzą nowe sekwencje, które są oparte na sortowaniu elementów członkowskich źródła na podstawie długości każdego ciągu.

Aby zezwolić na wiele kryteriów sortowania, zarówno OrderBy, jak i OrderByDescending zwracają wartość OrderSequence<T zamiast ogólnego IEnumerable><T>. Dwa operatory są definiowane tylko w obszarze OrderedSequence<T>, a mianowicie ThenBy i ThenByDescending , które stosują dodatkowe (podrzędne) kryterium sortowania. Następnie/ThenByDescending się zwracają OrderedSequence<T>, pozwalając na zastosowanie dowolnej liczby operatorów ThenBy/ThenByDescending :

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

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

Ocena zapytania, do których odwołuje się zapytanie s1 w tym przykładzie, spowoduje uzyskanie następującej sekwencji wartości:

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

Oprócz rodziny operatorów OrderBy standardowe operatory zapytania zawierają również operator reverse . Odwrotnie po prostu wylicza sekwencję i zwraca te same wartości w odwrotnej kolejności. W przeciwieństwie do kolumny OrderBy funkcja Reverse nie uwzględnia samych wartości rzeczywistych podczas określania kolejności, a raczej opiera się wyłącznie na kolejności, w którym wartości są generowane przez bazowe źródło.

Operator OrderBy nakłada kolejność sortowania na sekwencję wartości. Standardowe operatory zapytań obejmują również operator GroupBy , który nakłada partycjonowanie na sekwencję wartości na podstawie funkcji wyodrębniania klucza. Operator GroupBy zwraca sekwencję wartości IGrouping — jedną dla każdej napotkanej odrębnej wartości klucza. IGrouping to element IEnumerable, który dodatkowo zawiera klucz, który został użyty do wyodrębnienia jego zawartości:

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

Najprostsza aplikacja funkcji GroupBy wygląda następująco:

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

Po uruchomieniu ten program wyświetla następujące informacje:

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

A la Select, GroupBy umożliwia podanie funkcji projekcji, która jest używana do wypełniania członków grup.

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

Ta odmiana wyświetla następujące informacje:

Strings of length 6
  A
  C
  G
  H
Strings of length 5
  B
  D
  F
Strings of length 7
  E

Uwaga W tym przykładzie przewidywany typ nie musi być taki sam jak w przypadku źródła. W tym przypadku utworzyliśmy grupowanie liczb całkowitych na znaki z sekwencji ciągów.

Operatory agregacji

Kilka standardowych operatorów zapytań jest zdefiniowanych w celu agregowania sekwencji wartości w jedną wartość. Najbardziej ogólnym operatorem agregacji jest Agregacja, która jest zdefiniowana w następujący sposób:

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

Operator agregacji ułatwia wykonywanie obliczeń w sekwencji wartości. Agregacja działa przez wywołanie wyrażenia lambda raz dla każdego elementu członkowskiego bazowej sekwencji. Za każdym razem , gdy funkcja Aggregate wywołuje wyrażenie lambda, przekazuje zarówno element członkowski z sekwencji, jak i zagregowaną wartość (wartość początkowa jest parametrem inicjacji do funkcji Aggregate). Wynik wyrażenia lambda zastępuje poprzednią zagregowaną wartość, a funkcja Aggregate zwraca końcowy wynik wyrażenia lambda.

Na przykład ten program używa funkcji Aggregate do gromadzenia całkowitej liczby znaków w tablicy ciągów:

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

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

Oprócz operatora agregacji ogólnego przeznaczenia standardowe operatory zapytań obejmują również operator liczby ogólnego przeznaczenia i cztery operatory agregacji liczbowej (Min, Max, Sum i Average), które upraszczają te typowe operacje agregacji. Funkcje agregacji liczbowej działają nad sekwencjami typów liczbowych (na przykład int, double, decimal) lub sekwencjami dowolnych wartości, o ile funkcja jest dostarczana, aby projekty składowe sekwencji w typ liczbowy.

Ten program ilustruje obie formy właśnie opisanego operatora 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

Uwaga Druga instrukcja Sum jest równoważna poprzedniemu przykładowi przy użyciu funkcji Aggregate.

Wybierz pozycję vs. SelectMany

Operator Select wymaga, aby funkcja transform tworzyła jedną wartość dla każdej wartości w sekwencji źródłowej. Jeśli funkcja transform zwraca wartość, która sama jest sekwencją, do odbiorcy należy ręczne przechodzenie podsekwencji. Rozważmy na przykład ten program, który dzieli ciągi na tokeny przy użyciu istniejącej metody 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);

Po uruchomieniu ten program wyświetli następujący tekst:

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

W idealnym przypadku chcielibyśmy, aby nasze zapytanie zwróciło sekwencję tokenów, a nie uwidoczniło ciąg pośredni[] użytkownikowi. Aby to osiągnąć, użyjemy operatora SelectMany zamiast operatora Select . Operator SelectMany działa podobnie do operatora Select . Różni się w tym, że funkcja przekształcania ma zwrócić sekwencję, która jest następnie rozwinięta przez operator SelectMany . Oto nasz program przepisany przy użyciu polecenia 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);

Użycie funkcji SelectMany powoduje rozszerzenie każdej sekwencji pośredniej w ramach normalnej oceny.

Funkcja SelectMany jest idealna do łączenia dwóch źródeł informacji:

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

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

W wyrażeniu lambda przekazanym do funkcji SelectMany zapytanie zagnieżdżone ma zastosowanie do innego źródła, ale ma w zakresie n parametr przekazywany z zewnętrznego źródła. Tak więc ludzie. Gdzie jest wywoływany raz dla każdego n, z wynikowych sekwencji spłaszczone przez SelectMany dla końcowych danych wyjściowych. Wynik jest sekwencją wszystkich osób, których nazwa pojawia się w tablicy nazw .

Operatory łączenia

W programie zorientowanym obiektowo obiekty, które są ze sobą powiązane, zwykle są połączone z odwołaniami do obiektów, które są łatwe do nawigowania. To samo zwykle nie ma wartości true dla zewnętrznych źródeł informacji, gdzie wpisy danych często nie mają opcji, ale "wskazywać" na siebie symbolicznie, z identyfikatorami lub innymi danymi, które mogą jednoznacznie zidentyfikować wskazaną jednostkę. Koncepcja sprzężeń odnosi się do operacji łączenia elementów sekwencji wraz z elementami, z którymi "pasują" z innej sekwencji.

Poprzedni przykład z selectMany rzeczywiście robi to dokładnie, pasując ciągi do osób, których nazwy są tymi ciągami. Jednak w tym konkretnym celu podejście SelectMany nie jest bardzo wydajne — będzie przechodzić w pętli przez wszystkie elementy osób dla każdego i każdego elementu nazw. Łącząc wszystkie informacje z tego scenariusza — dwa źródła informacji i "klucze", do których są one dopasowane — razem w jednym wywołaniu metody operator sprzężenia może wykonać znacznie lepsze zadanie:

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

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

Jest to trochę ustne, ale zobacz, jak elementy pasują do siebie: metoda Join jest wywoływana w "zewnętrznym" źródle danych, nazwy. Pierwszym argumentem jest "wewnętrzne" źródło danych , osoby. Drugie i trzecie argumenty to wyrażenia lambda do wyodrębniania kluczy z elementów źródeł zewnętrznych i wewnętrznych, odpowiednio. Te klucze są używane przez metodę Join , aby dopasować elementy. W tym miejscu chcemy, aby nazwy odpowiadały właściwości Name osób. Końcowe wyrażenie lambda jest następnie odpowiedzialne za tworzenie elementów wynikowej sekwencji: jest wywoływana z każdą parą pasujących elementów n i p i służy do kształtowania wyniku. W tym przypadku decydujemy się odrzucić n i zwrócić wartość p. Wynik końcowy to lista elementów osoby, którychnazwa znajduje się na liście nazwisk.

Bardziej zaawansowanym kuzynem sprzężenia jest operator GroupJoin . GroupJoin różni się od sprzężenia w sposób, w jaki wyrażenie lambda kształtowania wyniku jest używane: Zamiast wywoływana z każdą parą elementów zewnętrznych i wewnętrznych, będzie wywoływana tylko raz dla każdego elementu zewnętrznego, z sekwencją wszystkich elementów wewnętrznych, które pasują do tego elementu zewnętrznego. Aby zrobić ten konkretny:

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

To wywołanie tworzy sekwencję nazw, które zostały rozpoczęte w połączeniu z liczbą osób, które mają tę nazwę. W związku z tym operator GroupJoin umożliwia oparcie wyników na całym "zestawie dopasowań" dla elementu zewnętrznego.

Składnia zapytania

Istniejąca instrukcja foreach w języku C# udostępnia składnię deklaratywną dla iteracji w metodach IEnumerable/IEnumerator programu .NET Frameworks. Instrukcja foreach jest ściśle opcjonalna, ale okazała się bardzo wygodnym i popularnym mechanizmem języka.

Bazując na tym precedensie, wyrażenia zapytań upraszczają zapytania o składnię deklaratywną dla najbardziej typowych operatorów zapytań: Where, Join, GroupJoin, Select, SelectMany, GroupBy, OrderBy,OrderByDescending, ThenByDescending i Cast.

Zacznijmy od przyjrzenia się prostej kwerendzie, z którą rozpoczęliśmy ten dokument:

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

Za pomocą wyrażenia zapytania możemy ponownie napisać tę dokładną instrukcję w następujący sposób:

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

Podobnie jak instrukcja foreach w języku C#, wyrażenia zapytań są bardziej kompaktowe i łatwiejsze do odczytania, ale są całkowicie opcjonalne. Każde wyrażenie, które można zapisać jako wyrażenie zapytania, ma odpowiednią składnię (choć bardziej szczegółową) przy użyciu notacji kropkowej.

Zacznijmy od przyjrzenia się podstawowej strukturze wyrażenia zapytania. Każde wyrażenie zapytania składniowego w języku C# zaczyna się od klauzuli from i kończy się klauzulą select lub group . Po początkowej klauzuli from można wykonać zero lub więcej z, let, where, join i orderby klauzuli. Każda klauzula from jest generatorem, który wprowadza zmienną zakresu w sekwencji; każda klauzula let nadaje nazwę wynikowi wyrażenia; i każda klauzula where jest filtrem, który wyklucza elementy z wyniku. Każda klauzula sprzężenia koreluje nowe źródło danych z wynikami powyższych klauzul. Klauzula orderby określa kolejność dla wyniku:

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

Rozważmy na przykład te dwa wyrażenia zapytania:

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;

Kompilator traktuje te wyrażenia zapytań tak, jakby zostały napisane przy użyciu następującej jawnej notacji kropkowej:

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

Wyrażenia zapytań przechodzą mechaniczne tłumaczenie na wywołania metod o określonych nazwach. Dokładna implementacja operatora zapytania, która jest wybierana, zależy zatem zarówno od typu zmiennych, których dotyczy zapytanie, jak i metod rozszerzeń, które znajdują się w zakresie.

Pokazane do tej pory wyrażenia zapytania używały tylko jednego generatora. Gdy jest używany więcej niż jeden generator, każdy kolejny generator jest oceniany w kontekście jego poprzednika. Rozważmy na przykład tę niewielką modyfikację zapytania:

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

Po uruchomieniu względem tej tablicy wejściowej:

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

Uzyskamy następujące wyniki:

Burke Burke
Frank Frank
David David

Powyższe wyrażenie zapytania rozszerza się do tego wyrażenia notacji kropkowej:

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

Uwaga Ta wersja funkcji SelectMany przyjmuje dodatkowe wyrażenie lambda, które jest używane do generowania wyniku na podstawie elementów z sekwencji zewnętrznych i wewnętrznych. W tym wyrażeniu lambda dwie zmienne zakresu są zbierane w typie anonimowym. Kompilator wymyśla nazwę zmiennej $1 , aby oznaczyć ten typ anonimowy w kolejnych wyrażeniach lambda.

Specjalny rodzaj generatora jest klauzulą sprzężenia , która wprowadzi elementy innego źródła, które pasują do elementów powyższych klauzul zgodnie z podanymi kluczami. Klauzula sprzężenia może zwracać pasujące elementy jeden po drugim, ale jeśli zostanie określona z klauzulą into , pasujące elementy zostaną podane jako grupa:

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

Nic dziwnego, że to zapytanie rozszerza się bezpośrednio do tego, co widzieliśmy wcześniej:

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

Często warto traktować wyniki jednego zapytania jako generator w kolejnym zapytaniu. Aby to obsłużyć, wyrażenia zapytań używają słowa kluczowego into , aby połączyć nowe wyrażenie zapytania po klauzuli select lub group. Jest to nazywane kontynuacją zapytania.

Słowo kluczowe into jest szczególnie przydatne w przypadku przetwarzania końcowego wyników klauzuli group by . Rozważmy na przykład ten program:

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

Ten program generuje następujące dane wyjściowe:

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

W tej sekcji opisano sposób implementowania wyrażeń zapytań w języku C#. Inne języki mogą zdecydować się na obsługę dodatkowych operatorów zapytań z jawną składnią lub nie mają w ogóle wyrażeń zapytań.

Należy pamiętać, że składnia zapytania nie jest w żaden sposób podłączona do standardowych operatorów zapytań. Jest to funkcja czysto składniowa, która ma zastosowanie do dowolnego elementu spełniającego wzorzec zapytania przez zaimplementowanie metod bazowych z odpowiednimi nazwami i podpisami. Opisane powyżej standardowe operatory zapytań używają metod rozszerzenia w celu rozszerzenia interfejsu IEnumerable<T> . Deweloperzy mogą wykorzystać składnię zapytań dla dowolnego typu, o ile upewniają się, że jest ona zgodna ze wzorcem zapytania, przez bezpośrednią implementację niezbędnych metod lub przez dodanie ich jako metod rozszerzeń.

Ta rozszerzalność jest wykorzystywana w samym projekcie LINQ przez aprowizację dwóch interfejsów API z obsługą linQ, a mianowicie LINQ to SQL, która implementuje wzorzec LINQ na potrzeby dostępu do danych opartych na języku SQL i LINQ to XML, co umożliwia wykonywanie zapytań LINQ na danych XML. Oba te elementy zostały opisane w poniższych sekcjach.

LINQ to SQL: integracja z programem SQL

Zapytanie platformy .NET Language-Integrated może służyć do wykonywania zapytań względem relacyjnych magazynów danych bez opuszczania składni ani środowiska czasu kompilacji lokalnego języka programowania. Ta funkcja o nazwie code-named LINQ to SQL korzysta z integracji informacji o schemacie SQL z metadanymi CLR. Ta integracja kompiluje definicje tabel SQL i widoków w typy CLR, do których można uzyskać dostęp z dowolnego języka.

LINQ to SQL definiuje dwa podstawowe atrybuty: [Table] i [Column], które wskazują, które typy i właściwości CLR odpowiadają zewnętrznym danym SQL. Atrybut [Table] można zastosować do klasy i kojarzy typ CLR z nazwaną tabelą LUB widokiem SQL. Atrybut [Kolumna] można zastosować do dowolnego pola lub właściwości i kojarzy element członkowski z nazwaną kolumną SQL. Oba atrybuty są sparametryzowane, aby umożliwić zachowywanie metadanych specyficznych dla języka SQL. Rozważmy na przykład następującą prostą definicję schematu 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
)

Odpowiednik CLR wygląda następująco:

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

Uwaga W tym przykładzie kolumny dopuszczające wartość null są mapowane na typy dopuszczające wartość null w clR (typy dopuszczające wartość null po raz pierwszy pojawiły się w wersji 2.0 .NET Framework), a w przypadku typów SQL, które nie mają korespondencji 1:1 z typem CLR (na przykład nvarchar, char, text), oryginalny typ SQL jest zachowywany w metadanych CLR.

Aby wydać zapytanie względem magazynu relacyjnego, implementacja LINQ to SQL wzorca LINQ tłumaczy zapytanie z formularza drzewa wyrażeń na wyrażenie SQL i ADO.NET obiekt DbCommand odpowiedni do zdalnego oceny. Rozważmy na przykład to proste zapytanie:

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

Typ DataContext udostępnia uproszczony translator, który tłumaczy standardowe operatory zapytań na język SQL. Element DataContext używa istniejącego ADO.NET IDbConnection do uzyskiwania dostępu do magazynu i może zostać zainicjowany za pomocą ustanowionego obiektu połączenia ADO.NET lub parametrów połączenia, których można użyć do utworzenia.

Metoda GetTable udostępnia zmienne zgodne z IEnumerable, których można użyć w wyrażeniach zapytań do reprezentowania zdalnej tabeli lub widoku. Wywołania metody GetTable nie powodują żadnej interakcji z bazą danych , a nie reprezentują potencjału interakcji z tabelą zdalną lub widoku przy użyciu wyrażeń zapytań. W naszym przykładzie powyżej zapytanie nie jest przesyłane do magazynu, dopóki program nie wykonuje iteracji w wyrażeniu zapytania, w tym przypadku przy użyciu instrukcji foreach w języku C#. Gdy program najpierw wykonuje iterację zapytania, maszyna DataContext tłumaczy drzewo wyrażeń na następującą instrukcję SQL, która jest wysyłana do magazynu:

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

Należy pamiętać, że tworząc możliwości zapytań bezpośrednio w lokalnym języku programowania, deweloperzy uzyskują pełną moc modelu relacyjnego bez konieczności statycznego pieczenia relacji do typu CLR. Oznacza to, że kompleksowe mapowanie obiektów/relacyjnych może również korzystać z tej podstawowej funkcji zapytań dla użytkowników, którzy chcą korzystać z tej funkcji. LINQ to SQL udostępnia funkcje mapowania obiektów, z którymi deweloper może definiować relacje między obiektami i nawigować po nich. Można odwoływać się do orders jako właściwości klasy Customer przy użyciu mapowania, aby nie trzeba było jawnie łączyć tych dwóch ze sobą. Pliki mapowania zewnętrznego umożliwiają oddzielenie mapowania od modelu obiektów w celu uzyskania bardziej zaawansowanych możliwości mapowania.

LINQ to XML: integracja xml

Program .NET Language-Integrated Query for XML (LINQ to XML) umożliwia wykonywanie zapytań dotyczących danych XML przy użyciu standardowych operatorów zapytań, a także operatorów specyficznych dla drzewa, które zapewniają nawigację przypominającą ścieżkę XPath za pośrednictwem elementów potomnych, elementów nadrzędnych i elementów równorzędnych. Zapewnia wydajną reprezentację w pamięci dla kodu XML, która integruje się z istniejącą infrastrukturą czytnika/zapisywaniaSystem.Xmli jest łatwiejsza w użyciu niż W3C DOM. Istnieją trzy typy, które wykonują większość pracy integracji kodu XML z zapytaniami: XName, XElement i XAttribute.

XName zapewnia łatwy w użyciu sposób radzenia sobie z kwalifikowanymi identyfikatorami przestrzeni nazw (QNames) używanymi zarówno jako nazwy elementów, jak i atrybutów. XName obsługuje wydajną atomizację identyfikatorów w sposób niewidoczny i umożliwia używanie symboli lub zwykłych ciągów wszędzie tam, gdzie jest potrzebna nazwa QName.

Elementy i atrybuty XML są reprezentowane odpowiednio przy użyciu elementów XElement i XAttribute . XElement i XAttribute obsługują normalną składnię konstrukcji, umożliwiając deweloperom pisanie wyrażeń XML przy użyciu składni naturalnej:

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

var s = e.ToString();

Odpowiada to następującemu kodowi XML:

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

Zwróć uwagę, że do utworzenia wyrażenia XML nie jest potrzebny wzorzec fabryki oparty na modelu DOM i że implementacja ToString przyniosła tekstowy kod XML. Elementy XML można również skonstruować z istniejącego elementu XmlReader lub literału ciągu:

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

Element XElement obsługuje również emitowanie kodu XML przy użyciu istniejącego typu XmlWriter .

XElement dovetails z operatorami zapytań, umożliwiając deweloperom pisanie zapytań względem informacji innych niż XML i generowanie wyników XML przez konstruowanie elementów XElement w treści klauzuli select:

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

To zapytanie zwraca sekwencję elementów XElements. Aby umożliwić tworzenie elementów XElement z wyniku tego rodzaju zapytania, konstruktor XElement umożliwia bezpośrednie przekazywanie sekwencji elementów jako argumentów:

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

To wyrażenie XML powoduje następujący kod XML:

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

Powyższe instrukcje mają bezpośrednie tłumaczenie na język Visual Basic. Jednak język Visual Basic 9.0 obsługuje również używanie literałów XML, które umożliwiają wyrażanie wyrażeń zapytań przy użyciu deklaratywnej składni XML bezpośrednio z poziomu języka Visual Basic. Poprzedni przykład można skonstruować za pomocą instrukcji Visual Basic:

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

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

Przykłady do tej pory pokazały, jak konstruować nowe wartości XML przy użyciu zapytania zintegrowanego z językiem. Typy XElement i XAttribute upraszczają również wyodrębnianie informacji ze struktur XML. Element XElement udostępnia metody dostępu, które umożliwiają stosowanie wyrażeń zapytań do tradycyjnych osi XPath. Na przykład następujące zapytanie wyodrębnia tylko nazwy z elementu XElement pokazanego powyżej:

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

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

Aby wyodrębnić wartości ustrukturyzowane z pliku XML, po prostu używamy wyrażenia inicjatora obiektów w naszej klauzuli select:

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

Należy pamiętać, że zarówno XAttribute , jak i XElement obsługują jawne konwersje w celu wyodrębnienia wartości tekstowej jako typu pierwotnego. Aby poradzić sobie z brakującymi danymi, możemy po prostu rzutować do typu dopuszczalnego wartości null:

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

W tym przypadku używamy wartości domyślnej 21 , gdy brakuje atrybutu Age .

Program Visual Basic 9.0 zapewnia bezpośrednią obsługę języka dla metod dostępu Elements, Attribute i Potomnych elementu XElement, umożliwiając dostęp do danych opartych na formacie XML przy użyciu bardziej kompaktowej i bezpośredniej składni nazywanej właściwościami osi XML. Tej funkcji można użyć do zapisania poprzedniej instrukcji języka C#w następujący sposób:

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

W Visual Basic, x...<Osoba> pobiera wszystkie elementy w kolekcji Potomkowie x o nazwie Person, podczas gdy wyrażenie e.@Age znajduje wszystkie atrybuty XAttributes o nazwie Age. The Value właściwość pobiera pierwszy atrybut w kolekcji i wywołuje właściwość Value dla tego atrybutu.

Podsumowanie

Zapytanie platformy .NET Language-Integrated dodaje możliwości zapytań do środowiska CLR i języków przeznaczonych dla niego. Obiekt zapytań opiera się na wyrażeniach lambda i drzewach wyrażeń, aby umożliwić predykaty, projekcje i wyrażenia wyodrębniania kluczy do użycia jako nieprzezroczysty kod wykonywalny lub jako przezroczyste dane w pamięci odpowiednie do przetwarzania lub tłumaczenia podrzędnego. Standardowe operatory zapytań zdefiniowane przez projekt LINQ działają nad dowolnym źródłem informacji opartym na protokole IEnumerable<> i są zintegrowane z ADO.NET (LINQ to SQL) i System.Xml (LINQ to XML), aby umożliwić korzystanie z danych relacyjnych i XML w celu uzyskania korzyści ze zintegrowanego z językiem zapytania.

Standardowe operatory zapytań w skrócie

Operator Opis
Lokalizacja Operator ograniczeń oparty na funkcji predykatu
Wybierz/WybierzMany Operatory projekcji oparte na funkcji selektora
Take/Skip/ TakeWhile/SkipWhile Operatory partycjonowania na podstawie funkcji pozycji lub predykatu
Join/GroupJoin Operatory sprzężenia oparte na funkcjach selektora kluczy
Concat Operator łączenia
OrderBy/ThenBy/OrderByDescending/ThenByDescending Operatory sortowania sortowania w kolejności rosnącej lub malejącej na podstawie opcjonalnych funkcji selektora kluczy i porównywania
Reverse Operator sortowania odwraca kolejność sekwencji
Grupuj wg Operator grupowania oparty na opcjonalnych funkcjach selektora kluczy i porównywania
Distinct Ustawianie operatora usuwania duplikatów
Union/Intersect Ustawianie operatorów zwracających zestaw unii lub skrzyżowania
Z wyjątkiem Ustawianie różnicy w zestawie zwracanych przez operatorów
Asenumerable Operator konwersji na IEnumerable<T>
ToArray/ToList Operator konwersji do tablicy lub listy<T>
ToDictionary/ToLookup Operatory konwersji do słownika<K,T> lub Lookup<K,T> (wielo słownik) na podstawie funkcji selektora kluczy
OfType/Cast Operatory konwersji na IEnumerable<T> na podstawie filtrowania według lub konwersji na argument typu
Sequenceequal Operator równości sprawdzający równość elementu parowania
First/FirstOrDefault/Last/LastOrDefault/Single/Single/SingleOrDefault Operatory elementów zwracające element początkowy/końcowy/tylko na podstawie opcjonalnej funkcji predykatu
ElementAt/ElementAtOrDefault Operatory elementów zwracające element na podstawie pozycji
Defaultifempty Operator elementu zastępujący pustą sekwencję domyślną pojedynczą sekwencją
Zakres Operator generowania zwracający liczby w zakresie
Repeat Operator generowania zwracający wiele wystąpień danej wartości
Pusty Operator generowania zwracający pustą sekwencję
Dowolne/wszystkie Kwantyfikator sprawdzający egzystencjalne lub uniwersalne zadowolenie funkcji predykatu
Contains Kwantyfikator sprawdzający obecność danego elementu
Count/LongCount Operatory agregacji liczące elementy na podstawie opcjonalnej funkcji predykatu
Suma/min/maksimum/średnia Operatory agregacji oparte na opcjonalnych funkcjach selektora
Agregacja Operator agregujący gromadzący wiele wartości na podstawie funkcji akumulacji i opcjonalnego inicjatora