Espressione di query approfondita (10) di programmazione funzionale C#

Espressione di query approfondita (10) di programmazione funzionale C#

[LINQ tramite serie C#]

[Serie di approfondimento programmazione funzionale C#]

Ultima versione:https://weblogs.asp.net/dixin/functional-csharp-query-expression

C# 3.0 introduce l'espressione di query, uno zucchero sintattico di query simile a SQL per la composizione dei metodi di query.

Sintassi e compilazione

Quella che segue è la sintassi dell'espressione di query:

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]

Introduce nuove parole chiave del linguaggio in C#, chiamate parole chiave di query:

  • da
  • unisciti, accesi, uguali
  • lascia
  • dove
  • ordina per, crescente, decrescente
  • seleziona
  • gruppo, per
  • in

L'espressione di query viene compilata per interrogare le chiamate al metodo in fase di compilazione:

Espressione di query Metodo di query
clausola single from con clausola select Seleziona
più clausole con clausola select Seleziona molti
Digita clausole from/join Trasmetti
clausola di join senza into Unisciti
unisci la clausola con in Partecipa al gruppo
clausola let Seleziona
dove clausole Dove
clausola orderby con o senza ascendente Ordina per, poi per
clausola orderby con discendente OrderByDescending, ThenByDescending
clausola di gruppo GroupBy
in con la continuazione Query nidificata

È già stato dimostrato come funziona la sintassi delle espressioni di query per LINQ. In realtà, questa sintassi non è specifica per la query LINQ o per i tipi IEnumerable/ParallelQuery/IQueryable, ma uno zucchero sintattico C# generale. Prendi la clausola select (compilata nella chiamata al metodo Select) come esempio, può funzionare per qualsiasi tipo, purché il compilatore possa trovare un metodo di istanza Select o un metodo di estensione per quel tipo. Prendiamo come esempio, non ha un metodo Seleziona istanza, quindi è possibile definire il seguente metodo di estensione per accettare una funzione di selezione:

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

Ora la clausola select della sintassi dell'espressione della query può essere applicata a int:

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

E sono compilati sopra Seleziona chiamata al metodo di estensione:

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

Più in generale, il metodo Seleziona può essere definito per qualsiasi tipo:

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

Ora seleziona la clausola e il metodo Seleziona possono essere applicati a qualsiasi tipo:

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

Alcuni strumenti, come Resharper, una potente estensione per Visual Studio, possono aiutare a convertire le espressioni di query in metodi di query in fase di progettazione:

Modello di espressione della query

Per abilitare tutte le parole chiave di query per un determinato tipo, è necessario fornire una serie di metodi di query. Le seguenti interfacce mostrano le firme dei metodi richiesti per un tipo interrogabile localmente:

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

Tutti i metodi precedenti restituiscono ILocalSource, quindi questi metodi o clausole di espressione di query possono essere facilmente composti. I metodi di query precedenti sono rappresentati come metodi di istanza. Come accennato, anche i metodi di estensione funzionano. Questo è chiamato il modello di espressione della query. Allo stesso modo, le seguenti interfacce mostrano le firme dei metodi di query richiesti per un tipo interrogabile in remoto, che sostituisce tutti i parametri di funzione con i parametri dell'albero delle espressioni:

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

L'esempio seguente mostra come viene abilitata la sintassi dell'espressione di query per ILocal e IRemote:

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

La loro sintassi sembra identica ma sono compilati per chiamate a metodi di query differenti:

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 fornisce 3 set di metodi di query integrati:

  • IEnumerable rappresenta l'origine dati sequenziale locale e la query, il suo modello di espressione della query è implementato dai metodi di estensione forniti da System.Linq.Enumerable
  • ParallelQuery rappresenta l'origine dati parallela locale e la query, il suo modello di espressione di query è implementato dai metodi di estensione forniti da System.Linq.ParallelEnumerable
  • IQueryable rappresenta l'origine dati remota e la query, il suo modello di espressione di query è implementato dai metodi di estensione forniti da System.Linq.Queryable

Quindi l'espressione di query funziona per questi 3 tipi di LINQ. I dettagli sull'utilizzo e la compilazione delle espressioni di query sono trattati nel capitolo LINQ to Objects.

Espressione di query e metodo di query

L'espressione di query viene compilata per eseguire query sulle chiamate al metodo, entrambe le sintassi possono essere usate per creare una query LINQ. Tuttavia, l'espressione di query non copre tutti i metodi di query e i relativi overload. Ad esempio, la query Skip and Take non è supportata dalla sintassi dell'espressione della query:

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

La query seguente implementa il filtraggio e la mappatura delle query con l'espressione della query, ma Skip e Take devono essere chiamati come metodi di query, quindi è in una sintassi ibrida:

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

Un altro esempio è dove il metodo di query per IEnumerable ha 2 overload:

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

Il primo sovraccarico Where è supportato dall'espressione di query where clausola, il secondo overload non lo è.

Tutta la sintassi delle espressioni di query e tutti i metodi di query verranno discussi in dettaglio nei capitoli successivi. L'espressione di query è anche uno strumento per costruire un flusso di lavoro funzionale generale, che sarà discusso anche nel capitolo Teoria delle categorie.