Deep Dive into LINQ シリーズ記事一覧
はじめに
この記事は Deep Dive into LINQ シリーズの第2回目の記事です。
Deep Dive into LINQ シリーズでは、 LINQ の内部実装やパフォーマンス特性について基本的なことから調べていきます。
前回に引き続き、LINQ の First メソッドについて研究していきます。
今回は実際に自作の First メソッドを作ってみて、おおまかな処理の流れをつかみます。
First メソッドを作ってみる
条件なしの First メソッド
まず最もシンプルな、条件なしの First メソッドを作ってみます。
public static TSource MyFirst<TSource>(this IEnumerable<TSource> source)
これは次のような動作のメソッドです。
- シーケンスの一番最初の要素を返す
- シーケンスが空っぽだったら、 InvalidOperationExceptionを発生させる
IEnumerable<T> の要素を列挙するには、 GetEnumerator メソッドで IEnumerator<T> という型のインスタンスを作ります。
public interface IEnumerator<T>
{
    bool MoveNext();
    T Current { get; }
}
IEnumerator<T> はシーケンスの要素を一つづつ列挙するためのインターフェースです。
IEnumerator<T> は MoveNext というメソッドを持っており、これは「次の要素に移動する」というメソッドです。
現在の要素は Current プロパティで取得できます。
MoveNext は次の要素がある場合は true、そうでなければ false を返します。
これを使って First メソッドを実装してみると、次のようになります。
public static TSource MyFirst<TSource>(this IEnumerable<TSource> source)
{
    ArgumentNullException.ThrowIfNull(source);
    using var enumerator = source.GetEnumerator();
    // MoveNext できたら(つまり、最初の要素が存在したら)その要素を返す
    if (enumerator.MoveNext())
    {
        return enumerator.Current;
    }
    // MoveNext できなかったら(つまり、シーケンスが空だったら)例外を throw する
    throw new InvalidOperationException("シーケンスが空です");
}
まず、 enumerator.MoveNext() を呼びます。もしこれが true を返せば、シーケンスの最初の要素に移動できたということなので、 Current を返します。
もし false を返した場合、最初の要素がない、すなわちシーケンスが空であるということなので、 InvalidOperationException を throw します。
MoveNext あたりの処理は FirstOrDefault メソッドとも共通にできそうなので、 TryGetFirst というメソッドに切り出しておきます。
public static TSource MyFirst<TSource>(this IEnumerable<TSource> source)
{
    return source.TryGetFirst(out var first)
        ? first!
        : throw new InvalidOperationException("シーケンスが空です"); // 要素がなければ例外
}
// 最初の要素の取得を試み、取得できれば out 引数で結果を返します。
private static bool TryGetFirst<TSource>(this IEnumerable<TSource> source, out TSource? first)
{
    ArgumentNullException.ThrowIfNull(source);
    using var enumerator = source.GetEnumerator();
    // 要素が見つかれば返す
    if (enumerator.MoveNext())
    {
        first = enumerator.Current;
        return true;
    }
    // 要素がなければ false
    first = default;
    return false;
}
条件なしの FirstOrDefault メソッド
同じように FirstOrDefault メソッドも実装してみます。
First メソッドとほとんど同じ実装ですが、例外を throw するかどうかが異なります。
public static TSource? MyFirstOrDefault<TSource>(this IEnumerable<TSource> source) =>
    source.TryGetFirst(out var first) ? first : default;
// デフォルト値を指定できるメソッド
public static TSource? MyFirstOrDefault<TSource>(this IEnumerable<TSource> source, TSource? defaultValue) =>
    source.TryGetFirst(out var first) ? first : defaultValue;
条件付き First, FirstOrDefault メソッド
条件付きのものも同様に実装してみます。
これも TryGetFirst メソッドを先に作っておきます。
// 条件を満たす最初の要素の取得を試み、取得できれば out 引数で結果を返します。
private static bool TryGetFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, out TSource? first)
{
    ArgumentNullException.ThrowIfNull(source);
    ArgumentNullException.ThrowIfNull(predicate);
    // 条件を満たす要素が見つかれば返す
    foreach (var element in source)
    {
        if (predicate(element))
        {
            first = element;
            return true;
        }
    }
    // 条件を満たす要素がなければ false
    first = default;
    return false;
}
条件付きの First メソッドは、条件に一致する最初の要素を返すのでした。
そのため、条件に合う要素が見つかるまで順番に要素を検査しています。
この TryGetFirst メソッドを使って、 First メソッドと FirstOrDefault メソッドを次のように実装できます。
public static TSource MyFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    return source.TryGetFirst(predicate, out var first)
        ? first!
        : throw new InvalidOperationException("シーケンスが空です"); // 要素がなければ例外
}
public static TSource? MyFirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) =>
    source.TryGetFirst(predicate, out var first) ? first : default;
public static TSource? MyFirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource? defaultValue) =>
    source.TryGetFirst(predicate, out var first) ? first : defaultValue;
まとめ
今回は First メソッドを簡単に実装してみました。
次回は、実際に .NET 6 での First メソッドの実装を眺めてみて、最適化の方法を探ります。
今回のコードは GitHub で公開しています。