C# Functional Programming In-Depth (10) Abfrageausdruck

C# Functional Programming In-Depth (10) Abfrageausdruck

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

Neueste Version:https://weblogs.asp.net/dixin/functional-csharp-query-expression

C# 3.0 führt einen Abfrageausdruck ein, einen SQL-ähnlichen syntaktischen Abfragezucker für die Zusammensetzung von Abfragemethoden.

Syntax und Kompilierung

Das Folgende ist die Syntax des Abfrageausdrucks:

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]
[continuation]

Es führt neue Sprachschlüsselwörter in C# ein, die als Abfrageschlüsselwörter bezeichnet werden:

  • von
  • beitreten, gleich
  • lassen
  • wo
  • orderby, aufsteigend, absteigend
  • auswählen
  • Gruppe, von
  • in

Der Abfrageausdruck wird zur Kompilierungszeit in Abfragemethodenaufrufe kompiliert:

Abfrageausdruck Abfragemethode
einzelne from-Klausel mit select-Klausel Auswählen
mehrere from-Klauseln mit select-Klausel SelectMany
Geben Sie from/join-Klauseln ein 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

Es wurde bereits gezeigt, wie die Abfrageausdruckssyntax für LINQ funktioniert. Tatsächlich ist diese Syntax nicht spezifisch für LINQ-Abfragen oder IEnumerable/ParallelQuery/IQueryable-Typen, sondern ein allgemeiner syntaktischer C#-Zucker. Nehmen Sie als Beispiel die select-Klausel (kompiliert zum Select-Methodenaufruf), sie kann für jeden Typ funktionieren, solange der Compiler eine Select-Instanzmethode oder eine Erweiterungsmethode für diesen Typ finden kann. Nehmen Sie int als Beispiel, es hat keine Instanzmethode auswählen, daher kann die folgende Erweiterungsmethode definiert werden, um eine Auswahlfunktion zu akzeptieren:

internal static partial class Int32Extensions
{
    internal static TResult Select<TResult>(this int int32, Func<int, TResult> selector) => 
        selector(int32);
}

Jetzt kann die select-Klausel der Abfrageausdruckssyntax auf int:

angewendet werden
internal static partial class QueryExpression
{
    internal static void SelectInt32()
    {
        int mapped1 = from zero in default(int) // 0
                      select zero; // 0
        double mapped2 = from three in 1 + 2 // 3
                         select Math.Sqrt(three + 1); // 2
    }
}

Und sie werden kompiliert, um den Aufruf der oben genannten Erweiterungsmethode auszuwählen:

internal static void CompiledSelectInt32()
{
    int mapped1 = Int32Extensions.Select(default, zero => zero); // 0
    double mapped2 = Int32Extensions.Select(1 + 2, three => Math.Sqrt(three + 1)); // 2
}

Allgemeiner kann die Select-Methode für jeden Typ definiert werden:

internal static partial class ObjectExtensions
{
    internal static TResult Select<TSource, TResult>(this TSource value, Func<TSource, TResult> selector) => 
        selector(value);
}

Jetzt können Select-Klausel und Select-Methode auf jeden Typ angewendet werden:

internal static void SelectGuid()
{
    string mapped = from newGuid in Guid.NewGuid()
                    select newGuid.ToString();
}

internal static void CompiledSelectGuid()
{
    string mapped = ObjectExtensions.Select(Guid.NewGuid(), newGuid => newGuid.ToString());
}

Einige Tools wie Resharper, eine leistungsstarke Erweiterung für Visual Studio, können beim Konvertieren von Abfrageausdrücken in Abfragemethoden zur Entwurfszeit helfen:

Abfrageausdrucksmuster

Um alle Abfrageschlüsselwörter für einen bestimmten Typ zu aktivieren, muss eine Reihe von Abfragemethoden bereitgestellt werden. Die folgenden Schnittstellen demonstrieren die Signaturen der erforderlichen Methoden für einen lokal abfragbaren Typ:

public interface ILocal
{
    ILocal<T> Cast<T>();
}

public interface ILocal<T> : ILocal
{
    ILocal<T> Where(Func<T, bool> predicate);

    ILocal<TResult> Select<TResult>(Func<T, TResult> selector);

