【C#】LINQ to Objectsに対応した拡張メソッドを定義する

C#のLINQは非常に強力ですが、標準メソッドだけではプロジェクト固有の要件を満たせない場合があります。そのような場合、IEnumerable<T>インターフェースに対する拡張メソッドを定義することで、標準のLINQメソッド(WhereSelectなど)と同じ感覚で呼び出せる独自の処理を追加できます。

ここでは、標準のContainsメソッドを拡張し、条件式(述語)を受け取れるようにする実装例を紹介します。なお、標準LINQには同様の機能を持つAnyメソッドが存在しますが、学習およびメソッド名の統一性の観点からContainsのオーバーロードとして実装します。


目次

拡張メソッドの実装ルール

LINQ to Objects(IEnumerable<T>)を拡張する場合、以下のポイントを押さえる必要があります。

  1. 汎用性: ジェネリック(<T>)を使用し、あらゆる型のコレクションに対応させる。
  2. 引数: 第1引数に this IEnumerable<T> source を指定する。
  3. 条件式: 判定ロジックを受け取るために Func<T, bool>(または Predicate<T>)を使用する。

実践的なコード例:条件付きContainsの実装

以下のコードは、指定された条件を満たす要素が含まれているかを判定する拡張メソッドです。

using System;
using System.Collections.Generic;

namespace CustomLinqExtensions
{
    class Program
    {
        static void Main()
        {
            // 文字列の配列(データソース)
            var drinks = new[]
            {
                "wine", "sake", "beer", "whisky", "liqueur", "cocktail", "champagne"
            };

            // 自作したContains拡張メソッドを使用
            // 条件: 文字数が8文字以上の要素が含まれているか?
            bool hasLongNameDrink = drinks.Contains(x => x.Length >= 8);
            Console.WriteLine($"8文字以上の飲み物はある? : {hasLongNameDrink}"); // True (champagne)


            // 整数の配列
            var nums = new[] { 1, 3, 5, 7, 9, 11 };

            // 条件: 偶数が含まれているか?
            bool hasEvenNumber = nums.Contains(x => x % 2 == 0);
            Console.WriteLine($"偶数はある? : {hasEvenNumber}"); // False
        }
    }

    // 拡張メソッドを定義する静的クラス
    public static class EnumerableExtensions
    {
        /// <summary>
        /// 指定された条件を満たす要素がシーケンスに含まれているかどうかを判断します。
        /// (標準の Any() と同等の機能ですが、Containsという名前で定義しています)
        /// </summary>
        /// <typeparam name="T">要素の型</typeparam>
        /// <param name="source">対象のシーケンス</param>
        /// <param name="predicate">条件をテストする関数</param>
        /// <returns>条件を満たす要素が1つでもあれば true、なければ false</returns>
        public static bool Contains<T>(this IEnumerable<T> source, Func<T, bool> predicate)
        {
            // 引数のnullチェック(堅牢な実装のため)
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (predicate == null) throw new ArgumentNullException(nameof(predicate));

            foreach (var element in source)
            {
                // 条件関数を実行し、trueであれば即座にtrueを返して終了
                if (predicate(element))
                {
                    return true;
                }
            }

            // ループをすべて回っても見つからなかった場合はfalse
            return false;
        }
    }
}

実行結果

8文字以上の飲み物はある? : True
偶数はある? : False

技術的なポイントと修正点

1. Func<T, bool> の採用

入力されたコードでは Predicate<T> が使用されていましたが、現在のLINQ標準ライブラリの慣習に合わせ、Func<T, bool> を使用するのが一般的です。どちらも「Tを受け取りboolを返す」という意味では同じですが、WhereSelectなどの標準メソッドとの親和性が高いのは Func です。

2. ループ処理のロジック修正

入力されたコードには論理的な誤りがありました。

// 【誤】これだと1つ目の要素が不一致なら即座にfalseを返してしまい、2つ目以降がチェックされません。
foreach (var element in source) {
    if (predicate(element)) return true;
    else return false; 
}

正しいロジックは、「ループ内で一致したら即座に true」「ループを最後まで回りきったら false」という構造です。

3. thisキーワードによる拡張

メソッド定義の第1引数に this を付けることで、あたかも source オブジェクトのインスタンスメソッドであるかのように source.Contains(...) と記述できます。これにより、コードの可読性が大幅に向上します。

まとめ

IEnumerable<T> を拡張することで、標準ライブラリにはない独自の検索ロジックや変換処理を、LINQの一部として自然に組み込むことができます。プロジェクト内で頻出するループ処理がある場合は、このように拡張メソッド化して再利用性を高めることを検討してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次