[LINQ über C#] - [C#-Funktionen]
Der C#-Abfrageausdruck definiert eine SQL-ähnliche Abfrage. Das Folgende ist ein Abfrageausdruck, der mit einer IEnumerable
public static partial class LinqToObjects { public static IEnumerable<int> Positive(IEnumerable<int> source) { return from value in source where value > 0 select value; } }
Und der folgende Abfrageausdruck funktioniert mit einer IQeuryable
public static string[] ProductNames(string categoryName) { using (AdventureWorksDataContext adventureWorks = new AdventureWorksDataContext()) { IQueryable<string> query = from product in adventureWorks.Products where product.ProductSubcategory.ProductCategory.Name == categoryName orderby product.ListPrice ascending select product.Name; // Define query. return query.ToArray(); // Execute query. } }
Syntax
Die Syntax des C#-Abfrageausdrucks ist wie SQL:
from [Type] identifier in source [from [Type] identifier in source] [join [Type] identifier in source on expression equals expression [into identifier]] [let identifier = expression] [where predicate] [orderby ordering [ascending | descending][, ordering [ascending | descending], …]] select expression | group expression by key [into identifier] [continueation]
was Abfrageschlüsselwörter beinhaltet:
- von
- in
- beitreten, gleich
- lassen
- wo
- orderby, aufsteigend, absteigend
- auswählen
- Gruppe, von
- in
Diese Syntax und Beispiele werden später im Detail erklärt.
Zusammenstellung
Der Abfrageausdruck wird zur Kompilierzeit in Abfragemethoden (auch Abfrageoperatoren genannt) übersetzt (kompiliert):
Abfrageausdruck | Abfragemethode |
einzelne from-Klausel mit select-Klausel | Auswählen |
mehrere from-Klauseln mit select-Klausel | SelectMany |
T in From/Join-Klauseln | Besetzung |
join-Klausel ohne into | Beitreten |
join-Klausel mit into | Gruppenbeitritt |
let-Klausel | Auswählen |
where-Klauseln | Wo |
orderby-Klausel mit oder ohne aufsteigend | OrderBy, ThenBy |
orderby-Klausel mit absteigendem | OrderByDescending, ThenByDescending |
Gruppenklausel | Gruppieren nach |
in mit Fortsetzung | Verschachtelte Abfrage |
Beispielsweise werden die obigen 2 Abfrageausdrücke in Abfragemethodenaufrufe kompiliert:
public static partial class LinqToObjects { public static IEnumerable<int> Positive(IEnumerable<int> source) { return source.Where(value => value > 0); } } public static partial class LinqToSql { public static string[] ProductNames(string categoryName) { using (NorthwindDataContext database = new NorthwindDataContext()) { IQueryable<string> query = database.Products .Where(product => product.Category.CategoryName == categoryName) .Select(product => product.ProductName); // Define query. return query.ToArray(); // Execute query. } } }
Hier:
- In der positiven Methode ist die Quelle ein IEnumerable
, daher wird der Abfrageausdruck kompiliert zu: - ein Where-Abfragemethodenaufruf für IEnumerbale
. Die Where-Methode von IEnumerable hat: - ein Func
-Parameter, die where-Klausel wird zu einer anonymen Methode kompiliert, die durch einen Lambda-Ausdruck dargestellt werden kann:value => value> 0.
- ein Func
- ein Where-Abfragemethodenaufruf für IEnumerbale
- In der ProductNames-Methode ist database.Products ein IQueryable
, daher wird der Abfrageausdruck kompiliert zu: - ein Where-Abfragemethodenaufruf für IQueryable
. Die Where-Methode von IQueryable hat ein: - Expression
>-Parameter, sodass die Where-Klausel zu einem Ausdrucksbaum kompiliert wird, der durch einen Lambda-Ausdruck dargestellt werden kann:product => product.Category.CategoryName ==categoryName
- Expression
- ein Select-Query-Methodenaufruf für IQueryable
. Die Select-Methode von IQueryable hat ein: - Parameter
- Expression
>. Hier ist TResult string, weil product.ProductName ausgewählt ist, also wird die select-Klausel zu einer Expression >-Ausdrucksbaumstruktur kompiliert, die durch einen Lambda-Ausdruck dargestellt werden kann:product => product.ProductName
- Expression
- ein Where-Abfragemethodenaufruf für IQueryable
Wenn die obigen Erweiterungsmethoden und die Lambda-Ausdruckssyntax vollständig desuagriert werden, werden die Abfrageausdrücke in Positive tatsächlich kompiliert zu:
public static class CompiledLinqToObjects { [CompilerGenerated] private static Func<int, bool> cachedAnonymousMethodDelegate; [CompilerGenerated] private static bool Positive0(int value) { return value > 0; } public static IEnumerable<int> Positive(IEnumerable<int> source) { return Enumerable.Where( source, cachedAnonymousMethodDelegate ?? (cachedAnonymousMethodDelegate = Positive0)); } }
Und der Abfrageausdruck in ProductNames wird kompiliert zu:
internal static class CompiledLinqToSql { [CompilerGenerated] private sealed class Closure { internal string categoryName; } internal static string[] ProductNames(string categoryName) { Closure closure = new Closure { categoryName = categoryName }; AdventureWorks adventureWorks = new AdventureWorks(); try { ParameterExpression product = Expression.Parameter(typeof(Product), "product"); // Define query IQueryable<string> query = Queryable.Select( Queryable.Where( adventureWorks.Products, Expression.Lambda<Func<Product, bool>>( Expression.Equal( // => product.ProductSubCategory.ProductCategory.Name == closure.categoryName Expression.Property( Expression.Property( // product.ProductSubCategory.ProductCategory.Name Expression.Property(product, "ProductSubCategory"), // product.ProductSubCategory "ProductCategory"), // ProductSubCategory.ProductCategory "Name"), // ProductCategory.Name Expression.Field( // Or Expression.Constant(categoryName) works too. Expression.Constant(closure), "categoryName"), // closure.categoryName false, typeof(string).GetMethod("op_Equals")), // == product)), Expression.Lambda<Func<Product, string>>( // product => product.ProductName Expression.Property(product, "ProductName"), // => product.ProductName product)); // product => // Execute query. return query.ToArray(); } finally { adventureWorks.Dispose(); } } }
In der ProductNames-Methode wird der categoryName-Parameter in eine Closure-Klasse eingeschlossen.
Abfrageausdrucksmuster
Um das obige Abfrageschlüsselwort zu aktivieren, muss die Quelle für den Abfrageausdruck einige bestimmte Methoden bereitstellen. Die folgenden Klassen demonstrieren diese Methoden zur vollständigen Unterstützung der obigen Abfrageschlüsselwörter:
public abstract class Source { public abstract Source<T> Cast<T>(); } public abstract class Source<T> : Source { public abstract Source<T> Where(Func<T, bool> predicate); public abstract Source<TResult> Select<TResult>(Func<T, TResult> selector); public abstract Source<TResult> SelectMany<TSelector, TResult>( Func<T, Source<TSelector>> selector, Func<T, TSelector, TResult> resultSelector); public abstract Source<TResult> Join<TInner, TKey, TResult>( Source<TInner> inner, Func<T, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<T, TInner, TResult> resultSelector); public abstract Source<TResult> GroupJoin<TInner, TKey, TResult>( Source<TInner> inner, Func<T, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<T, Source<TInner>, TResult> resultSelector); public abstract OrderedSource<T> OrderBy<TKey>(Func<T, TKey> keySelector); public abstract OrderedSource<T> OrderByDescending<TKey>(Func<T, TKey> keySelector); public abstract Source<SoourceGroup<TKey, T>> GroupBy<TKey>(Func<T, TKey> keySelector); public abstract Source<SoourceGroup<TKey, TElement>> GroupBy<TKey, TElement>( Func<T, TKey> keySelector, Func<T, TElement> elementSelector); } public abstract class OrderedSource<T> : Source<T> { public abstract OrderedSource<T> ThenBy<TKey>(Func<T, TKey> keySelector); public abstract OrderedSource<T> ThenByDescending<TKey>(Func<T, TKey> keySelector); } public abstract class SoourceGroup<TKey, T> : Source<T> { public abstract TKey Key { get; } }
Hier werden die Abfragemethoden alle als Instanzmethoden demonstriert. Tatsächlich funktionieren entweder Instanz- oder Erweiterungsmethoden. .NET bietet integrierte Abfragemethoden als Erweiterungsmethoden:
- System.Linq.Enumerable-Klasse enthält die Erweiterungsmethoden für IEnumerable
- System.Linq.Queryable-Klasse enthält die Erweiterungsmethoden für IQueryable
Die integrierten Abfragemethoden sind alle für Sequenzen – entweder IEnumerable
public static partial class Int32Extensions { public static TResult Select<TResult>(this int value, Func<int, TResult> selector) => selector(value); }
Diese Select-Methode folgt der Select-Signatur im obigen Abfrageausdrucksmuster. Beachten Sie auch in der obigen Kompilierungstabelle, dass die Select-Abfragemethode aus dem Select-Abfrageschlüsselwort kompiliert werden kann. Infolgedessen kann int (Typ System.Int32) jetzt durch den LINQ-Abfrageausdruck mit der Auswahlklausel abgefragt werden:
public static void QueryExpression() { int query1 = from zero in default(int) // 0 select zero; // 0 string query2 = from three in 1 + 2 // 3 select (three + 4).ToString(CultureInfo.InvariantCulture); // "7" }
Das sieht etwas zu schick aus. Tatsächlich werden sie zur Kompilierzeit nur zu Aufrufen der obigen Select-Erweiterungsmethode für int:
public static void QueryMethod() { int query1 = Int32Extensions.Select(default(int), zero => zero); string query2 = Int32Extensions.Select( (1 + 2), three => (three + 4).ToString(CultureInfo.InvariantCulture)); // "7" }
Wenn eine Where-Abfragemethode für int implementiert ist, kann das Schlüsselwort where in LINQ-Abfragen an int usw. verwendet werden.
Hier kann das Experiment mit Select noch etwas weiter gehen. Das int-Argument von Select kann durch einen beliebigen Typ ersetzt werden:
public static partial class ObjectExtensions { public static TResult Select<TSource, TResult>(this TSource value, Func<TSource, TResult> selector) => selector(value); }
Dann gibt es analog:
string query = from newGuild in Guid.NewGuid() select newGuild.ToString();
die kompiliert wird zu:
string query = ObjectExtensions.Select(Guid.NewGuid(), newGuild => newGuild.ToString());
Dieses leistungsstarke Design ermöglicht die LINQ-Abfragesyntax für jeden Datentyp.
Einige Tools wie Resharper, eine leistungsstarke Erweiterung für Visual Studio, können zur Entwurfszeit Abfrageausdrücke in Abfragemethoden kompilieren:
Dies ist sehr nützlich, um die Wahrheit der LINQ-Abfrage herauszufinden.
Abfrageausdruck vs. Abfragemethode
In Bezug auf den Abfrageausdruck, der in Abfragemethodenaufrufe kompiliert wird, kann jeder von ihnen beim Codieren einer LINQ-Abfrage verwendet werden. In diesem Tutorial werden Abfragemethoden gegenüber Abfrageausdrücken bevorzugt, weil:
- Abfragemethoden werden vom Abfrageausdruck entzuckert, sodass sie näher an der „Wahrheit“ liegen.
- Abfrageausdrücke können einige Abfragemethoden ausdrücken, aber nicht alle Überladungen davon.
- Konsistenz. Abfrageausdruck deckt nicht alle Abfrageszenarien/Abfrageüberladungen ab, dann muss Abfragemethode verwendet werden, sodass die Abfrage am Ende eine Mischung aus Abfrageausdruck und Abfragemethoden ist.
Beispielsweise hat die integrierte Abfragemethode Select zwei Überladungen:
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate); public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate);
Die erste Where-Logik kann wie oben erwähnt durch einen Abfrageausdruck ausgedrückt werden, die zweite Where-Logik jedoch nicht. Die folgende Abfrage kann nicht mit dem Abfrageausdruck implementiert werden:
public static partial class LinqToObjects { public static IEnumerable<Person> Where (IEnumerable<Person> source) => source.Where((person, index) => person.Age >= 18 && index%2 == 0); }
Ein weiteres Beispiel ist, dass der Abfrageausdruck die Abfrageergebnisse nicht auslagern kann:
public static string[] ProductNames(string categoryName, int pageSize, int pageIndex) { using (AdventureWorksDataContext adventureWorks = new AdventureWorksDataContext()) { IQueryable<string> query = (from product in adventureWorks.Products where product.ProductSubcategory.ProductCategory.Name == categoryName orderby product.ListPrice ascending select product.Name) .Skip(pageSize * checked(pageIndex - 1)) .Take(pageSize); // Define query. return query.ToArray(); // Execute query. } }
Abfragemethoden sehen konsistenter aus:
public static string[] ProductNames2(string categoryName, int pageSize, int pageIndex) { using (AdventureWorksDataContext adventureWorks = new AdventureWorksDataContext()) { IQueryable<string> query = adventureWorks .Products .Where(product => product.ProductSubcategory.ProductCategory.Name == categoryName) .OrderBy(product => product.ListPrice) .Select(product => product.Name) .Skip(pageSize * checked(pageIndex - 1)) .Take(pageSize); // Define query. return query.ToArray(); // Execute query. } }
Der Abfrageausdruck wird in einem späteren Kapitel ausführlich erläutert. Es ist im Wesentlichen auch ein leistungsstarkes Werkzeug zum Aufbau eines funktionalen Workflows, der ebenfalls in einem anderen Kapitel erklärt wird.