C#
.NET

C#でLINQライクなコレクションメソッドを書くとき、C# 8の静的ローカル関数と変数のシャドーイングが嬉しい

C# 7からローカル関数が使えるようになりました。ローカル関数の使いどころの一つは、LINQライクなコレクションメソッドを書く時です。

遅延実行のLINQライクメソッドを実装する際、適切に引数のnullチェック・nullだった場合の例外スローをする必要があります。それを実現するために、ローカル関数が登場するまでは、「nullチェック」と「メインの処理」を別のメソッドに分ける必要がありました。(詳しくは、「【ちゃんと投げよう】あなたの作ったオレオレLINQメソッドは間違ってるかもしれない!【ArgumentNullException】」を参照してください。)

しかし、ローカル関数の登場で「メインの処理」をローカル関数として定義することで、単一のメソッドで遅延実行のLINQライクメソッドを実装することができるようになりました。


そんな便利なローカル関数ですが、C# 8からもっと便利になります。


  • 静的ローカル関数

  • 変数シャドーイング

が導入されるからです。

特に、LINQライクなコレクションメソッドを書くとき、これがとても嬉しいです。

次に示すLINQライクメソッド、Scanのコードを使ってその理由を説明します。

public static IEnumerable<TSource> Scan<TSource>(

this IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}

if (accumulator == null)
{
throw new ArgumentNullException(nameof(accumulator));
}

return Impl();

IEnumerable<TSource> Impl()
{
var hasSeed = false;
var acc = default(TSource);

foreach (var item in source)
{
if (!hasSeed)
{
hasSeed = true;
acc = item;
continue;
}

acc = accumulator(acc, item);
yield return acc;
}
}
}


まずは、「静的ローカル関数」のメリットを紹介します。

先ほどのScanのコードにおいて、ローカル関数Implは外部の変数であるsourceaccumulatorをキャプチャしています。できれば、キャプチャは避けたいです。

そこでC# 8から導入された「静的ローカル関数」を使ってみます。設定をC# 8にしてローカル関数Implにstaticをつけてみましょう。

// C# 7では、静的ローカル関数に対応していないのでコンパイルエラー 

// C# 8でも、次のコードはコンパイルエラーになる
public static IEnumerable<TSource> Scan<TSource>(
this IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
/* 略 */

return Impl();

static IEnumerable<TSource> Impl()
{
var hasSeed = false;
var acc = default(TSource);

foreach (var item in source) // sourceが原因
{
if (!hasSeed)
{
hasSeed = true;
acc = item;
continue;
}

acc = accumulator(acc, item); // accumulatorも原因
yield return acc;
}
}
}

Implは静的ローカル関数なのに、外部変数である、sourceaccumulatorをキャプチャしているのでコンパイルエラーとなります。

これを解決するために、変数をキャプチャしないように、引数としてsourceaccumulatorを渡すようにコードを変更します。

// C# 7では、静的ローカル関数に対応していないのでコンパイルエラー 

public static IEnumerable<TSource> Scan<TSource>(
this IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
/* 略 */

return Impl(source, accumulator);

// 引数として、sourceとaccumulatorを渡す
static IEnumerable<TSource> Impl(IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
var hasSeed = false;
var acc = default(TSource);

foreach (var item in source)
{
if (!hasSeed)
{
hasSeed = true;
acc = item;
continue;
}

acc = accumulator(acc, item);
yield return acc;
}
}
}

このように静的ローカル関数にすることで、外部変数のキャプチャに気づくことができます。うっかり外部変数をキャプチャすることを防ぐことができます。


次に「変数シャドーイング」のメリットを紹介します。

C# 8の話をする前に、C# 7で起きていたことを整理しましょう。

最初に見せたコードでは、「外部変数のキャプチャが起きている」という説明をしました。

「C# 7では静的ローカル関数は使えないけれど、C# 7でも引数として変数を渡せばよいのではないか?」と思った方もいるのではないでしょうか?できなくはないのですが、ちょっと面倒です。次のコードはコンパイルエラーになります。

// C# 7では、名前が衝突してコンパイルエラー 

public static IEnumerable<TSource> Scan<TSource>(
this IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
/* 略 */

return Impl(source, accumulator);

// コンパイルエラー
// sourceとaccumulator名前はもう使われているから
IEnumerable<TSource> Impl(IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
var hasSeed = false;
var acc = default(TSource);

foreach (var item in source)
{
if (!hasSeed)
{
hasSeed = true;
acc = item;
continue;
}

acc = accumulator(acc, item);
yield return acc;
}
}
}

C#7では、ローカル関数内で、外側にすでに存在している変数や引数と同じ名前をつけることはできません。そのため、次のように引数名を変える必要があります。

// C# 7でもC# 8でもコンパイルが通る

public static IEnumerable<TSource> Scan<TSource>(
this IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
/* 略 */

return Impl(source, accumulator);

// コンパイルエラーを回避
// sourceとaccumulator名前はもう使われているから、sourceImplとaccumulatorImplという名前を使う
IEnumerable<TSource> Impl(IEnumerable<TSource> sourceImpl, Func<TSource, TSource, TSource> accumulatorImpl)
{
var hasSeed = false;
var acc = default(TSource);

foreach (var item in sourceImpl)
{
if (!hasSeed)
{
hasSeed = true;
acc = item;
continue;
}

acc = accumulatorImpl(acc, item);
yield return acc;
}
}
}

これはちょっと面倒です

さて、C# 8で導入される「変数シャドーイング」の話をしましょう。

C# 8では、「変数のシャドーイング」が認められ、ローカル関数内で外側にすでに存在している変数や引数と同じ名前で、新たに変数・引数を定義できるようになりました。

これにより、C# 7ではコンパイルエラーになる次のコードが、C# 8ではコンパイルが通るようになりました。

// C# 7では、名前が衝突してコンパイルエラー 

// C# 8では、「変数のシャドーイング」が認められ、コンパイルが通る
public static IEnumerable<TSource> Scan<TSource>(
this IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
/* 略 */

return Impl(source, accumulator);

// C#8では、変数のシャドーイングが認められて、sourceとaccumulatorという名前でもOK
IEnumerable<TSource> Impl(IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
var hasSeed = false;
var acc = default(TSource);

foreach (var item in source)
{
if (!hasSeed)
{
hasSeed = true;
acc = item;
continue;
}

acc = accumulator(acc, item);
yield return acc;
}
}
}


このようにC# 8では、


  • 静的ローカル関数

  • 変数シャドーイング

が導入され、LINQライクなコレクションメソッドを書く時に、さらにローカル関数が便利になりました。

C# 8では、次のようなコードをかくことができます。

public static IEnumerable<TSource> Scan<TSource>(

this IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
/* 略 */

return Impl(source, accumulator);

static IEnumerable<TSource> Impl(IEnumerable<TSource> source, Func<TSource, TSource, TSource> accumulator)
{
var hasSeed = false;
var acc = default(TSource);

foreach (var item in source)
{
if (!hasSeed)
{
hasSeed = true;
acc = item;
continue;
}

acc = accumulator(acc, item);
yield return acc;
}
}
}



  • 静的ローカル関数

  • 変数シャドーイング

について、さらに詳しい日本語情報は@ufcppさんの「ローカル関数と匿名関数」を参照してください。