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 で公開しています。