First メソッドを作ってみる

Deep Dive into LINQ #2

Posted on 2022-03-04
target: .NET 6

Deep Dive into LINQ シリーズ記事一覧

Part 1 - First メソッドの使い方

Part 2 - First メソッドを作ってみる

Part 3 - First メソッドの実装をざっくり読む

Part 4 - First メソッドのパフォーマンス特性

はじめに

この記事は 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 で公開しています。