プログラミング言語C#において、ラムダ式はC# 3.0(2007年11月にリリース)で導入されました。C# 14(2025年11月にリリース)に至るまで、ラムダ式にはいくつかのアップデートが行われています。本記事では、そんなC#のラムダ式に関連するアップデートを紹介します。
本記事で紹介する内容は、2025年11月に正式リリースされたC# 14/.NET 10までの内容です。
静的なラムダ式(C# 9)
C# 9から、静的なラムダ式が使えるようになりました。英語では「static anonymous function」と表現されています。
ラムダ式と匿名メソッド式に static修飾子がつけられるようになりました。static修飾子がついているラムダ式・匿名メソッド式では、ローカル変数やインスタンスフィールドのキャプチャが禁止されます。
// 次のコードは、C# 9より前のコードではコンパイルエラー。
// C# 9以降ではコンパイルできる。
Func<int, int> f = static x => x + 1; // 外側の変数は参照不可
ラムダ式の中で、意図せずローカル変数やフィールドをキャプチャしてしまうことが、避けられるようになりました。
int num = LoadNumber();
// 意図せずnumにアクセスしてしまっている
// だが、staticをつけているためコンパイルエラーになり、気が付けた
Func<int, int> f = static x => x + num;
なお、staticとしてラムダ式を定義しても、staticメソッドとしてコンパイルされるとは限らない点に注意してください。
以下、公式ドキュメントより。
静的匿名関数定義がメタデータ内のstatic メソッドとして出力されるかどうかについては保証されません。 これは最適化するためにコンパイラの実装に任されています。
パラメーターの破棄(C# 9)
C# 9から、ラムダ式・匿名メソッド式においてパラメーターの破棄(ディスカード)が可能になりました。英語では「Lambda discard parameters」。公式ドキュメントでは「ラムダディスカードパラメーター」と表記されています。
次のコードでは、ラムダ式を使って「2つのint型の引数を受け取って、必ず0を返すデリゲート」を、生成しています。
// _0と_1は使っていない
// C# 9より前では、ラムダ式では引数に必ず名前を付けないといけなかった
Func<int, int, int> func = (_0, _1) => 0;
上のコードにおいて、パラメーター「_0」と「_1」は使っていません。C# 9より前では、ラムダ式では引数に必ず名前を付けないといけませんでした。使わないパラメーターにも、名前を付けなくてはいけなかったのです。
C# 9から、ラムダ式でパラメーターの破棄(ディスカード)が可能になりました。その結果、次のように使わないパラメーターを「_」と書けるようになりました。2つのパラメーターに同じ「_」というパラメーター名を使っていることに注目してください。
// C# 9から破棄(ディスカード)が使えるようになった
Func<int, int, int> func = (_, _) => 0;
「破棄パラメーターとして扱われるのは、_がパラメータとして複数回登場する場合だけ」な点に注意してください。1つのパラメーターが「_」な場合、普通のパラメーターとして扱われます。もともとC#では、「_」はパラメーターの名前として有効な名前でした。そのため、互換性の理由から破棄パラメーターではなく、通常のパラメーターとして扱われます。
次のコードは「破棄(ディスカード)」を使っていません。そして、コンパイルエラーにもなりません。
// 次のコードは、C# 9より前でもコンパイルできる
// なぜなら、パラメーター名として「_」をつけることができるから
Func<int, int> func0 = (_) => 0;
Func<int, int> func1 = (_) => _ + 1;
Func<int, int, int> func2 = (num, _) => num + _;
自然なデリゲート型を推論(C# 10)
C# 10より前では、デリゲートの型を明示しないといけない状況があり、明示的な型の指定は煩雑でした。これがC# 10で自然(Natural)なデリゲート型を推論してくれるようになり、簡潔な記述が可能になりました。
C# 10より前だと、次のコードはコンパイルエラーになります。デリゲート型が一意に決まらないからです。
// C# 10より前だと、これはコンパイルエラー
// squaredNumberFuncは、Func<int, int>っぽいし、それで良さそうだけれど、
// Func<int, int>だとは限らない、つまり一意に決まらない
var squaredNumberFunc = (int num) => num * num;
C# 10以降は、次のコードはコンパイルエラーになりません。自然なデリゲート型として、Func<int, int>型に推論されます。
// C# 10以降だと、コンパイルが通る
// squaredNumberFuncは自然な型としてFunc<int, int>になる
var squaredNumberFunc = (int num) => num * num;
もう少し具体的な例として「ASP.NET CoreのMapPostメソッド」を使って、今まで何が困っていて、これにより何が嬉しいのかを紹介します。
EndpointRouteBuilderExtensions.MapPost拡張メソッドは、第3パラメーターとして「System.Delegate」型を取ります。
public static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapPost(
this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler
);
C# 10より前では、次のコードはコンパイルエラーになりました。Delegateをパラメーターにとる箇所で、メソッドグループ「PostPhoto」から生成したデリゲートの型が、一意に決まらないためです。
// メソッドPostPhoto
Photo PostPhoto([FromBody] Photo photo) => photo;
// 本当はこう書きたいけれど、C# 10より前だとコンパイルエラー。
// Delegateを引数にとる箇所で、型が決まらないため。
app.MapPost("photos", PostPhoto);
C# 10より前では、次のコードでコンパイルエラーを解消するために、Func<Photo, Photo>のようにデリゲートの型を明示してあげる必要がありました。
// メソッドPostPhoto
Photo PostPhoto([FromBody] Photo photo) => photo;
// Delegateを引数にとる箇所で、
// デリゲート型がFunc<Photo, Photo>に一意に決まるため、コンパイルエラーにならない
app.MapPost("photos", Func<Photo, Photo> PostPhoto);
C# 10より自然(Natural)なデリゲートの型を推論してくれるようになり、先ほどのコードではFunc<Photo, Photo>のようにデリゲートの型を明示しなくてもよくなりました。
// メソッドPostPhoto
Photo PostPhoto([FromBody] Photo photo) => photo;
// C# 10以降なら、
// Delegateを引数にとる箇所でも、自然なデリゲート型を推論してくれるため、
// コンパイルエラーにならない
app.MapPost("photos", PostPhoto);
ラムダ式に属性の適用(C# 10)
C# 10から、ラムダ式に属性を指定できるようになりました。
C# 10より前では、ラムダ式に属性を指定することができませんでした。そのため、ラムダ式を使いたい場面でも、メソッドやローカル関数を使わないといけないことがありました。C# 10からは、ラムダ式に属性が指定できるようになり、わざわざメソッドやローカル関数を定義しなくてもよくなりました。
「ASP.NET Core」に「FromBodyAttribute」という属性があります。その属性のサンプルコードでは、次のように属性が指定されたラムダ式のコードが記述されています。
app.MapPost("/from-body", ([FromBody] Person person) => person);
public record Person(string Name, int Age);
app.MapPost("/allow-empty-body",
(
[Description("An optional request body")]
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] Body body
) =>
ラムダ式で戻り値型を明示的に指定できるように(C# 10)
C# 10のラムダ式のアップデートの1つです。
ラムダ式で、戻り値型を明示的に指定できるようになりました。
var squaredNumberFunc = int (int num) => num * num;
上のコードでは、わざわざ戻り値型を明示するメリットはありませんが、次のように戻り値型が推論できないコードでvarを使いたい場合、便利かもしれません。
record Enemy(bool IsBoss, bool IsEvent);
class Battle(Enemy enemy);
class BossBattle(Enemy enemy) : Battle(enemy);
class EventBattle(Enemy enemy) : Battle(enemy);
// 戻り値型として、Battleを明示的に指定しないとコンパイルエラーになる。
// デリゲートbattleConverterの型が、自然なデリゲート型でも推論できないため
var battleConverter = Battle (Enemy enemy) =>
{
if (enemy.IsEvent)
{
return new EventBattle(enemy);
}
else if (enemy.IsBoss)
{
return new BossBattle(enemy);
}
else
{
return null;
}
};
// ※ あくまで言語仕様説明のためのサンプルのコード
ラムダパラメーターに省略可能パラメーターや可変長パラメーターを指定できるように(C# 12)
C# 12から、省略可能な引数や可変長引数を指定できるようになりました。
次のコードでは、ラムダ式でデリゲートを作る際に、省略可能なパラメーターを利用しています。
// 省略可能なパラメーターを利用
var greeting = (string name = "Guest") => Console.WriteLine($"Hello, {name}!");
// パラメーターを渡して呼び出し
greeting("Ryota");
// パラメーターを省略して呼び出し
greeting();
次のコードでは、ラムダ式でデリゲートを作る際に、可変長パラメーターを利用しています。
// 可変長パラメーターを利用
var showNames = (params IEnumerable<string> names) =>
{
foreach (var name in names)
{
Console.WriteLine(name);
}
};
// 3個のパラメーターで呼び出し
showNames("Taro", "Jiro", "Saburo");
// 1個のパラメーターで呼び出し
showNames("Taro");
// 0個のパラメーターで呼び出し
showNames();
修飾子付きのパラメータを型名なしでシンプルに呼び出せるように(C# 14)
C# 14のアップデートで、ラムダ式のパラメーターにinやoutなどのパラメーター修飾子をつけても、パラメーターの型を指定せずにラムダ式を記述できるようになりました。
次のようなデリゲート型を例に紹介します。
delegate bool CanParse<T>(string text);
delegate bool TryParse<T>(string text, out T result);
C# 14より前では、outパラメーター修飾子を使っているため、最後のラムダ式を使ったparseInt1のコードではパラメーター型を指定する必要がありました。そのため、C# 14より前ではparseInt1のコードはコンパイルエラーになりました。
// 次のコードはOK、型を明示している
CanParse<int> canParseInt0 = (string text) => int.TryParse(text, out _);
// 次のコードはOK、outパラメーター修飾子を利用していなため、string型が省略できる
CanParse<int> canParseInt1 = (text) => int.TryParse(text, out _);
// 次のコードはOK、型を明示している
TryParse<int> parseInt0 = (string text, out int result) => int.TryParse(text, out result);
// 次のコードはコンパイルエラー
// outパラメーター修飾子を使っているため、stringとintの型が省略できない
// TryParse<int> parseInt1 = (text, out result) => int.TryParse(text, out result);
C# 14から、ラムダ式のパラメーターにinやoutなどのパラメーター修飾子をつけても、パラメーターの型を指定せずにラムダ式を記述できるようになりました。そのため、次のコードはコンパイルできるようになりました。
// C# 14以降ではコンパイルできるようになった。
// outパラメーター修飾子を使っていても、
// パラメーターの型を指定せずにラムダ式を記述できるようになったため。
TryParse<int> parseInt1 = (text, out result) => int.TryParse(text, out result);
ひとこと
10年前、次に示すラムダ式・デリゲートを紹介する記事を執筆し、嬉しい評価やコメントをいただく機会に恵まれました。
しかし、この10年間ラムダ式の機能はどんどん強化されているのにも関わらず、それらを紹介できていませんでした。本記事で、ラムダ式・デリゲートのアップデート・機能強化を紹介できてよかったです。