C# はバージョンを重ねるごとにメソッド関連の記述を改善する機能を追加してきました。本記事では Expression-Bodied メンバ(C# 6.0 〜)、ローカル関数(C# 7.0)、static ローカル関数(C# 8.0)の 3 つを整理します。
Expression-Bodied メンバ(C# 6.0 〜)
C# 6.0 で、メソッドや読み取り専用プロパティを => を使って 1 行で書ける Expression-Bodied 構文が導入されました。
// 従来の書き方
public int Add(int x, int y)
{
return x + y;
}
// Expression-Bodied 構文(C# 6.0)
public int Add(int x, int y) => x + y;
return とブレースが不要になり、単純なメソッドをコンパクトに書けます。
対応範囲の拡張
C# 6.0 ではメソッドと読み取り専用プロパティのみに対応していましたが、C# 7.0 でコンストラクタ・デストラクタ・プロパティの get/set アクセサー・インデクサーにも拡張されました。
class Point
{
public int X { get; }
public int Y { get; }
// コンストラクタ(C# 7.0)
public Point(int x, int y) => (X, Y) = (x, y);
// デストラクタ(C# 7.0)
~Point() => Console.WriteLine("破棄されました");
// プロパティ get/set(C# 7.0)
private int _value;
public int Value
{
get => _value;
set => _value = value;
}
// 読み取り専用プロパティ(C# 6.0)
public string Label => $"({X}, {Y})";
// メソッド(C# 6.0)
public double Distance() => Math.Sqrt(X * X + Y * Y);
}
void メソッドへの適用
戻り値が void のメソッドにも使えます。
public void Print() => Console.WriteLine($"({X}, {Y})");
static メソッドへの適用
static メソッドでも同様に書けます。
public static int Max(int a, int b) => a > b ? a : b;
ローカル関数(C# 7.0)
C# 7.0 で、メソッドの中にメソッドを定義するローカル関数が導入されました。
public int Factorial(int n)
{
return Calc(n);
// ローカル関数(メソッド内に定義)
int Calc(int x) => x <= 1 ? 1 : x * Calc(x - 1);
}
Calc は Factorial の中でのみ有効であり、外部からは呼び出せません。
ローカル関数の特徴
外側のスコープの変数にアクセスできる
public void Process(int[] data)
{
int threshold = 10; // 外側の変数
var result = data.Where(IsValid).ToArray();
bool IsValid(int value) => value > threshold; // threshold を参照できる
}
引数検証と遅延実行の分離
イテレータや async メソッドは実行が遅延するため、引数の検証が即時に行われません。ローカル関数を使うとこれを解決できます。
// 公開メソッドで即時検証、ローカル関数でイテレータを実装
public IEnumerable<int> GetRange(int start, int count)
{
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
return Iterator();
IEnumerable<int> Iterator()
{
for (int i = 0; i < count; i++)
yield return start + i;
}
}
再帰も書ける
public int Fibonacci(int n)
{
return Fib(n);
int Fib(int x) => x <= 1 ? x : Fib(x - 1) + Fib(x - 2);
}
Expression-Bodied との組み合わせ
ローカル関数にも Expression-Bodied 構文が使えます。
int Double(int x) => x * 2;
static ローカル関数(C# 8.0)
C# 8.0 では、ローカル関数に static 修飾子を付けられるようになりました。static ローカル関数は外側のスコープの変数やインスタンスメンバを参照できないことをコンパイラが保証します。
public int Process(int input)
{
int factor = 3;
return Compute(input);
static int Compute(int x) => x * 2; // factor は参照不可
}
Compute 内で factor を参照しようとするとコンパイルエラーになります。
static ローカル関数のメリット
意図の明示
「この関数は外側の状態に依存しない」という意図をコードで表現できます。
クロージャの回避
通常のローカル関数が外側の変数をキャプチャすると、コンパイラは内部的にクロージャオブジェクトを生成します。static をつけることでキャプチャを禁止し、クロージャによるヒープ割り当てを防げます。
パラメータで必要な値を受け取る
外側の変数を使わない代わりに、必要な値は引数として渡します。
public double CalculateTotal(IEnumerable<decimal> prices, decimal taxRate)
{
return prices.Sum(p => ApplyTax(p, taxRate));
static decimal ApplyTax(decimal price, decimal rate) => price * (1 + rate);
}
通常のローカル関数 vs static ローカル関数
| ローカル関数 | static ローカル関数 | |
|---|---|---|
| 導入バージョン | C# 7.0 | C# 8.0 |
| 外側の変数への参照 | できる | できない |
this への参照 |
できる | できない |
| クロージャの生成 | キャプチャ時に発生 | 発生しない |
| 意図の明示 | 依存関係が不明 | 純粋な関数であることが明確 |
まとめ
| 機能 | 導入 | 概要 |
|---|---|---|
| Expression-Bodied メンバ(メソッド・プロパティ) | C# 6.0 | => で 1 行表記 |
| Expression-Bodied メンバ(コンストラクタ等) | C# 7.0 | 対応範囲を拡張 |
| ローカル関数 | C# 7.0 | メソッド内に関数を定義、外側の変数を参照可 |
| static ローカル関数 | C# 8.0 | 外側の変数を参照しないことを保証 |
これらを適切に組み合わせることで、短く・読みやすく・意図が明確なコードを書けます。