    ILocal<TResult> SelectMany<TSelector, TResult>(
        Func<T, ILocal<TSelector>> selector,
        Func<T, TSelector, TResult> resultSelector);

    ILocal<TResult> Join<TInner, TKey, TResult>(
        ILocal<TInner> inner,
        Func<T, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        Func<T, TInner, TResult> resultSelector);

    ILocal<TResult> GroupJoin<TInner, TKey, TResult>(
        ILocal<TInner> inner,
        Func<T, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        Func<T, ILocal<TInner>, TResult> resultSelector);

    IOrderedLocal<T> OrderBy<TKey>(Func<T, TKey> keySelector);

    IOrderedLocal<T> OrderByDescending<TKey>(Func<T, TKey> keySelector);

    ILocal<ILocalGroup<TKey, T>> GroupBy<TKey>(Func<T, TKey> keySelector);

    ILocal<ILocalGroup<TKey, TElement>> GroupBy<TKey, TElement>(
        Func<T, TKey> keySelector, Func<T, TElement> elementSelector);
}

public interface IOrderedLocal<T> : ILocal<T>
{
    IOrderedLocal<T> ThenBy<TKey>(Func<T, TKey> keySelector);

    IOrderedLocal<T> ThenByDescending<TKey>(Func<T, TKey> keySelector);
}

public interface ILocalGroup<TKey, T> : ILocal<T>
{
    TKey Key { get; }
}

Alle obigen Methoden geben ILocalSource zurück, sodass diese Methoden oder Abfrageausdrucksklauseln einfach zusammengesetzt werden können. Die obigen Abfragemethoden werden als Instanzmethoden dargestellt. Wie bereits erwähnt, funktionieren auch Erweiterungsmethoden. Dies wird als Abfrageausdrucksmuster bezeichnet. In ähnlicher Weise demonstrieren die folgenden Schnittstellen die Signaturen der erforderlichen Abfragemethoden für einen remote abfragbaren Typ, der alle Funktionsparameter durch Ausdrucksbaumparameter ersetzt:

public interface IRemote
{
    IRemote<T> Cast<T>();
}

public interface IRemote<T> : IRemote
{
    IRemote<T> Where(Expression<Func<T, bool>> predicate);

    IRemote<TResult> Select<TResult>(Expression<Func<T, TResult>> selector);

    IRemote<TResult> SelectMany<TSelector, TResult>(
        Expression<Func<T, IRemote<TSelector>>> selector,
        Expression<Func<T, TSelector, TResult>> resultSelector);

    IRemote<TResult> Join<TInner, TKey, TResult>(
        IRemote<TInner> inner,
        Expression<Func<T, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<T, TInner, TResult>> resultSelector);

    IRemote<TResult> GroupJoin<TInner, TKey, TResult>(
        IRemote<TInner> inner,
        Expression<Func<T, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<T, IRemote<TInner>, TResult>> resultSelector);

    IOrderedRemote<T> OrderBy<TKey>(Expression<Func<T, TKey>> keySelector);

    IOrderedRemote<T> OrderByDescending<TKey>(Expression<Func<T, TKey>> keySelector);

    IRemote<IRemoteGroup<TKey, T>> GroupBy<TKey>(Expression<Func<T, TKey>> keySelector);

    IRemote<IRemoteGroup<TKey, TElement>> GroupBy<TKey, TElement>(
        Expression<Func<T, TKey>> keySelector, Expression<Func<T, TElement>> elementSelector);
}

public interface IOrderedRemote<T> : IRemote<T>
{
    IOrderedRemote<T> ThenBy<TKey>(Expression<Func<T, TKey>> keySelector);

    IOrderedRemote<T> ThenByDescending<TKey>(Expression<Func<T, TKey>> keySelector);
}

public interface IRemoteGroup<TKey, T> : IRemote<T>
{
    TKey Key { get; }
}

Das folgende Beispiel zeigt, wie die Abfrageausdruckssyntax für ILocal und IRemote aktiviert wird:

internal static void LocalQuery(ILocal<Uri> uris)
{
    ILocal<string> query =
        from uri in uris
        where uri.IsAbsoluteUri // ILocal.Where and anonymous method.
        group uri by uri.Host into hostUris // ILocal.GroupBy and anonymous method.
        orderby hostUris.Key // ILocal.OrderBy and anonymous method.
        select hostUris.ToString(); // ILocal.Select and anonymous method.
}

