次の方法で共有


LINQ を拡張する方法

すべての LINQ ベースのメソッドは、似た 2 つのパターンのいずれかに従います。 列挙可能なシーケンスを受け取ります。 これらは、別のシーケンスまたは単一の値を返します。 図形の一貫性により、似た図形を持つメソッドを記述することで LINQ を拡張できます。 実際、.NET ライブラリは、LINQ が最初に導入されて以来、多くの .NET リリースで新しいメソッドを取得しました。 この記事では、同じパターンに従う独自のメソッドを記述して LINQ を拡張する例を示します。

LINQ クエリのカスタム メソッドを追加する

LINQ クエリに使用するメソッドのセットを拡張するには、 IEnumerable<T> インターフェイスに拡張メソッドを追加します。 たとえば、標準の平均または最大操作に加えて、値のシーケンスから 1 つの値を計算するカスタム集計メソッドを作成します。 また、値のシーケンスに対してカスタム フィルターまたは特定のデータ変換として機能し、新しいシーケンスを返すメソッドも作成します。 このようなメソッドの例としては、 DistinctSkipReverseがあります。

IEnumerable<T> インターフェイスを拡張するときに、任意の列挙可能なコレクションにカスタム メソッドを適用できます。 詳細については、「拡張メソッド」を参照してください。

集計メソッドは、一連の値から 1 つの値を計算します。 LINQ には、 AverageMinMaxなど、いくつかの集計メソッドが用意されています。 IEnumerable<T> インターフェイスに拡張メソッドを追加することで、独自の集計メソッドを作成できます。

C# 14 以降では、複数の拡張メンバーを含む 拡張ブロック を宣言できます。 拡張ブロックは、キーワード extension の後にかっこで囲まれた受信側パラメーターを使用して宣言します。 次のコード例は、拡張ブロックで Median という拡張メソッドを作成する方法を示しています。 メソッドは、 double型の数値のシーケンスの中央値を計算します。

extension(IEnumerable<double>? source)
{
    public double Median()
    {
        if (source is null || !source.Any())
        {
            throw new InvalidOperationException("Cannot compute median for a null or empty set.");
        }

        var sortedList =
            source.OrderBy(number => number).ToList();

        int itemIndex = sortedList.Count / 2;

        if (sortedList.Count % 2 == 0)
        {
            // Even number of items.
            return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
        }
        else
        {
            // Odd number of items.
            return sortedList[itemIndex];
        }
    }
}

this修飾子を静的メソッドに追加して拡張メソッドを宣言することもできます。 次のコードは、同等の Median 拡張メソッドを示しています。

public static class EnumerableExtension
{
    public static double Median(this IEnumerable<double>? source)
    {
        if (source is null || !source.Any())
        {
            throw new InvalidOperationException("Cannot compute median for a null or empty set.");
        }

        var sortedList =
            source.OrderBy(number => number).ToList();

        int itemIndex = sortedList.Count / 2;

        if (sortedList.Count % 2 == 0)
        {
            // Even number of items.
            return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
        }
        else
        {
            // Odd number of items.
            return sortedList[itemIndex];
        }
    }
}

IEnumerable<T> インターフェイスから他の集計メソッドを呼び出すのと同じ方法で、列挙可能なコレクションに対していずれかの拡張メソッドを呼び出します。

次のコード例は、Median型の配列に対して double メソッドを使用する方法を示しています。

double[] numbers = [1.9, 2, 8, 4, 5.7, 6, 7.2, 0];
var query = numbers.Median();

Console.WriteLine($"double: Median = {query}");
// This code produces the following output:
//     double: Median = 4.85

さまざまな型のシーケンスを受け入れるように、 集計メソッドをオーバーロード できます。 標準的な方法は、型ごとにオーバーロードを作成することです。 もう 1 つの方法は、ジェネリック型を受け取り、デリゲートを使用して特定の型に変換するオーバーロードを作成することです。 両方の方法を組み合わせることもできます。

サポートする型ごとに特定のオーバーロードを作成できます。 次のコード例は、Median型のint メソッドのオーバーロードを示しています。

// int overload
public static double Median(this IEnumerable<int> source) =>
    (from number in source select (double)number).Median();

次のコードに示すように、Median型とinteger型の両方に対してdoubleオーバーロードを呼び出すようになりました。

double[] numbers1 = [1.9, 2, 8, 4, 5.7, 6, 7.2, 0];
var query1 = numbers1.Median();

Console.WriteLine($"double: Median = {query1}");

int[] numbers2 = [1, 2, 3, 4, 5];
var query2 = numbers2.Median();

Console.WriteLine($"int: Median = {query2}");
// This code produces the following output:
//     double: Median = 4.85
//     int: Median = 3

オブジェクトの ジェネリック シーケンス を受け入れるオーバーロードを作成することもできます。 このオーバーロードは、デリゲートをパラメーターとして受け取り、それを使用してジェネリック型のオブジェクトのシーケンスを特定の型に変換します。

