次の方法で共有


ラムダとメソッドグループの省略可能なパラメーターおよびパラメーター配列

手記

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

概要

C# 10 で導入されたラムダの機能強化 (関連する背景参照) に基づいて構築するには、ラムダの既定のパラメーター値と 配列のサポートを追加することを提案します。 これにより、ユーザーは次のラムダを実装できます。

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6

var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3

同様に、メソッド グループに対して同じ種類の動作を許可します。

var addWithDefault = AddWithDefaultMethod;
addWithDefault(); // 3
addWithDefault(5); // 6

var counter = CountMethod;
counter(); // 0
counter(1, 2); // 2

int AddWithDefaultMethod(int addTo = 2) {
  return addTo + 1;
}
int CountMethod(params int[] xs) {
  return xs.Length;
}

関連する背景

C# 10 での ラムダの機能強化

メソッドグループ変換仕様 §10.8

モチベーション

.NET エコシステムのアプリ フレームワークでは、ラムダを頻繁に活用することで、ユーザーはエンドポイントに関連付けられているビジネス ロジックをすばやく記述できます。

var app = WebApplication.Create(args);

app.MapPost("/todos/{id}", (TodoService todoService, int id, string task) => {
  var todo = todoService.Create(id, task);
  return Results.Created(todo);
});

ラムダは現在、パラメーターの既定値の設定をサポートしていないため、ユーザーがデータを提供しなかったシナリオに対して回復性があるアプリケーションを構築する必要がある場合は、より簡潔に提案された構文ではなく、ローカル関数を使用するか、ラムダ本体内で既定値を設定する必要があります。

var app = WebApplication.Create(args);

app.MapPost("/todos/{id}", (TodoService todoService, int id, string task = "foo") => {
  var todo = todoService.Create(id, task);
  return Results.Created(todo);
});

また、提案された構文には、ラムダとローカル関数の間の混乱を招く違いを減らすという利点があり、特にメソッド グループを参照として提供できる API でラムダが使用される他のシナリオでは、機能を損なうことなく、コンストラクトと関数へのラムダの "スケールアップ" を簡単に推論できます。 これは、前述のユース ケース シナリオではカバーされていない params 配列をサポートするための主な動機でもあります。

例えば:

var app = WebApplication.Create(args);

Result TodoHandler(TodoService todoService, int id, string task = "foo") {
  var todo = todoService.Create(id, task);
  return Results.Created(todo);
}

app.MapPost("/todos/{id}", TodoHandler);

以前の動作

C# 12 より前では、ユーザーが省略可能なパラメーターまたは params パラメーターを持つラムダを実装すると、コンパイラによってエラーが発生します。

var addWithDefault = (int addTo = 2) => addTo + 1; // error CS1065: Default values are not valid in this context.
var counter = (params int[] xs) => xs.Length; // error CS1670: params is not valid in this context

基になるメソッドに省略可能なパラメーターまたは params パラメーターがあるメソッド グループをユーザーが使用しようとすると、この情報は伝達されないため、予期される引数の数が一致しないため、メソッドの呼び出しは型チェックされません。