internal static void RemoteQuery(IRemote<Uri> uris)
{
    IRemote<string> query =
        from uri in uris
        where uri.IsAbsoluteUri // IRemote.Where and expression tree.
        group uri by uri.Host into hostUris // IRemote.GroupBy and expression tree.
        orderby hostUris.Key // IRemote.OrderBy and expression tree.
        select hostUris.ToString(); // IRemote.Select and expression tree.
}

Ihre Syntax sieht identisch aus, aber sie werden zu unterschiedlichen Aufrufen von Abfragemethoden kompiliert:

internal static void CompiledLocalQuery(ILocal<Uri> uris)
{
    ILocal<string> query = uris
        .Where(uri => uri.IsAbsoluteUri) // ILocal.Where and anonymous method.
        .GroupBy(uri => uri.Host) // ILocal.GroupBy and anonymous method.
        .OrderBy(hostUris => hostUris.Key) // ILocal.OrderBy and anonymous method.
        .Select(hostUris => hostUris.ToString()); // ILocal.Select and anonymous method.
}

internal static void CompiledRemoteQuery(IRemote<Uri> uris)
{
    IRemote<string> query = uris
        .Where(uri => uri.IsAbsoluteUri) // IRemote.Where and expression tree.
        .GroupBy(uri => uri.Host) // IRemote.GroupBy and expression tree.
        .OrderBy(hostUris => hostUris.Key) // IRemote.OrderBy and expression tree.
        .Select(hostUris => hostUris.ToString()); // IRemote.Select and expression tree.
}

.NET bietet 3 Sätze integrierter Abfragemethoden:

  • IEnumerable stellt eine lokale sequentielle Datenquelle und Abfrage dar, sein Abfrageausdrucksmuster wird durch Erweiterungsmethoden implementiert, die von System.Linq.Enumerable
  • bereitgestellt werden
  • ParallelQuery stellt eine lokale parallele Datenquelle und Abfrage dar, sein Abfrageausdrucksmuster wird durch Erweiterungsmethoden implementiert, die von System.Linq.ParallelEnumerable
  • bereitgestellt werden
  • IQueryable stellt eine entfernte Datenquelle und Abfrage dar, sein Abfrageausdrucksmuster wird durch Erweiterungsmethoden implementiert, die von System.Linq.Queryable
  • bereitgestellt werden

Der Abfrageausdruck funktioniert also für diese 3 Arten von LINQ. Die Einzelheiten zur Verwendung und Kompilierung von Abfrageausdrücken werden im Kapitel LINQ to Objects behandelt.

Abfrageausdruck vs. Abfragemethode

Der Abfrageausdruck wird zu Abfragemethodenaufrufen kompiliert, beide Syntaxen können zum Erstellen einer LINQ-Abfrage verwendet werden. Der Abfrageausdruck deckt jedoch nicht alle Abfragemethoden und ihre Überladungen ab. Beispielsweise werden Skip- und Take-Abfragen nicht von der Abfrageausdruckssyntax unterstützt:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> source, int count);

        public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count);
    }
}

Die folgende Abfrage implementiert das Filtern und Zuordnen von Abfragen mit Abfrageausdruck, aber Skip und Take müssen als Abfragemethoden aufgerufen werden, also in einer Hybridsyntax:

public static void QueryExpressionAndMethod(IEnumerable<Product> products)
{
    IEnumerable<string> query =
        (from product in products
         where product.ListPrice > 0
         select product.Name)
        .Skip(20)
        .Take(10);
}

Ein weiteres Beispiel ist, wo die Abfragemethode für IEnumerable zwei Überladungen hat:

namespace System.Linq
{
    public static class Enumerable
    {
        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-Überladung wird von der Where-Klausel des Abfrageausdrucks unterstützt, die zweite Überladung nicht.

Die gesamte Abfrageausdruckssyntax und alle Abfragemethoden werden in späteren Kapiteln ausführlich besprochen. Der Abfrageausdruck ist auch ein Werkzeug zum Erstellen eines allgemeinen funktionalen Arbeitsablaufs, der auch im Kapitel „Kategorietheorie“ behandelt wird.