Grundlegendes zu C#-Funktionen (10) Abfrageausdruck

Grundlegendes zu C#-Funktionen (10) Abfrageausdruck

[LINQ über C#] - [C#-Funktionen]

Der C#-Abfrageausdruck definiert eine SQL-ähnliche Abfrage. Das Folgende ist ein Abfrageausdruck, der mit einer IEnumerable-Sequenz arbeitet:

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-Sequenz:

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.
  • 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
    • 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

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 oder IQueryable. Das Abfrageausdrucksmuster gilt jedoch für alles (jeden CLR-Typ). Um diese große Flexibilität zu demonstrieren, kann eine Abfragemethode für int (Typ System.Int32) implementiert werden:

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.