xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

ローカル関数の使いどころ - LINQ 拡張 -

C# 7 で搭載予定のローカル関数。その詳細は以下を参照いただくとして、どういうケースで使えそうかと考えていたら「あ、そうか LINQ だ!」と思ったので紹介します。

yield (遅延評価) の罠

と書くだけで察しの良い方はお気付きかと思いますが、yield を含むメソッドはその機能ゆえに若干クセがあります。以下をご覧いただければ分かるように、例外が飛ぶタイミングがメソッド呼び出し時ではありません

public static void Main()
{
    var xs = CreateEnumerable();  //--- 遅延評価されるのでここでは例外が飛ばない
    var x  = xs.First();  //--- ここで初めて最初の yield までが評価されて例外が飛ぶ
}

public static IEnumerable<int> CreateEnumerable()
{
    throw new Exception("m9(^Д^)");
    yield return 123;
}

これまでのオレオレ LINQ 拡張での書き方

通常、メソッドの実装をする際には引数チェックを行います。LINQ のオレオレ拡張を作っている際も例外ではないので、何も考えなければ以下のようなコードを書くでしょう。

public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    if (source == null)   throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));

    foreach (var x in source)
        yield return selector(x);
}

しかしこの書き方は先の遅延評価の罠に引っかかり、実際の評価時まで引数チェックが働きません。引数チェックだけはメソッド呼び出し時に実行されてほしいので、以下のようにメソッドを分離して実装するのが定石になっていました。

public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    //--- 引数チェックだけするメソッド
    if (source == null)   throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));
    return source.MySelectCore(selector);
}

private static IEnumerable<TResult> MySelectCore<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    //--- yield だけするメソッド
    foreach (var x in source)
        yield return selector(x);
}

オレオレ LINQ 拡張へのローカル関数の適用

C# 7 で導入されるローカル関数は yield を使うことができるため、以下のようにメソッドを分離する必要がなくなります。非常にスッキリ!

public static IEnumerable<TResult> MySelect<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
{
    //--- 引数チェック
    if (source == null)   throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));

    //--- ローカル関数の定義
    //--- パフォーマンス改善がなされたクロージャで引数を渡しつつ...
    IEnumerable<TResult> core()
    {
        foreach (var x in source)
            yield return selector(x);
    }

    //--- 実行!!
    return core();
}