C# 関数型プログラミングの詳細 (10) クエリ式

[C# シリーズ経由の LINQ]

[C# 関数型プログラミングの詳細シリーズ]

最新バージョン:https://weblogs.asp.net/dixin/functional-csharp-query-expression

C# 3.0 では、クエリ メソッドを構成するための 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]
[continuation]

クエリ キーワードと呼ばれる新しい言語キーワードが C# に導入されます。

  • から
  • 結合、オン、等しい
  • させて
  • どこ
  • 並べ替え、昇順、降順
  • 選択
  • グループ別

クエリ式は、コンパイル時にクエリ メソッド呼び出しにコンパイルされます:

クエリ式 クエリ方法
単一の from 句と select 句 選択
複数の from 句と select 句 SelectMany
from/join 句を入力 キャスト
into なしの join 句 参加
join 句と into GroupJoin
let 節 選択
where句 場所
昇順の有無にかかわらず orderby 句 OrderBy、ThenBy
降順の orderby 句 OrderByDescending、ThenByDescending
グループ句 GroupBy
続きへ ネストされたクエリ

クエリ式の構文が LINQ でどのように機能するかは既に実証されています。実際、この構文は LINQ クエリまたは IEnumerable/ParallelQuery/IQueryable 型に固有のものではなく、一般的な C# 構文糖衣です。 select 句 (Select メソッド呼び出しにコンパイルされる) を例にとると、コンパイラがその型の Select インスタンス メソッドまたは拡張メソッドを見つけることができる限り、任意の型に対して機能します。 int を例にとると、Select インスタンス メソッドがないため、セレクター関数を受け入れるように次の拡張メソッドを定義できます。

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

クエリ式構文の select 句を 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
    }
}

そして、それらは上記の Select 拡張メソッド呼び出しにコンパイルされます:

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

より一般的には、Select メソッドは任意の型に対して定義できます:

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

select 句と Select メソッドを任意のタイプに適用できるようになりました:

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

Visual Studio の強力な拡張機能である Resharper などの一部のツールは、設計時にクエリ式をクエリ メソッドに変換するのに役立ちます。

クエリ式パターン

特定のタイプのすべてのクエリ キーワードを有効にするには、一連のクエリ メソッドを提供する必要があります。次のインターフェイスは、ローカルでクエリ可能な型に必要なメソッドのシグネチャを示しています:

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

上記のメソッドはすべて ILocalSource を返すため、これらのメソッドまたはクエリ式句は簡単に構成できます。上記のクエリ メソッドは、インスタンス メソッドとして表されます。前述のように、拡張メソッドも機能します。これをクエリ式パターンと呼びます。同様に、次のインターフェイスは、すべての関数パラメーターを式ツリー パラメーターに置き換える、リモートでクエリ可能な型に必要なクエリ メソッドのシグネチャを示しています。

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

次の例は、ILocal および 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.
}

これらの構文は同じように見えますが、異なるクエリ メソッド呼び出しにコンパイルされます:

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 は 3 セットの組み込みクエリ メソッドを提供します:

  • IEnumerable はローカルのシーケンシャル データ ソースとクエリを表し、そのクエリ式パターンは System.Linq.Enumerable によって提供される拡張メソッドによって実装されます
  • ParallelQuery はローカルの並列データ ソースとクエリを表し、そのクエリ式パターンは System.Linq.ParallelEnumerable によって提供される拡張メソッドによって実装されます
  • IQueryable はリモート データ ソースとクエリを表し、そのクエリ式パターンは System.Linq.Queryable によって提供される拡張メソッドによって実装されます

したがって、クエリ式はこれら 3 種類の LINQ で機能します。クエリ式の使用とコンパイルの詳細は、LINQ to Objects の章で説明されています。

クエリ式とクエリ メソッド

クエリ式はクエリ メソッド呼び出しにコンパイルされます。どちらの構文も LINQ クエリの作成に使用できます。ただし、クエリ式は、すべてのクエリ メソッドとそのオーバーロードをカバーしているわけではありません。たとえば、Skip および Take クエリは、クエリ式の構文ではサポートされていません:

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

次のクエリは、クエリ式を使用してフィルタリングとマッピング クエリを実装しますが、Skip と Take はクエリ メソッドとして呼び出す必要があるため、ハイブリッド構文になります:

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

別の例は、IEnumerable のクエリ メソッドには 2 つのオーバーロードがあります:

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

最初の Where オーバーロードはクエリ式 where 句でサポートされていますが、2 番目のオーバーロードはサポートされていません。

すべてのクエリ式の構文とすべてのクエリ メソッドについては、後の章で詳しく説明します。クエリ式は、一般的な機能ワークフローを構築するためのツールでもあります。これについては、カテゴリ理論の章でも説明します。