次のコードは、Median デリゲートをパラメーターとして受け取るFunc<T,TResult> メソッドのオーバーロードを示しています。 このデリゲートは、ジェネリック型 T のオブジェクトを受け取り、 double型のオブジェクトを返します。

// generic overload
public static double Median<T>(
    this IEnumerable<T> numbers, Func<T, double> selector) =>
    (from num in numbers select selector(num)).Median();

任意の型のオブジェクトのシーケンスに対して Median メソッドを呼び出すようになりました。 型に独自のメソッド オーバーロードがない場合は、デリゲート パラメーターを渡す必要があります。 C# では、この目的にラムダ式を使用できます。 また、Visual Basic でのみ、メソッド呼び出しの代わりに Aggregate 句または Group By 句を使用する場合は、この句のスコープ内にある任意の値または式を渡すことができます。

次のコード例は、整数の配列と文字列の配列に対して Median メソッドを呼び出す方法を示しています。 文字列の場合、配列内の文字列の長さの中央値が計算されます。 この例では、各ケースの Func<T,TResult> メソッドに Median デリゲート パラメーターを渡す方法を示します。

int[] numbers3 = [1, 2, 3, 4, 5];

/*
    You can use the num => num lambda expression as a parameter for the Median method
    so that the compiler will implicitly convert its value to double.
    If there is no implicit conversion, the compiler will display an error message.
*/
var query3 = numbers3.Median(num => num);

Console.WriteLine($"int: Median = {query3}");

string[] numbers4 = ["one", "two", "three", "four", "five"];

// With the generic overload, you can also use numeric properties of objects.
var query4 = numbers4.Median(str => str.Length);

Console.WriteLine($"string: Median = {query4}");
// This code produces the following output:
//     int: Median = 3
//     string: Median = 4

値のIEnumerable<T>を返すカスタム クエリ メソッドを使用して、 インターフェイスを拡張できます。 この場合、メソッドは IEnumerable<T>型のコレクションを返す必要があります。 このようなメソッドを使用して、フィルターまたはデータ変換を一連の値に適用できます。

次の例では、コレクション内の他のすべての要素を最初の要素から返す AlternateElements という名前の拡張メソッドを作成する方法を示します。

// Extension method for the IEnumerable<T> interface.
// The method returns every other element of a sequence.
public static IEnumerable<T> AlternateElements<T>(this IEnumerable<T> source)
{
    int index = 0;
    foreach (T element in source)
    {
        if (index % 2 == 0)
        {
            yield return element;
        }

        index++;
    }
}

次のコードに示すように、 IEnumerable<T> インターフェイスから他のメソッドを呼び出すのと同様に、列挙可能なコレクションに対してこの拡張メソッドを呼び出すことができます。

string[] strings = ["a", "b", "c", "d", "e"];

var query5 = strings.AlternateElements();

foreach (var element in query5)
{
    Console.WriteLine(element);
}
// This code produces the following output:
//     a
//     c
//     e

この記事に示す各例には、異なる レシーバーがあります。 つまり、各メソッドは、一意の受信側を指定する別の拡張ブロックで宣言する必要があります。 次のコード例は、3 つの異なる拡張ブロックを持つ 1 つの静的クラスを示しています。それぞれに、この記事で定義されているメソッドのいずれかが含まれています。

public static class EnumerableExtension
{
    extension(IEnumerable<double>? source)
    {
        public double Median()
        {
            if (source is null || !source.Any())
            {
                throw new InvalidOperationException("Cannot compute median for a null or empty set.");
            }

            var sortedList =
                source.OrderBy(number => number).ToList();

            int itemIndex = sortedList.Count / 2;

            if (sortedList.Count % 2 == 0)
            {
                // Even number of items.
                return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
            }
            else
            {
                // Odd number of items.
                return sortedList[itemIndex];
            }
        }
    }

    extension(IEnumerable<int> source)
    {
        public double Median() =>
            (from number in source select (double)number).Median();
    }

    extension<T>(IEnumerable<T> source)
    {
        public double Median(Func<T, double> selector) =>
            (from num in source select selector(num)).Median();

        public IEnumerable<T> AlternateElements()
        {
            int index = 0;
            foreach (T element in source)
            {
                if (index % 2 == 0)
                {
                    yield return element;
                }

                index++;
            }
        }
    }
}

最後の拡張ブロックは、ジェネリック拡張ブロックを宣言します。 受信側の型パラメーターは、 extension 自体で宣言されます。

前の例では、各拡張ブロックで 1 つの拡張メンバーを宣言しています。 ほとんどの場合、同じ受信者に対して複数の拡張メンバーを作成します。 このような場合は、それらのメンバーの拡張機能を 1 つの拡張ブロックで宣言する必要があります。