void M1(int i = 1) { }
var m1 = M1; // Infers Action<int>
m1(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int>'

void M2(params int[] xs) { }
var m2 = M2; // Infers Action<int[]>
m2(); // error CS7036: There is no argument given that corresponds to the required parameter 'obj' of 'Action<int[]>'

新しい動作

この提案 (C# 12 の一部) に従って、既定値と params は、次の動作でラムダ パラメーターに適用できます。

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6

var counter = (params int[] xs) => xs.Length;
counter(); // 0
counter(1, 2, 3); // 3

既定値と params は、次のようなメソッド グループを具体的に定義することで、メソッド グループのパラメーターに適用できます。

int AddWithDefault(int addTo = 2) {
  return addTo + 1;
}

var add1 = AddWithDefault; 
add1(); // ok, default parameter value will be used

int Counter(params int[] xs) {
  return xs.Length;
}

var counter1 = Counter;
counter1(1, 2, 3); // ok, `params` will be used

互換性に影響する変更点

C# 12 より前では、メソッド グループの推論された型が Action または Func されるため、次のコードがコンパイルされます。

void WriteInt(int i = 0) {
  Console.Write(i);
}

var writeInt = WriteInt; // Inferred as Action<int>
DoAction(writeInt, 3); // Ok, writeInt is an Action<int>

void DoAction(Action<int> a, int p) {
  a(p);
}

int Count(params int[] xs) {
  return xs.Length;
}
var counter = Count; // Inferred as Func<int[], int>
DoFunction(counter, 3); // Ok, counter is a Func<int[], int>

int DoFunction(Func<int[], int> f, int p) {
  return f(new[] { p });
}

この変更 (C# 12 の一部) の後、この性質のコードは .NET SDK 7.0.200 以降でコンパイルされなくなります。

void WriteInt(int i = 0) {
  Console.Write(i);
}

var writeInt = WriteInt; // Inferred as anonymous delegate type
DoAction(writeInt, 3); // Error, cannot convert from anonymous delegate type to Action

void DoAction(Action<int> a, int p) {
  a(p);
}

int Count(params int[] xs) {
  return xs.Length;
}
var counter = Count; // Inferred as anonymous delegate type
DoFunction(counter, 3); // Error, cannot convert from anonymous delegate type to Func

int DoFunction(Func<int[], int> f, int p) {
  return f(new[] { p });
}

この破壊的変更の影響を考慮する必要があります。 幸いにも、メソッド グループの型を推論する var の使用は C# 10 以降でのみサポートされているため、この動作に明示的に依存するコードのみが中断されます。

詳細な設計

文法とパーサーの変更

この機能強化では、ラムダ式の文法を次のように変更する必要があります。

 lambda_expression
   : modifier* identifier '=>' (block | expression)
-  | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
+  | attribute_list* modifier* type? lambda_parameter_list '=>' (block | expression)
   ;

+lambda_parameter_list
+  : lambda_parameters (',' parameter_array)?
+  | parameter_array
+  ;

 lambda_parameter
   : identifier
-  | attribute_list* modifier* type? identifier
+  | attribute_list* modifier* type? identifier default_argument?
   ;

これにより、既定のパラメーター値と params 配列はラムダに対してのみ許可され、delegate { } 構文で宣言された匿名メソッドでは許可されないことに注意してください。

ラムダ パラメーターには、メソッド パラメーター (§15.6.2) と同じ規則が適用されます。

  • refout、または this 修飾子を持つパラメーターに default_argumentを指定することはできません。
  • parameter_array は省略可能なパラメーターの後に発生する可能性がありますが、既定値を指定することはできません。parameter_array の引数を省略すると、代わりに空の配列が作成されます。

この提案ではセマンティクスのみが変更されるため、メソッド グループに文法を変更する必要はありません。

匿名関数の変換には、次の追加 (太字) が必要です (§10.7)。

具体的には、匿名関数 F は、提供 D デリゲート型と互換性があります。

  • [...]
  • F に明示的に型指定されたパラメーターリストがある場合、D の各パラメーターは、Fの対応するパラメーターと、params 修飾子と既定値を無視して、同じ型と修飾子を持ちます。

以前の提案の更新

前の提案では、関数型 仕様に次の追加 (太字) が必要です。

メソッド グループ には、メソッド グループ内のすべての候補メソッドに、既定値や 修飾子を含む共通のシグネチャ がある場合、自然な型があります。 (メソッド グループに拡張メソッドが含まれている可能性がある場合、候補には、含まれる型とすべての拡張メソッド スコープが含まれます)。

匿名関数の式またはメソッド グループの自然な型は、function_typeです。 function_type はメソッド シグネチャを表します。パラメーターの型、既定値 、ref の種類、params 修飾子、戻り値の型と ref の種類です。 同じシグネチャを持つ匿名関数式またはメソッド グループの function_typeは同じです。

前の提案では、デリゲート型 仕様に次の追加 (太字) が必要です。

パラメーター型が P1, ..., Pn され、戻り値の型が R 匿名関数またはメソッド グループのデリゲート型は次のとおりです。

  • パラメーターまたは戻り値が値渡しでなく、または任意のパラメーターが省略可能であるか、paramsであるか、パラメーターが16個を超えた場合、またはパラメーターの型や戻り値が有効な型引数でない場合 ((int* p) => { }など)、そのデリゲートは匿名関数やメソッドグループに一致するシグネチャを持つ、合成されたinternal匿名デリゲート型であり、パラメーター名は単一パラメーターの場合arg、それ以外の場合はarg1, ..., argnです。

バインダーの変更

新しいデリゲート型の合成

ref パラメーターまたは out パラメーターを持つデリゲートの動作と同様に、デリゲート型は、省略可能なパラメーターまたは params パラメーターで定義されたラムダまたはメソッド グループに対して合成されます。 以下の例では、これらの匿名デリゲート型を表すために、a'b'などの表記が使用されることに注意してください。

var addWithDefault = (int addTo = 2) => addTo + 1;
// internal delegate int a'(int arg = 2);
var printString = (string toPrint = "defaultString") => Console.WriteLine(toPrint);
// internal delegate void b'(string arg = "defaultString");
var counter = (params int[] xs) => xs.Length;
// internal delegate int c'(params int[] arg);
string PathJoin(string s1, string s2, string sep = "/") { return $"{s1}{sep}{s2}"; }
var joinFunc = PathJoin;
// internal delegate string d'(string arg1, string arg2, string arg3 = " ");

変換と統一の動作

省略可能なパラメーターを持つ匿名デリゲートは、パラメーター名に関係なく、同じパラメーターが (位置に基づいて) 同じ既定値を持つ場合に統合されます。

int E(int j = 13) {
  return 11;
}

int F(int k = 0) {
  return 3;
}

int G(int x = 13) {
  return 4;
}

var a = (int i = 13) => 1;
// internal delegate int b'(int arg = 13);
var b = (int i = 0) => 2;
// internal delegate int c'(int arg = 0);
var c = (int i = 13) => 3;
// internal delegate int b'(int arg = 13);
var d = (int c = 13) => 1;
// internal delegate int b'(int arg = 13);

var e = E;
// internal delegate int b'(int arg = 13);
var f = F;
// internal delegate int c'(int arg = 0);
var g = G;
// internal delegate int b'(int arg = 13);

a = b; // Not allowed
a = c; // Allowed
a = d; // Allowed
c = e; // Allowed
e = f; // Not Allowed
b = f; // Allowed
e = g; // Allowed

d = (int c = 10) => 2; // Warning: default parameter value is different between new lambda
                       // and synthesized delegate b'. We won't do implicit conversion

最後のパラメーターとして配列を持つ匿名デリゲートは、パラメーター名に関係なく、最後のパラメーターに同じ params 修飾子と配列型がある場合に統合されます。

int C(int[] xs) {
  return xs.Length;
}

int D(params int[] xs) {
  return xs.Length;
}

var a = (int[] xs) => xs.Length;
// internal delegate int a'(int[] xs);
var b = (params int[] xs) => xs.Length;
// internal delegate int b'(params int[] xs);

var c = C;
// internal delegate int a'(int[] xs);
var d = D;
// internal delegate int b'(params int[] xs);

a = b; // Not allowed
a = c; // Allowed
b = c; // Not allowed
b = d; // Allowed

c = (params int[] xs) => xs.Length; // Warning: different delegate types; no implicit conversion
d = (int[] xs) => xs.Length; // OK. `d` is `delegate int (params int[] arg)`

同様に、省略可能なパラメーターと params パラメーターを既にサポートしている名前付きデリゲートとの互換性は当然あります。 既定値または params 修飾子が変換で異なる場合、ラムダ式内にある場合、ソース修飾子は使用されません。ラムダは他の方法では呼び出すことができないためです。 これはユーザーには直感的とは思えない可能性があるため、ソースの既定値または params 修飾子が存在し、ターゲットの修飾子とは異なる場合に警告が生成されます。 ソースがメソッド グループの場合は、単独で呼び出すことができるため、警告は生成されません。

delegate int DelegateNoDefault(int x);
delegate int DelegateWithDefault(int x = 1);

int MethodNoDefault(int x) => x;
int MethodWithDefault(int x = 2) => x;
DelegateNoDefault d1 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d2 = MethodWithDefault; // no warning: source is a method group
DelegateWithDefault d3 = MethodNoDefault; // no warning: source is a method group
DelegateNoDefault d4 = (int x = 1) => x; // warning: source present, target missing
DelegateWithDefault d5 = (int x = 2) => x; // warning: source present, target different
DelegateWithDefault d6 = (int x) => x; // no warning: source missing, target present

delegate int DelegateNoParams(int[] xs);
delegate int DelegateWithParams(params int[] xs);

int MethodNoParams(int[] xs) => xs.Length;
int MethodWithParams(params int[] xs) => xs.Length;
DelegateNoParams d7 = MethodWithParams; // no warning: source is a method group
DelegateWithParams d8 = MethodNoParams; // no warning: source is a method group
DelegateNoParams d9 = (params int[] xs) => xs.Length; // warning: source present, target missing
DelegateWithParams d10 = (int[] xs) => xs.Length; // no warning: source missing, target present

IL/ランタイムの動作

既定のパラメーター値がメタデータに出力されます。 この機能の IL は、ref パラメーターと out パラメーターを持つラムダに対して出力される IL と本質的に非常に似ています。 System.Delegate または同様のクラスが生成され、省略可能なパラメーターまたは params パラメーターを持つ標準の名前付きデリゲートの場合と同様に、Invoke メソッドには既定のパラメーター値または System.ParamArrayAttribute を設定する .param ディレクティブが含まれます。

これらのデリゲート型は、通常どおり実行時に検査できます。 コードでは、関連付けられた MethodInfoを使用して、ラムダまたはメソッド グループに関連付けられた ParameterInfo 内の DefaultValue をユーザーがイントロスペクトできます。

var addWithDefault = (int addTo = 2) => addTo + 1;
int AddWithDefaultMethod(int addTo = 2)
{
    return addTo + 1;
}

var defaultParm = addWithDefault.Method.GetParameters()[0].DefaultValue; // 2

var add1 = AddWithDefaultMethod;
defaultParm = add1.Method.GetParameters()[0].DefaultValue; // 2

質問を開く

これらはどちらも実装されていません。 これらはオープンな提案のままです。

オープンな質問:、これは既存の DefaultParameterValue 属性とどのように対話しますか?

提案された回答: パリティの場合は、ラムダの DefaultParameterValue 属性を許可し、デリゲートの生成動作が構文でサポートされている既定のパラメーター値と一致することを確認します。

var a = (int i = 13) => 1;
// same as
var b = ([DefaultParameterValue(13)] int i) => 1;
b = a; // Allowed

オープンな質問: 最初に、これは現在の提案の範囲外ですが、将来的に議論する価値がある可能性があることに注意してください。 暗黙的に型指定されたラムダ パラメーターで既定値をサポートする必要がありますか? つまり、

delegate void M1(int i = 3);
M1 m = (x = 3) => x + x; // Ok

delegate void M2(long i = 2);
M2 m = (x = 3.0) => ...; //Error: cannot convert implicitly from long to double

この推論は、より多くの議論を必要とするいくつかのトリッキーな変換の問題につながります。

ここには、解析のパフォーマンスに関する考慮事項もあります。 たとえば、現在、(x = がラムダ式の先頭になることは決してありません。 ラムダの既定値に対してこの構文が許可されている場合、パーサーは用語がラムダであるかどうかを判断するために、より大きな先読み (=> トークンまでスキャン) が必要になります。

デザイン会議

  • LDM 2022-10-10: 既定のパラメーター値と同じ方法で params のサポートを追加することを決定します。