LINQ: .NET Language-Integrated-Abfrage
Don Box, Anders Hejlsberg
Februar 2007
Gilt für:
Visual Studio Code-Name "Orcas"
.NET Framework 3.5
Zusammenfassung: Universelle Abfragefunktionen, die dem .NET Framework hinzugefügt wurden, gelten für alle Informationsquellen, nicht nur für relationale oder XML-Daten. Diese Funktion wird als .NET Language-Integrated Query (LINQ) bezeichnet. (32 gedruckte Seiten)
Inhalte
.NET Language-Integrated-Abfrage
Erste Schritte mit Standardabfrageoperatoren
Sprachfeatures, die das LINQ-Projekt unterstützen
Weitere Standardabfrageoperatoren
Abfragesyntax
LINQ to SQL: SQL-Integration
LINQ to XML: XML-Integration
Zusammenfassung
.NET Language-Integrated-Abfrage
Nach zwei Jahrzehnten hat die Branche einen stabilen Punkt in der Entwicklung objektorientierter (OO)-Programmiertechnologien erreicht. Programmierer nehmen nun Features wie Klassen, Objekte und Methoden als selbstverständlich an. Bei der Betrachtung der aktuellen und der nächsten Generation von Technologien hat sich gezeigt, dass die nächste große Herausforderung in der Programmiertechnologie darin besteht, die Komplexität des Zugriffs auf und die Integration von Informationen zu reduzieren, die nicht nativ mit OO-Technologie definiert sind. Die beiden häufigsten Quellen für Nicht-OO-Informationen sind relationale Datenbanken und XML.
Anstatt unseren Programmiersprachen und der Laufzeit relationale oder XML-spezifische Features hinzuzufügen, haben wir mit dem LINQ-Projekt einen allgemeineren Ansatz verfolgt und dem .NET Framework universelle Abfragefunktionen hinzugefügt, die für alle Informationsquellen gelten, nicht nur relationale oder XML-Daten. Diese Funktion wird als .NET Language-Integrated Query (LINQ) bezeichnet.
Wir verwenden den Begriff sprachintegriert , um anzugeben, dass die Abfrage ein integriertes Feature der primären Programmiersprachen des Entwicklers ist (z. B. Visual C#, Visual Basic). Mit der sprachintegrierten Abfrage können Abfrageausdrücke von den umfangreichen Metadaten, der Überprüfung der Kompilierzeitsyntax, der statischen Eingabe und intelliSense profitieren, die zuvor nur für imperativen Code verfügbar war. Die sprachintegrierende Abfrage ermöglicht es auch, eine einzige deklarative Abfragefunktion für allgemeine Zwecke auf alle Im-Memory-Informationen anzuwenden, nicht nur auf Informationen aus externen Quellen.
.NET Language-Integrated Query definiert eine Reihe allgemeiner Standardabfrageoperatoren , die es ermöglichen, Traversal-, Filter- und Projektionsvorgänge in einer direkten, aber deklarativen Weise in einem beliebigen - ausdrucken zu können. NET-basierte Programmiersprache. Mit den Standardabfrageoperatoren können Abfragen auf jede IEnumerable<T-basierte> Informationsquelle angewendet werden. LINQ ermöglicht Es Dritten, den Satz von Standardabfrageoperatoren durch neue domänenspezifische Operatoren zu erweitern, die für die Zieldomäne oder -technologie geeignet sind. Noch wichtiger ist, dass Es Dritten auch frei steht, die Standardabfrageoperatoren durch ihre eigenen Implementierungen zu ersetzen, die zusätzliche Dienste wie Remoteauswertung, Abfrageübersetzung, Optimierung usw. bereitstellen. Durch Die Einhaltung der Konventionen des LINQ-Musters genießen solche Implementierungen die gleiche Sprachintegration und Toolunterstützung wie die Standardabfrageoperatoren.
Die Erweiterbarkeit der Abfragearchitektur wird im LINQ-Projekt selbst verwendet, um Implementierungen bereitzustellen, die sowohl über XML- als auch über SQL-Daten funktionieren. Die Abfrageoperatoren über XML (LINQ to XML) verwenden eine effiziente, benutzerfreundliche XML-Einrichtung im Arbeitsspeicher, um XPath/XQuery-Funktionalität in der Hostprogrammiersprache bereitzustellen. Die Abfrageoperatoren über relationale Daten (LINQ to SQL) bauen auf der Integration SQL-basierter Schemadefinitionen in das CLR-Typsystem (Common Language Runtime) auf. Diese Integration ermöglicht eine starke Eingabe relationaler Daten und behält gleichzeitig die Ausdruckskraft des relationalen Modells und die Leistung der Abfrageauswertung direkt im zugrunde liegenden Speicher bei.
Erste Schritte mit Standardabfrageoperatoren
Um die sprachintegrierte Abfrage am Arbeitsplatz zu sehen, beginnen wir mit einem einfachen C# 3.0-Programm, das die Standardabfrageoperatoren verwendet, um den Inhalt eines Arrays zu verarbeiten:
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);
}
}
Wenn Sie dieses Programm kompilieren und ausführen würden, wird dies als Ausgabe angezeigt:
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();
Die Abfrage der lokalen Variablen wird mit einem Abfrageausdruck initialisiert. Ein Abfrageausdruck arbeitet mit einer oder mehreren Informationsquellen, indem er einen oder mehrere Abfrageoperatoren aus den Standardabfrageoperatoren oder domänenspezifischen Operatoren anwendet. Dieser Ausdruck verwendet drei der Standardabfrageoperatoren: Where, OrderBy und Select.
Visual Basic 9.0 unterstützt auch LINQ. Dies ist die vorherige Anweisung, die in Visual Basic 9.0 geschrieben wurde:
Dim query As IEnumerable(Of String) = From s in names _
Where s.Length = 5 _
Order By s _
Select s.ToUpper()
Sowohl die hier gezeigten C#- als auch die Visual Basic-Anweisungen verwenden Abfrageausdrücke. Wie die foreach-Anweisung sind Abfrageausdrücke praktisch deklarative Kurzhand über Code, den Sie manuell schreiben können. Die obigen Anweisungen sind semantisch identisch mit der folgenden expliziten Syntax, die in C# dargestellt wird:
IEnumerable<string> query = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
Diese Abfrageform wird als methodenbasierte Abfrage bezeichnet. Die Argumente für die Operatoren Where, OrderBy und Select werden als Lambdaausdrücke bezeichnet, bei denen es sich um Codefragmente handelt, die ähnlich wie Delegaten sind. Sie ermöglichen es, die Standardabfrageoperatoren einzeln als Methoden zu definieren und mithilfe der Punktnotation aneinandergereiht zu werden. Zusammen bilden diese Methoden die Grundlage für eine erweiterbare Abfragesprache.
Sprachfeatures, die das LINQ-Projekt unterstützen
LINQ basiert vollständig auf universellen Sprachfeatures, von denen einige neu in C# 3.0 und Visual Basic 9.0 sind. Jedes dieser Features verfügt über ein eigenes Hilfsprogramm, aber zusammen bieten diese Features eine erweiterbare Möglichkeit zum Definieren von Abfragen und abfragebaren APIs. In diesem Abschnitt untersuchen wir diese Sprachfeatures und wie sie zu einem viel direkteren und deklarativeren Stil von Abfragen beitragen.
Lambdaausdrücke und Ausdrucksstrukturen
Viele Abfrageoperatoren ermöglichen es dem Benutzer, eine Funktion bereitzustellen, die Filterung, Projektion oder Schlüsselextraktion ausführt. Die Abfragefunktionen bauen auf dem Konzept der Lambdaausdrücke auf, die Entwicklern eine bequeme Möglichkeit bieten, Funktionen zu schreiben, die als Argumente für die nachfolgende Auswertung übergeben werden können. Lambdaausdrücke ähneln CLR-Delegaten und müssen einer Methodensignatur entsprechen, die durch einen Delegattyp definiert wird. Um dies zu veranschaulichen, können wir die obige Anweisung mithilfe des Func-Delegattyps in eine gleichwertige, aber explizitere Form erweitern:
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);
Lambdaausdrücke sind die natürliche Entwicklung anonymer Methoden in C# 2.0. Beispielsweise hätten wir das vorherige Beispiel mit anonymen Methoden wie den folgenden geschrieben:
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);
Im Allgemeinen steht es dem Entwickler frei, benannte Methoden, anonyme Methoden oder Lambdaausdrücke mit Abfrageoperatoren zu verwenden. Lambdaausdrücke haben den Vorteil, dass sie die direkteste und kompakteste Syntax für die Erstellung bereitstellen. Noch wichtiger ist, dass Lambdaausdrücke entweder als Code oder als Daten kompiliert werden können, sodass Lambdaausdrücke zur Laufzeit von Optimierern, Übersetzern und Auswertern verarbeitet werden können.
Der Namespace System.Linq.Expressions definiert einen differenzierten generischen Typ, Expression<T>, der angibt, dass eine Ausdrucksstruktur für einen bestimmten Lambdaausdruck anstelle eines herkömmlichen IL-basierten Methodentexts gewünscht ist. Ausdrucksstrukturen sind effiziente In-Memory-Datendarstellungen von Lambdaausdrücken und machen die Struktur des Ausdrucks transparent und explizit.
Die Bestimmung, ob der Compiler ausführbare IL oder eine Ausdrucksstruktur ausgibt, wird durch die Verwendung des Lambdaausdrucks bestimmt. Wenn ein Lambdaausdruck einer Variablen, einem Feld oder einem Parameter zugewiesen wird, deren Typ ein Delegat ist, gibt der Compiler eine IL aus, die mit dem einer anonymen Methode identisch ist. Wenn ein Lambdaausdruck einer Variablen, einem Feld oder einem Parameter zugewiesen wird, deren Typ Ausdruck<T> für einen Delegattyp T ist, gibt der Compiler stattdessen eine Ausdrucksstruktur aus.
Betrachten Sie beispielsweise die folgenden beiden Variablendeklarationen:
Func<int, bool> f = n => n < 5;
Expression<Func<int, bool>> e = n => n < 5;
Die Variable f ist ein Verweis auf einen Delegat, der direkt ausführbar ist:
bool isSmall = f(2); // isSmall is now true
Die Variable e ist ein Verweis auf eine Ausdrucksstruktur, die nicht direkt ausführbar ist:
bool isSmall = e(2); // compile error, expressions == data
Im Gegensatz zu Delegaten, die effektiv undurchsichtiger Code sind, können wir wie jede andere Datenstruktur in unserem Programm mit der Ausdrucksstruktur interagieren.
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);
Im obigen Beispiel wird die Ausdrucksstruktur zur Laufzeit zerlegt und die folgende Zeichenfolge ausgegeben:
n LessThan 5
Diese Fähigkeit, Ausdrücke zur Laufzeit als Daten zu behandeln, ist entscheidend, um ein Ökosystem von Drittanbieterbibliotheken zu ermöglichen, die die Basisabfrageabstraktionen nutzen, die Teil der Plattform sind. Die LINQ to SQL Datenzugriffsimplementierung nutzt diese Funktion, um Ausdrucksstrukturen in T-SQL-Anweisungen zu übersetzen, die für die Auswertung im Speicher geeignet sind.
Erweiterungsmethoden
Lambdaausdrücke sind ein wichtiger Bestandteil der Abfragearchitektur. Erweiterungsmethoden sind eine andere. Erweiterungsmethoden kombinieren die Flexibilität der in dynamischen Sprachen beliebten "Ententypisierung" mit der Leistung und Kompilierzeitüberprüfung statisch typisierter Sprachen. Mit Erweiterungsmethoden können Dritte den öffentlichen Auftrag eines Typs mit neuen Methoden erweitern und dennoch einzelnen Typautoren erlauben, ihre eigene spezialisierte Implementierung dieser Methoden bereitzustellen.
Erweiterungsmethoden werden in statischen Klassen als statische Methoden definiert, sind aber mit dem [System.Runtime.CompilerServices.Extension]- Attribut in CLR-Metadaten gekennzeichnet. Sprachen sollten eine direkte Syntax für Erweiterungsmethoden bereitstellen. In C# werden Erweiterungsmethoden durch diesen Modifizierer angegeben, der auf den ersten Parameter der Erweiterungsmethode angewendet werden muss. Sehen wir uns die Definition des einfachsten Abfrageoperators an , wo:
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;
}
}
}
Der Typ des ersten Parameters einer Erweiterungsmethode gibt an, auf welchen Typ die Erweiterung angewendet wird. Im obigen Beispiel erweitert die Where-Erweiterungsmethode den Typ IEnumerable<T>. Da Where eine statische Methode ist, können wir sie wie jede andere statische Methode direkt aufrufen:
IEnumerable<string> query = Enumerable.Where(names,
s => s.Length < 6);
Was erweiterungsmethoden jedoch einzigartig macht, ist, dass sie auch mit instance Syntax aufgerufen werden können:
IEnumerable<string> query = names.Where(s => s.Length < 6);
Erweiterungsmethoden werden zur Kompilierzeit aufgelöst, je nachdem, welche Erweiterungsmethoden sich im Bereich befinden. Wenn ein Namespace mit einer using-Anweisung in C# oder einer Import-Anweisung in Visual Basic importiert wird, werden alle Erweiterungsmethoden, die durch statische Klassen aus diesem Namespace definiert werden, in den Bereich übernommen.
Die Standardabfrageoperatoren werden als Erweiterungsmethoden des Typs System.Linq.Enumerable definiert. Wenn Sie die Standardabfrageoperatoren untersuchen, werden Sie feststellen, dass alle bis auf einige von ihnen in Bezug auf die IEnumerable<T-Schnittstelle> definiert sind. Dies bedeutet, dass jede IEnumerable<T-kompatible> Informationsquelle die Standardabfrageoperatoren einfach durch Hinzufügen der folgenden using-Anweisung in C# erhält:
using System.Linq; // makes query operators visible
Benutzer, die die Standardabfrageoperatoren für einen bestimmten Typ ersetzen möchten, können entweder ihre eigenen methoden mit gleichem Namen für den spezifischen Typ mit kompatiblen Signaturen definieren oder neue erweiterungsmethoden mit gleichem Namen definieren, die den spezifischen Typ erweitern. Benutzer, die die Standardabfrageoperatoren ganz vermeiden möchten, können System.Linq einfach nicht in den Bereich einfügen und ihre eigenen Erweiterungsmethoden für IEnumerable<T> schreiben.
Erweiterungsmethoden haben die niedrigste Priorität in Bezug auf die Auflösung und werden nur verwendet, wenn keine geeignete Übereinstimmung für den Zieltyp und seine Basistypen vorhanden ist. Dadurch können benutzerdefinierte Typen eigene Abfrageoperatoren bereitstellen, die Vorrang vor den Standardoperatoren haben. Betrachten Sie beispielsweise die folgende benutzerdefinierte Auflistung:
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;
}
}
Angesichts dieser Klassendefinition verwendet das folgende Programm die MySequence.Where-Implementierung, nicht die Erweiterungsmethode, da instance Methoden Vorrang vor Erweiterungsmethoden haben:
MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
Console.WriteLine(item);
Der OfType-Operator ist einer der wenigen Standardabfrageoperatoren, der eine IEnumerable<T-basierte> Informationsquelle nicht erweitert. Sehen wir uns den OfType-Abfrageoperator an:
public static IEnumerable<T> OfType<T>(this IEnumerable source) {
foreach (object item in source)
if (item is T)
yield return (T)item;
}
OfType akzeptiert nicht nur IEnumerable<T-basierte> Quellen, sondern auch Quellen, die für die nicht parametrisierte IEnumerable-Schnittstelle geschrieben wurden, die in Version 1.0 des .NET Framework vorhanden war. Mit dem OfType-Operator können Benutzer die Standardabfrageoperatoren wie folgt auf klassische .NET-Sammlungen anwenden:
// "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>();
In diesem Beispiel ergibt die Variable modern
dieselbe Sequenz von Werten wie klassisch. Sein Typ ist jedoch mit modernem IEnumerable<T-Code> kompatibel, einschließlich der Standardabfrageoperatoren.
Der OfType-Operator ist auch für neuere Informationsquellen nützlich, da er das Filtern von Werten aus einer Quelle basierend auf dem Typ ermöglicht. Beim Erstellen der neuen Sequenz lässt OfType einfach Member der ursprünglichen Sequenz aus, die nicht mit dem Type-Argument kompatibel sind. Betrachten Sie dieses einfache Programm, das Zeichenfolgen aus einem heterogenen Array extrahiert:
object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();
Wenn wir die JustStrings-Variable in einer foreach-Anweisung aufzählen, erhalten wir eine Sequenz von zwei Zeichenfolgen: "Hello" und "World".
Verzögerte Abfrageauswertung
Aufmerksame Leser haben möglicherweise bemerkt, dass der Standardoperator Where mithilfe des in C# 2.0 eingeführten Yield-Konstrukts implementiert wird. Diese Implementierungsmethode ist für alle Standardoperatoren üblich, die Sequenzen von Werten zurückgeben. Die Verwendung von Yield hat einen interessanten Vorteil, der darin besteht, dass die Abfrage erst ausgewertet wird, wenn sie durchlaufen wird, entweder mit einer foreach-Anweisung oder durch manuelle Verwendung der zugrunde liegenden GetEnumerator - und MoveNext-Methoden . Durch diese verzögerte Auswertung können Abfragen als IEnumerable<T-basierte> Werte beibehalten werden, die mehrmals ausgewertet werden können, wobei jedes Mal potenziell andere Ergebnisse erzielt werden.
Für viele Anwendungen ist dies genau das gewünschte Verhalten. Für Anwendungen, die die Ergebnisse der Abfrageauswertung zwischenspeichern möchten, werden zwei Operatoren, ToList und ToArray, bereitgestellt, die die sofortige Auswertung der Abfrage erzwingen und entweder ein Listen-T<> oder ein Array zurückgeben, das die Ergebnisse der Abfrageauswertung enthält.
Um zu sehen, wie die verzögerte Abfrageauswertung funktioniert, sehen Sie sich dieses Programm an, das eine einfache Abfrage über ein Array ausführt:
// 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);
Die Abfrage wird jedes Mal ausgewertet, wenn die Variable ayes durchlaufen wird. Um anzugeben, dass eine zwischengespeicherte Kopie der Ergebnisse erforderlich ist, können wir einfach einen ToList - oder ToArray-Operator wie folgt an die Abfrage anfügen:
// 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);
Sowohl ToArray als auch ToList erzwingen eine sofortige Abfrageauswertung. Gleiches gilt für die Standardabfrageoperatoren, die Singletonwerte zurückgeben (z. B. First, ElementAt, Sum, Average, All, Any).
Die IQueryable<T-Schnittstelle>
Dasselbe Modell der verzögerten Ausführung wird in der Regel für Datenquellen gewünscht, die die Abfragefunktionalität mithilfe von Ausdrucksbaumstrukturen implementieren, z. B. LINQ to SQL. Diese Datenquellen können von der Implementierung der IQueryable<T-Schnittstelle> profitieren, für die alle für das LINQ-Muster erforderlichen Abfrageoperatoren mithilfe von Ausdrucksstrukturen implementiert werden. Jedes IQueryable<T> hat eine Darstellung von "der Code, der zum Ausführen der Abfrage erforderlich ist" in Form einer Ausdrucksstruktur. Alle verzögerten Abfrageoperatoren geben ein neues IQueryable<T> zurück, das diese Ausdrucksstruktur um eine Darstellung eines Aufrufs dieses Abfrageoperators erweitert. Wenn es also an der Zeit ist, die Abfrage auszuwerten, in der Regel, weil das IQueryable<T> aufgezählt wird, kann die Datenquelle die Ausdrucksstruktur verarbeiten, die die gesamte Abfrage in einem Batch darstellt. Beispielsweise kann eine komplizierte LINQ to SQL Abfrage, die durch zahlreiche Aufrufe von Abfrageoperatoren abgerufen wird, dazu führen, dass nur eine einzelne SQL-Abfrage an die Datenbank gesendet wird.
Der Vorteil für Datenquellenimplementierer, diese Zurückstellungsfunktion wieder zu verwenden, indem die IQueryable<T-Schnittstelle>
implementiert wird, liegt auf der Hand. Für die Clients, die die Abfragen schreiben, ist es dagegen ein großer Vorteil, einen gemeinsamen Typ für Remoteinformationsquellen zu haben. Dadurch können sie nicht nur polymorphe Abfragen schreiben, die für verschiedene Datenquellen verwendet werden können, sondern eröffnet auch die Möglichkeit, Domänenübergreifende Abfragen zu schreiben.
Initialisieren zusammengesetzter Werte
Lambdaausdrücke und Erweiterungsmethoden stellen alles bereit, was wir für Abfragen benötigen, die einfach Elemente aus einer Sequenz von Werten herausfiltern. Die meisten Abfrageausdrücke führen auch Projektionen über diese Member durch, wodurch Elemente der ursprünglichen Sequenz effektiv in Member transformiert werden, deren Wert und Typ sich vom ursprünglichen element unterscheiden können. Um das Schreiben dieser Transformationen zu unterstützen, basiert LINQ auf einem neuen Konstrukt namens Objektinitialisierer , um neue Instanzen strukturierter Typen zu erstellen. Für den Rest dieses Dokuments wird davon ausgegangen, dass der folgende Typ definiert wurde:
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; }
}
}
Objektinitialisierer ermöglichen die einfache Erstellung von Werten, die auf den öffentlichen Feldern und Eigenschaften eines Typs basieren. Um beispielsweise einen neuen Wert vom Typ Person zu erstellen, können wir diese Anweisung schreiben:
Person value = new Person {
Name = "Chris Smith", Age = 31, CanCode = false
};
Semantisch entspricht diese Anweisung der folgenden Sequenz von Anweisungen:
Person value = new Person();
value.Name = "Chris Smith";
value.Age = 31;
value.CanCode = false;
Objektinitialisierer sind ein wichtiges Feature für sprachintegrierte Abfragen, da sie die Erstellung neuer strukturierter Werte in Kontexten ermöglichen, in denen nur Ausdrücke zulässig sind (z. B. in Lambdaausdrücken und Ausdrucksbaumstrukturen). Betrachten Sie beispielsweise diesen Abfrageausdruck, der einen neuen Person-Wert für jeden Wert in der Eingabesequenz erstellt:
IEnumerable<Person> query = names.Select(s => new Person {
Name = s, Age = 21, CanCode = s.Length == 5
});
Die Syntax der Objektinitialisierung eignet sich auch zum Initialisieren von Arrays mit strukturierten Werten. Betrachten Sie beispielsweise diese Arrayvariable, die mit einzelnen Objektinitialisierern initialisiert wird:
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 },
};
Strukturierte Werte und Typen
Das LINQ-Projekt unterstützt einen datenzentrierten Programmierstil, in dem einige Typen in erster Linie vorhanden sind, um eine statische "Form" über einen strukturierten Wert anstelle eines vollständigen Objekts mit Zustand und Verhalten bereitzustellen. Wenn diese Prämisse zu ihrer logischen Schlussfolgerung führt, ist es häufig der Fall, dass sich der Entwickler nur um die Struktur des Werts kümmert, und die Notwendigkeit eines benannten Typs für diese Form ist wenig sinnvoll. Dies führt zur Einführung anonymer Typen , die es ermöglichen, neue Strukturen "inline" mit ihrer Initialisierung zu definieren.
In C# ähnelt die Syntax für anonyme Typen der Syntax der Objektinitialisierung, mit der Ausnahme, dass der Name des Typs weggelassen wird. Betrachten Sie beispielsweise die folgenden beiden Anweisungen:
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
};
Die Variablen v1 und v2 verweisen beide auf ein In-Memory-Objekt, dessen CLR-Typ über die drei öffentlichen Eigenschaften Name, Age und CanCode verfügt. Die Variablen unterscheiden sich darin, dass v2 auf eine instance eines anonymen Typs verweist. In CLR-Begriffen unterscheiden sich anonyme Typen nicht von jedem anderen Typ. Was anonyme Typen besonders macht, ist, dass sie in Ihrer Programmiersprache keinen aussagekräftigen Namen haben. Die einzige Möglichkeit zum Erstellen von Instanzen eines anonymen Typs ist die Verwendung der oben gezeigten Syntax.
Damit Variablen auf Instanzen anonymer Typen verweisen können, aber dennoch von statischer Typisierung profitieren, führt C# implizit typisierte lokale Variablen ein: Die var-Schlüsselwort (keyword) kann anstelle des Typnamens für lokale Variablendeklarationen verwendet werden. Betrachten Sie beispielsweise dieses legale C# 3.0-Programm:
var s = "Bob";
var n = 32;
var b = true;
Die var Schlüsselwort (keyword) weist den Compiler an, den Typ der Variablen aus dem statischen Typ des Ausdrucks abzuleiten, der zum Initialisieren der Variablen verwendet wird. In diesem Beispiel sind die Typen s, n und bZeichenfolge, int und bool. Dieses Programm ist mit folgendem identisch:
string s = "Bob";
int n = 32;
bool b = true;
Die var-Schlüsselwort (keyword) ist ein Vorteil für Variablen, deren Typen aussagekräftige Namen haben, aber es ist eine Notwendigkeit für Variablen, die auf Instanzen anonymer Typen verweisen.
var value = new {
Name = " Brian Smith", Age = 31, CanCode = false
};
Im obigen Beispiel hat der Variablenwert einen anonymen Typ, dessen Definition der folgenden Pseudo-C# entspricht:
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() { ... }
}
Anonyme Typen können nicht über Assemblygrenzen hinweg freigegeben werden. Der Compiler stellt jedoch sicher, dass es in jeder Assembly höchstens einen anonymen Typ für eine bestimmte Sequenz von Eigenschaftennamen-/Typpaaren gibt.
Da anonyme Typen häufig in Projektionen verwendet werden, um ein oder mehrere Member eines vorhandenen strukturierten Werts auszuwählen, können wir bei der Initialisierung eines anonymen Typs einfach auf Felder oder Eigenschaften eines anderen Werts verweisen. Dies führt dazu, dass der neue anonyme Typ eine Eigenschaft erhält, deren Name, Typ und Wert alle aus der Eigenschaft oder dem Feld kopiert werden, auf die verwiesen wird.
Betrachten Sie für instance dieses Beispiel, das einen neuen strukturierten Wert erstellt, indem Eigenschaften aus anderen Werten kombiniert werden:
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"
Das Verweisen auf oben gezeigte Felder oder Eigenschaften ist einfach eine praktische Syntax zum Schreiben der folgenden expliziteren Form:
var couple = new {
Husband = new { Name = bob.Name, Age = bob.Age },
Wife = new { Name = jane.FirstName, Age = jane.Age }
};
In beiden Fällen erhält die Paarvariable eine eigene Kopie der Eigenschaften Name und Alter von bob und jane.
Anonyme Typen werden am häufigsten in der select-Klausel einer Abfrage verwendet. Betrachten Sie beispielsweise die folgende Abfrage:
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");
In diesem Beispiel konnten wir eine neue Projektion über den Typ Person erstellen, die genau der Form entspricht, die wir für unseren Verarbeitungscode benötigten, aber dennoch die Vorteile eines statischen Typs bietet.
Weitere Standardabfrageoperatoren
Zusätzlich zu den oben beschriebenen grundlegenden Abfragefunktionen bieten eine Reihe von Operatoren nützliche Möglichkeiten zum Bearbeiten von Sequenzen und zum Verfassen von Abfragen, sodass der Benutzer ein hohes Maß an Kontrolle über das Ergebnis im praktischen Rahmen der Standardabfrageoperatoren erhält.
Sortieren und Gruppieren
Im Allgemeinen führt die Auswertung einer Abfrage zu einer Sequenz von Werten, die in einer reihenfolge erzeugt werden, die in den zugrunde liegenden Informationsquellen intrinsisch ist. Um Entwicklern die explizite Kontrolle über die Reihenfolge zu geben, in der diese Werte erzeugt werden, werden Standardabfrageoperatoren definiert, um die Reihenfolge zu steuern. Der grundlegendste dieser Operatoren ist der OrderBy-Operator .
Die Operatoren OrderBy und OrderByDescending können auf jede Informationsquelle angewendet werden und ermöglichen es dem Benutzer, eine Schlüsselextraktionsfunktion bereitzustellen, die den Wert erzeugt, der zum Sortieren der Ergebnisse verwendet wird. OrderBy und OrderByDescending akzeptieren auch eine optionale Vergleichsfunktion, die verwendet werden kann, um eine Teilreihenfolge für die Schlüssel zu erzwingen. Sehen wir uns ein einfaches Beispiel an:
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);
Die ersten beiden Abfrageausdrücke erzeugen neue Sequenzen, die auf der Sortierung der Elemente der Quelle basierend auf dem Zeichenfolgenvergleich basieren. Die zweiten beiden Abfragen erzeugen neue Sequenzen, die auf dem Sortieren der Member der Quelle basierend auf der Länge der einzelnen Zeichenfolgen basieren.
Um mehrere Sortierkriterien zuzulassen, geben sowohl OrderBy als auch OrderByDescendingOrderedSequence<T> anstelle des generischen IEnumerable<T> zurück. Zwei Operatoren werden nur für OrderedSequence<T> definiert, nämlich ThenBy und ThenByDescending , die ein zusätzliches (untergeordnetes) Sortierkriterium anwenden. ThenBy/ThenByDescending selbst gibt OrderedSequence<T> zurück, sodass eine beliebige Anzahl von ThenBy/ThenByDescending-Operatoren angewendet werden kann:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);
Die Auswertung der Abfrage, auf die in diesem Beispiel von s1 verwiesen wird, würde die folgende Sequenz von Werten ergeben:
"Burke", "David", "Frank",
"Albert", "Connor", "George", "Harris",
"Everett"
Zusätzlich zur OrderBy-Operatorenfamilie enthalten die Standardabfrageoperatoren auch einen Reverse-Operator . Reverse listet einfach eine Sequenz auf und ergibt dieselben Werte in umgekehrter Reihenfolge. Im Gegensatz zu OrderBy berücksichtigt Reverse bei der Bestimmung der Reihenfolge nicht die tatsächlichen Werte selbst, sondern basiert ausschließlich auf der Reihenfolge, in der die Werte von der zugrunde liegenden Quelle erzeugt werden.
Der OrderBy-Operator erzwingt eine Sortierreihenfolge über eine Sequenz von Werten. Die Standardabfrageoperatoren enthalten auch den GroupBy-Operator , der eine Partitionierung über eine Sequenz von Werten auf der Grundlage einer Schlüsselextraktionsfunktion erzwingt. Der GroupBy-Operator gibt eine Sequenz von IGrouping-Werten zurück, eine für jeden einzelnen gefundenen Schlüsselwert. Ein IGrouping ist eine IEnumerable , die zusätzlich den Schlüssel enthält, der zum Extrahieren des Inhalts verwendet wurde:
public interface IGrouping<K, T> : IEnumerable<T> {
public K Key { get; }
}
Die einfachste Anwendung von GroupBy sieht wie folgt aus:
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);
}
Bei der Ausführung gibt dieses Programm Folgendes aus:
Strings of length 6
Albert
Connor
George
Harris
Strings of length 5
Burke
David
Frank
Strings of length 7
Everett
A la Select, GroupBy ermöglicht Es Ihnen, eine Projektionsfunktion bereitzustellen, die zum Auffüllen von Mitgliedern der Gruppen verwendet wird.
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);
}
Diese Variante gibt Folgendes aus:
Strings of length 6
A
C
G
H
Strings of length 5
B
D
F
Strings of length 7
E
Hinweis In diesem Beispiel muss der projizierte Typ nicht mit der Quelle identisch sein. In diesem Fall haben wir eine Gruppierung von ganzen Zahlen zu Zeichen aus einer Sequenz von Zeichenfolgen erstellt.
Aggregationsoperatoren
Mehrere Standardabfrageoperatoren sind definiert, um eine Sequenz von Werten in einem einzelnen Wert zu aggregieren. Der allgemeinste Aggregationsoperator ist Aggregate, der wie folgt definiert ist:
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;
}
Mit dem Aggregate-Operator ist es einfach, eine Berechnung über eine Sequenz von Werten durchzuführen. Aggregieren funktioniert, indem der Lambdaausdruck für jedes Element der zugrunde liegenden Sequenz einmal aufgerufen wird. Jedes Mal, wenn Aggregate den Lambdaausdruck aufruft, übergibt es sowohl das Element aus der Sequenz als auch einen aggregierten Wert (der Anfangswert ist der Seedparameter an Aggregate). Das Ergebnis des Lambdaausdrucks ersetzt den vorherigen aggregierten Wert, und Aggregate gibt das Endergebnis des Lambdaausdrucks zurück.
Dieses Programm verwendet beispielsweise Aggregate , um die Gesamtzeichenanzahl über ein Array von Zeichenfolgen zu akkumulieren:
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
int count = names.Aggregate(0, (c, s) => c + s.Length);
// count == 46
Zusätzlich zum Universellen Aggregatoperator enthalten die Standardabfrageoperatoren auch einen Universellen Count-Operator und vier numerische Aggregationsoperatoren (Min, Max, Sum und Average), die diese allgemeinen Aggregationsvorgänge vereinfachen. Die numerischen Aggregationsfunktionen arbeiten über Sequenzen numerischer Typen (z. B. int, double, dezimal) oder über Sequenzen beliebiger Werte, solange eine Funktion bereitgestellt wird, die Elemente der Sequenz in einen numerischen Typ projiziert.
Dieses Programm veranschaulicht beide Formen des soeben beschriebenen Sum-Operators :
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
Hinweis Die zweite Sum-Anweisung entspricht dem vorherigen Beispiel unter Verwendung von Aggregate.
Select vs. SelectMany
Der Select-Operator erfordert, dass die Transformationsfunktion einen Wert für jeden Wert in der Quellsequenz erzeugt. Wenn Ihre Transformationsfunktion einen Wert zurückgibt, der selbst eine Sequenz ist, ist es Aufgabe des Consumers, die Untersequenzen manuell zu durchlaufen. Betrachten Sie beispielsweise dieses Programm, das Zeichenfolgen mithilfe der vorhandenen String.Split-Methode in Token unterbricht:
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);
Bei der Ausführung gibt dieses Programm den folgenden Text aus:
Albert.was.here.Burke.slept.late.Connor.is.happy.
Im Idealfall hätten wir es uns gewünscht, dass unsere Abfrage eine zusammengekettete Sequenz von Token zurückgegeben hätte und die Zwischenzeichenfolge[] nicht für den Consumer verfügbar gemacht hätte. Um dies zu erreichen, verwenden wir den SelectMany-Operator anstelle des Select-Operators . Der SelectMany-Operator funktioniert ähnlich wie der Select-Operator . Sie unterscheidet sich darin, dass die Transformationsfunktion eine Sequenz zurückgibt, die dann durch den SelectMany-Operator erweitert wird. Hier ist unser Programm mit SelectMany neu geschrieben:
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);
Die Verwendung von SelectMany bewirkt, dass jede Zwischensequenz im Rahmen der normalen Auswertung erweitert wird.
SelectMany eignet sich ideal für die Kombination von zwei Informationsquellen:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.SelectMany(n =>
people.Where(p => n.Equals(p.Name))
);
Im Lambdaausdruck, der an SelectMany übergeben wird, gilt die geschachtelte Abfrage für eine andere Quelle, weist aber im Bereich
den Parameter auf, der n
von der äußeren Quelle übergeben wurde. Also Menschen. Wobei für jedes n einmal aufgerufen wird, wobei die resultierenden Sequenzen von SelectMany für die endgültige Ausgabe abgeflacht wurden. Das Ergebnis ist eine Sequenz aller Personen, deren Name im Namensarray angezeigt wird.
Verknüpfungsoperatoren
In einem objektorientierten Programm werden Objekte, die miteinander verknüpft sind, in der Regel mit Objektverweisen verknüpft, die leicht zu navigieren sind. Das gleiche gilt in der Regel nicht für externe Informationsquellen, bei denen Dateneinträge oft keine andere Möglichkeit haben, als symbolisch aufeinander zu zeigen, mit IDs oder anderen Daten, die eindeutig die Entität identifizieren können, auf die verwiesen wird. Das Konzept von Joins bezieht sich auf den Vorgang des Zusammenführens der Elemente einer Sequenz mit den Elementen, mit denen sie "übereinstimmen" aus einer anderen Sequenz.
Im vorherigen Beispiel mit SelectMany wird genau dies ausgeführt, da Zeichenfolgen mit Personen übereinstimmen, deren Namen diese Zeichenfolgen sind. Für diesen speziellen Zweck ist der SelectMany-Ansatz jedoch nicht sehr effizient. Er durchläuft alle Elemente von Personen für jedes einzelne Element von Namen. Indem alle Informationen dieses Szenarios – die beiden Informationsquellen und die "Schlüssel", mit denen sie verknüpft werden – in einem Methodenaufruf zusammengeführt werden, kann der Join-Operator eine viel bessere Arbeit erledigen:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.Join(people, n => n, p => p.Name, (n,p) => p);
Dies ist ein wenig müssig, aber sehen Sie, wie die Teile zusammenpassen: Die Join-Methode wird für die "äußere" Datenquelle aufgerufen, namen. Das erste Argument ist die "innere" Datenquelle People. Das zweite und dritte Argument sind Lambdaausdrücke, um Schlüssel aus den Elementen der äußeren bzw. inneren Quelle zu extrahieren. Diese Schlüssel werden von der Join-Methode verwendet, um die Elemente abzugleichen. Hier sollen die Namen selbst mit der Name-Eigenschaft der Personen übereinstimmen. Der endgültige Lambdaausdruck ist dann für die Erstellung der Elemente der resultierenden Sequenz verantwortlich: Er wird mit jedem Paar übereinstimmenden Elementen n und p aufgerufen und wird verwendet, um das Ergebnis zu formen. In diesem Fall wird das n verworfen und das p zurückgegeben. Das Endergebnis ist die Liste der Personenelemente von Personen , deren Name in der Liste der Namen enthalten ist.
Ein mächtigerer Cousin von Join ist der GroupJoin-Operator . GroupJoin unterscheidet sich von Join durch die Verwendung des ergebnisbildenden Lambdaausdrucks: Anstatt mit jedem einzelnen Paar von äußeren und inneren Elementen aufgerufen zu werden, wird er nur einmal für jedes äußere Element aufgerufen, wobei eine Sequenz aller inneren Elemente mit diesem äußeren Element übereinstimmt. So konkretisieren Sie folgendes:
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() }
);
Dieser Aufruf erzeugt eine Sequenz der Namen, mit denen Sie begonnen haben, gepaart mit der Anzahl der Personen, die diesen Namen haben. Daher können Sie mit dem GroupJoin-Operator Ihre Ergebnisse auf dem gesamten "Satz von Übereinstimmungen" für ein äußeres Element basieren.
Abfragesyntax
Die vorhandene foreach-Anweisung in C# stellt eine deklarative Syntax für die Iteration über die IEnumerable/IEnumerator-Methoden von .NET Frameworks bereit. Die foreach-Anweisung ist streng optional, hat sich aber als sehr praktischer und beliebter Sprachmechanismus erwiesen.
Basierend auf diesem Präzedenzfall vereinfachen Abfrageausdrücke Abfragen mit einer deklarativen Syntax für die häufigsten Abfrageoperatoren: Where, Join, GroupJoin, Select, SelectMany, GroupBy, OrderBy, ThenBy, OrderByDescending, ThenByDescending und Cast.
Beginnen wir mit der einfachen Abfrage, mit der wir dieses Papier begonnen haben:
IEnumerable<string> query = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
Mithilfe eines Abfrageausdrucks können wir diese genaue Anweisung wie folgt umschreiben:
IEnumerable<string> query = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
Wie die foreach-Anweisung in C# sind Abfrageausdrücke kompakter und einfacher zu lesen, sind aber völlig optional. Jeder Ausdruck, der als Abfrageausdruck geschrieben werden kann, verfügt über eine entsprechende (wenn auch ausführlichere) Syntax mit Punktnotation.
Beginnen wir mit der grundlegenden Struktur eines Abfrageausdrucks. Jeder syntaktische Abfrageausdruck in C# beginnt mit einer from-Klausel und endet entweder mit einer Select - oder Group-Klausel . Auf die anfängliche from-Klausel kann 0 oder mehr von, let, where, join und orderby-Klauseln folgen. Jede from-Klausel ist ein Generator, der eine Bereichsvariable über eine Sequenz einführt. jede let-Klausel gibt dem Ergebnis eines Ausdrucks einen Namen; und jede where-Klausel ist ein Filter, der Elemente aus dem Ergebnis ausschließt. Jede Joinklausel korreliert eine neue Datenquelle mit den Ergebnissen der vorherigen Klauseln. Eine orderby-Klausel gibt eine Reihenfolge für das Ergebnis an:
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
Betrachten Sie beispielsweise die folgenden beiden Abfrageausdrücke:
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;
Der Compiler behandelt diese Abfrageausdrücke so, als würden sie mit der folgenden expliziten Punktnotation geschrieben:
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
});
Abfrageausdrücke durchlaufen eine mechanische Übersetzung in Aufrufe von Methoden mit bestimmten Namen. Die genaue Abfrageoperatorimplementierung , die ausgewählt wird, hängt daher sowohl vom Typ der abgefragten Variablen als auch von den Erweiterungsmethoden ab, die sich im Bereich befinden.
Die bisher gezeigten Abfrageausdrücke haben nur einen Generator verwendet. Wenn mehr als ein Generator verwendet wird, wird jeder nachfolgende Generator im Kontext seines Vorgängers ausgewertet. Betrachten Sie beispielsweise diese geringfügige Änderung unserer Abfrage:
var query = from s1 in names
where s1.Length == 5
from s2 in names
where s1 == s2
select s1 + " " + s2;
Bei Ausführung für dieses Eingabearray:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
wir erhalten die folgenden Ergebnisse:
Burke Burke
Frank Frank
David David
Der obige Abfrageausdruck wird auf diesen Punktnotationsausdruck erweitert:
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);
Hinweis Diese Version von SelectMany verwendet einen zusätzlichen Lambdaausdruck, der verwendet wird, um das Ergebnis basierend auf Elementen aus der äußeren und inneren Sequenz zu erzeugen. In diesem Lambdaausdruck werden die beiden Bereichsvariablen in einem anonymen Typ gesammelt. Der Compiler erfindet einen Variablennamen von $1 , um diesen anonymen Typ in nachfolgenden Lambdaausdrücken anzugeben.
Eine spezielle Art von Generator ist die Join-Klausel , die Elemente einer anderen Quelle einführt, die mit den Elementen der vorherigen Klauseln gemäß den angegebenen Schlüsseln übereinstimmen. Eine Joinklausel kann die übereinstimmenden Elemente einzeln ergeben, aber wenn mit einer into-Klausel angegeben, werden die übereinstimmenden Elemente als Gruppe angegeben:
var query = from n in names
join p in people on n equals p.Name into matching
select new { Name = n, Count = matching.Count() };
Es überrascht nicht, dass diese Abfrage direkt zu einer abfrage erweitert wird, die wir bereits gesehen haben:
var query = names.GroupJoin(people, n => n, p => p.Name,
(n, matching) =>
new { Name = n, Count = matching.Count() }
);
Es ist häufig nützlich, die Ergebnisse einer Abfrage als Generator in einer nachfolgenden Abfrage zu behandeln. Um dies zu unterstützen, verwenden Abfrageausdrücke den in Schlüsselwort (keyword), um einen neuen Abfrageausdruck nach einer Select- oder Group-Klausel zu spleißen. Dies wird als Abfragefortsetzung bezeichnet.
Die in Schlüsselwort (keyword) ist besonders nützlich für die Nachverarbeitung der Ergebnisse einerGroup-by-Klausel
. Betrachten Sie beispielsweise dieses Programm:
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);
}
Dieses Programm gibt Folgendes aus:
Strings of length 7
Everett
Strings of length 6
Albert
Connor
George
Harris
Strings of length 5
Burke
David
Frank
In diesem Abschnitt wurde beschrieben, wie C# Abfrageausdrücke implementiert. Andere Sprachen können sich dafür entscheiden, zusätzliche Abfrageoperatoren mit expliziter Syntax zu unterstützen oder überhaupt keine Abfrageausdrücke zu haben.
Es ist wichtig zu beachten, dass die Abfragesyntax keineswegs hart mit den Standardabfrageoperatoren verbunden ist. Es handelt sich um ein rein syntaktisches Feature, das für alles gilt, was das Abfragemuster erfüllt, indem zugrunde liegende Methoden mit den entsprechenden Namen und Signaturen implementiert werden. Die oben beschriebenen Standardabfrageoperatoren verwenden dazu Erweiterungsmethoden, um die IEnumerable<T-Schnittstelle> zu erweitern. Entwickler können die Abfragesyntax für jeden beliebigen Typ nutzen, solange sie sicherstellen, dass sie dem Abfragemuster entspricht, entweder durch direkte Implementierung der erforderlichen Methoden oder durch Hinzufügen als Erweiterungsmethoden.
Diese Erweiterbarkeit wird im LINQ-Projekt selbst durch die Bereitstellung von zwei LINQ-fähigen APIs ausgenutzt, nämlich LINQ to SQL, die das LINQ-Muster für den SQL-basierten Datenzugriff implementiert, und LINQ to XML, das LINQ-Abfragen über XML-Daten zulässt. Beides wird in den folgenden Abschnitten beschrieben.
LINQ to SQL: SQL-Integration
.NET Language-Integrated Query kann verwendet werden, um relationale Datenspeicher abzufragen, ohne die Syntax oder Kompilierzeitumgebung der lokalen Programmiersprache zu verlassen. Diese Funktion mit dem Codenamen LINQ to SQL nutzt die Integration von SQL-Schemainformationen in CLR-Metadaten. Diese Integration kompiliert SQL-Tabellen- und Ansichtsdefinitionen in CLR-Typen, auf die von jeder Sprache aus zugegriffen werden kann.
LINQ to SQL definiert zwei Kernattribute, [Tabelle] und [Spalte], die angeben, welche CLR-Typen und -Eigenschaften externen SQL-Daten entsprechen. Das [Table]- Attribut kann auf eine Klasse angewendet werden und ordnet den CLR-Typ einer benannten SQL-Tabelle oder -Sicht zu. Das [Column]- Attribut kann auf jedes Feld oder jede Eigenschaft angewendet werden und ordnet das Element einer benannten SQL-Spalte zu. Beide Attribute werden parametrisiert, damit SQL-spezifische Metadaten beibehalten werden können. Betrachten Sie beispielsweise diese einfache SQL-Schemadefinition:
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
)
Die CLR-Entsprechung sieht wie folgt aus:
[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;
}
Hinweis In diesem Beispiel werden NULLable-Spalten nullable-Typen in der CLR zugeordnet (nullable-Typen wurden zuerst in Version 2.0 des .NET Framework angezeigt), und für SQL-Typen, die keine 1:1-Korrespondenz mit einem CLR-Typ aufweisen (z. B. nvarchar, char, text), wird der ursprüngliche SQL-Typ in den CLR-Metadaten beibehalten.
Um eine Abfrage für einen relationalen Speicher ausstellen zu können, übersetzt die LINQ to SQL Implementierung des LINQ-Musters die Abfrage aus der Ausdrucksstrukturform in einen SQL-Ausdruck und ADO.NET DbCommand-Objekt, das für die Remoteauswertung geeignet ist. Betrachten Sie beispielsweise diese einfache Abfrage:
// 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);
Der DataContext-Typ bietet einen einfachen Übersetzer, der die Standardabfrageoperatoren in SQL übersetzt. DataContext verwendet die vorhandene ADO.NET IDbConnection für den Zugriff auf den Speicher und kann entweder mit einem etablierten ADO.NET Verbindungsobjekt oder einer Verbindungszeichenfolge initialisiert werden, die zum Erstellen eines Objekts verwendet werden kann.
Die GetTable-Methode stellt IEnumerable-kompatible Variablen bereit, die in Abfrageausdrücken verwendet werden können, um die Remotetabelle oder -sicht darzustellen. Aufrufe von GetTable verursachen keine Interaktion mit der Datenbank, sondern stellen das Potenzial dar, mit der Remotetabelle oder -ansicht mithilfe von Abfrageausdrücken zu interagieren. In unserem obigen Beispiel wird die Abfrage erst an den Speicher übertragen, wenn das Programm den Abfrageausdruck durchläuft, in diesem Fall mit der foreach-Anweisung in C#. Wenn das Programm die Abfrage zum ersten Mal durchläuft, übersetzt die DataContext-Maschine die Ausdrucksstruktur in die folgende SQL-Anweisung, die an den Speicher gesendet wird:
SELECT [t0].[Age], [t1].[Amount],
[t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]
Es ist wichtig zu beachten, dass Entwickler durch das direkte Erstellen von Abfragefunktionen in die lokale Programmiersprache die volle Leistungsfähigkeit des relationalen Modells nutzen, ohne die Beziehungen statisch in den CLR-Typ einfügen zu müssen. Daher kann eine umfassende Objekt-/relationale Zuordnung auch von dieser kernigen Abfragefunktion für Benutzer profitieren, die diese Funktionalität wünschen. LINQ to SQL bietet objektrelationale Zuordnungsfunktionen, mit denen der Entwickler Beziehungen zwischen Objekten definieren und navigieren kann. Sie können auf Orders als Eigenschaft der Customer-Klasse mithilfe der Zuordnung verweisen, sodass Sie keine expliziten Verknüpfungen benötigen, um die beiden miteinander zu verknüpfen. Externe Zuordnungsdateien ermöglichen es, die Zuordnung vom Objektmodell zu trennen, um umfassendere Zuordnungsfunktionen zu ermöglichen.
LINQ to XML: XML-Integration
Mit .NET Language-Integrated Query for XML (LINQ to XML) können XML-Daten mithilfe der Standardabfrageoperatoren sowie strukturspezifischer Operatoren abgefragt werden, die XPath-ähnliche Navigation über Nachfahren, Vorgänger und Geschwister bereitstellen. Es bietet eine effiziente In-Memory-Darstellung für XML, die sich in die vorhandene System.Xml Reader/Writer-Infrastruktur integriert und einfacher zu verwenden ist als W3C DOM. Es gibt drei Typen, die die meisten Aufgaben der Integration von XML in Abfragen erledigen: XName, XElement und XAttribute.
XName bietet eine benutzerfreundliche Möglichkeit, die namespacequalifizierten Bezeichner (QNames) zu verarbeiten, die sowohl als Element- als auch Attributnamen verwendet werden. XName übernimmt die effiziente Zerstäubung von Bezeichnern transparent und ermöglicht die Verwendung von Symbolen oder einfachen Zeichenfolgen überall dort, wo ein QName benötigt wird.
XML-Elemente und -Attribute werden mit XElement bzw. XAttribute dargestellt. XElement und XAttribute unterstützen die normale Konstruktionssyntax, sodass Entwickler XML-Ausdrücke mit einer natürlichen Syntax schreiben können:
var e = new XElement("Person",
new XAttribute("CanCode", true),
new XElement("Name", "Loren David"),
new XElement("Age", 31));
var s = e.ToString();
Dies entspricht dem folgenden XML:
<Person CanCode="true">
<Name>Loren David</Name>
<Age>31</Age>
</Person>
Beachten Sie, dass kein DOM-basiertes Factorymuster erforderlich war, um den XML-Ausdruck zu erstellen, und dass die ToString-Implementierung den textuellen XML-Code lieferte. XML-Elemente können auch aus einem vorhandenen XmlReader oder aus einem Zeichenfolgenliteral erstellt werden:
var e2 = XElement.Load(xmlReader);
var e1 = XElement.Parse(
@"<Person CanCode='true'>
<Name>Loren David</Name>
<Age>31</Age>
</Person>");
XElement unterstützt auch das Ausgeben von XML mithilfe des vorhandenen XmlWriter-Typs .
XElement ist mit den Abfrageoperatoren verzahnt, sodass Entwickler Abfragen für Nicht-XML-Informationen schreiben und XML-Ergebnisse erstellen können, indem sie XElements im Textkörper einer Select-Klausel erstellen:
var query = from p in people
where p.CanCode
select new XElement("Person",
new XAttribute("Age", p.Age),
p.Name);
Diese Abfrage gibt eine Sequenz von XElements zurück. Damit XElements aus dem Ergebnis dieser Art von Abfrage erstellt werden kann, ermöglicht der XElement-Konstruktor die direkte Übergabe von Sequenzen von Elementen als Argumente:
var x = new XElement("People",
from p in people
where p.CanCode
select
new XElement("Person",
new XAttribute("Age", p.Age),
p.Name));
Dieser XML-Ausdruck ergibt den folgenden XML-Code:
<People>
<Person Age="11">Allen Frances</Person>
<Person Age="59">Connor Morgan</Person>
</People>
Die obige Anweisung verfügt über eine direkte Übersetzung in Visual Basic. Visual Basic 9.0 unterstützt jedoch auch die Verwendung von XML-Literalen, mit denen Abfrageausdrücke mithilfe einer deklarativen XML-Syntax direkt aus Visual Basic ausgedrückt werden können. Das vorherige Beispiel könnte mit der Visual Basic-Anweisung erstellt werden:
Dim x = _
<People>
<%= From p In people __
Where p.CanCode _
Select <Person Age=<%= p.Age %>>p.Name</Person> _
%>
</People>
Die bisherigen Beispiele haben gezeigt, wie neue XML-Werte mithilfe einer sprachintegrieren Abfrage erstellt werden. Die XElement - und XAttribute-Typen vereinfachen auch die Extraktion von Informationen aus XML-Strukturen. XElement stellt Accessormethoden bereit, mit denen Abfrageausdrücke auf die herkömmlichen XPath-Achsen angewendet werden können. Die folgende Abfrage extrahiert beispielsweise nur die Namen aus dem oben gezeigten XElement :
IEnumerable<string> justNames =
from e in x.Descendants("Person")
select e.Value;
//justNames = ["Allen Frances", "Connor Morgan"]
Um strukturierte Werte aus dem XML-Code zu extrahieren, verwenden wir einfach einen Objektinitialisiererausdruck in unserer select-Klausel:
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int)e.Attribute("Age")
};
Beachten Sie, dass sowohl XAttribute als auch XElement explizite Konvertierungen unterstützen, um den Textwert als primitiven Typ zu extrahieren. Um fehlende Daten zu behandeln, können wir einfach in einen NULLable-Typ umwandeln:
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int?)e.Attribute("Age") ?? 21
};
In diesem Fall verwenden wir den Standardwert 21 , wenn das Age-Attribut fehlt.
Visual Basic 9.0 bietet direkte Sprachunterstützung für die Accessormethoden Elements, Attribute und Descendants von XElement, sodass auf XML-basierte Daten mithilfe einer kompakteren und direkteren Syntax namens XML-Achseneigenschaften zugegriffen werden kann. Wir können diese Funktionalität verwenden, um die vorherige C#-Anweisung wie folgt zu schreiben:
Dim persons = _
From e In x...<Person> _
Select new Person { _
.Name = e.Value, _
.Age = IIF(e.@Age, 21) _
}
In Visual Basic ist x...<Person> ruft alle Elemente in der Descendants-Auflistung von x mit dem Namen Person ab, während der Ausdruck e.@Age alle XAttributes mit dem Namen Alter.
findet. Die Value-Eigenschaft ruft das erste Attribut in der Auflistung ab und ruft die Value-Eigenschaft für dieses Attribut auf.
Zusammenfassung
.NET Language-Integrated Query fügt der CLR und den Darauf ausgerichteten Sprachen Abfragefunktionen hinzu. Die Abfragefunktion baut auf Lambdaausdrücken und Ausdrucksstrukturen auf, damit Prädikate, Projektionen und Schlüsselextraktionsausdrücke als undurchsichtiger ausführbarer Code oder als transparente In-Memory-Daten verwendet werden können, die für die nachgelagerte Verarbeitung oder Übersetzung geeignet sind. Die vom LINQ-Projekt definierten Standardabfrageoperatoren arbeiten über jede IEnumerable<T-basierte> Informationsquelle und sind in ADO.NET (LINQ to SQL) und System.Xml (LINQ to XML) integriert, damit relationale und XML-Daten die Vorteile sprachintegrierter Abfragen nutzen können.
Standardabfrageoperatoren in a Nutshell
Operator | Beschreibung |
---|---|
Hierbei gilt: | Einschränkungsoperator basierend auf Prädikatfunktion |
Auswählen/AuswählenMany | Projektionsoperatoren basierend auf der Selektorfunktion |
Take/Skip/ TakeWhile/SkipWhile | Partitionierungsoperatoren basierend auf Position oder Prädikatfunktion |
Join/GroupJoin | Verknüpfen von Operatoren basierend auf Schlüsselauswahlfunktionen |
Concat | Concatenation-Operator |
OrderBy/ThenBy/OrderByDescending/ThenByDescending | Sortieroperatoren, die in aufsteigender oder absteigender Reihenfolge basierend auf optionalen Schlüsselauswahl- und Vergleichsfunktionen sortieren |
Reverse | Sortieroperator, der die Reihenfolge einer Sequenz umkehrt |
GroupBy | Gruppierungsoperator basierend auf optionalen Schlüsselauswahl- und Vergleichsfunktionen |
Distinct | Festlegen des Operators zum Entfernen von Duplikaten |
Union/Intersect | Festlegen von Operatoren, die Eine Satzunion oder Schnittmenge zurückgeben |
Except | Festlegen des Operators, der den Satzunterschied zurückgibt |
AsEnumerable | Konvertierungsoperator in IEnumerable<T> |
ToArray/ToList | Konvertierungsoperator in Array oder List<T> |
ToDictionary/ToLookup | Konvertierungsoperatoren in Dictionary<K,T> oder Lookup<K,T> (Multiwörterbuch) basierend auf der Schlüsselauswahlfunktion |
OfType/Cast | Konvertierungsoperatoren in IEnumerable<T> basierend auf der Filterung nach oder Konvertierung in das Typargument |
SequenceEqual | Gleichheitsoperator, der die Gleichheit von paarweisen Elementen überprüft |
First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault | Elementoperatoren, die ein anfängliches/endgültiges/reines Element basierend auf der optionalen Prädikatfunktion zurückgeben |
ElementAt/ElementAtOrDefault | Elementoperatoren, die ein Element basierend auf der Position zurückgeben |
DefaultIfEmpty | Elementoperator, der leere Sequenz durch singleton-Standardsequenz ersetzt |
Bereich | Generierungsoperator, der Zahlen in einem Bereich zurückgibt |
Wiederholen | Generierungsoperator, der mehrere Vorkommen eines bestimmten Werts zurückgibt |
Empty | Generierungsoperator, der eine leere Sequenz zurückgibt |
Beliebige/Alle | Quantifiziererprüfung auf existenzielle oder universelle Zufriedenheit der Prädikatfunktion |
Enthält | Quantifiziererüberprüfung auf Vorhandensein eines bestimmten Elements |
Count/LongCount | Aggregatoperatoren, die Elemente basierend auf der optionalen Prädikatfunktion zählen |
Summe/Min/Max/Durchschnitt | Aggregieren von Operatoren basierend auf optionalen Selektorfunktionen |
Aggregat | Aggregatoperator, der mehrere Werte basierend auf der Akkumulationsfunktion und optionalem Seed sammelt |