共用方式為


Lambda 和方法群組的可選參數和參數陣列

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異已在相關的 語言設計會議(LDM)記錄中擷取,見

您可以在 規格一文中瞭解將功能規範納入 C# 語言標準的過程

冠軍問題:https://github.com/dotnet/csharplang/issues/6051

總結

為了在 C# 10 中引入的 Lambda 改進基礎上進一步發展(請參閱 相關的背景),我們建議增加對 Lambda 中預設參數值和 params 陣列的支援。 這可讓用戶實作下列 Lambda:

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 中的 Lambda 函數改善

方法群組轉換規範 §10.8

動機

.NET 生態系統中的應用程式架構會大量運用 Lambda,讓使用者快速撰寫與端點相關聯的商業規則。

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);
});

Lambda 目前不支援為參數設定預設值,因此,如果開發人員想要建置面對使用者不提供數據的情況仍能正常運作的應用程式,他們必須使用本機函式或在 Lambda 主體內設定預設值,而非採用原本更簡潔的建議語法。

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);
});

建議的語法具有減少 lambda 與本機函數間混淆差異的優點,使得將 lambda 轉換為函數的過程更加容易,進而不影響其特性。這對於 lambda 被用在需要方法組做為參考的 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 參數實作 Lambda 時,編譯程式會引發錯誤。

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 可以套用至具有下列行為的 Lambda 參數:

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 之前,方法群組的推斷類型是 ActionFunc,所以下列程式碼能夠編譯:

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 表達式的文法進行下列變更。

 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?
   ;

請注意,這隻允許 Lambda 的預設參數值和 params 陣列,不適用於使用 delegate { } 語法宣告的匿名方法。

與方法參數的規則相同(•15.6.2) 適用於 Lambda 參數:

  • 具有 refoutthis 修飾詞的參數不能有 預設參數
  • parameter_array 可能會在選擇性參數之後出現,但不能有預設值,而 parameter_array 的自變數省略則會導致建立空陣列。

方法群組不需要變更文法,因為此提案只會變更其語意。

匿名函式轉換需要下列加法 (以粗體表示) (\10.7):

具體而言,匿名函式 F 與提供的委派類型相容 D

  • [...]
  • 如果 F 有明確類型的參數清單,D 中的每個參數都有與對應參數相同的類型和修飾詞,F忽略 params 修飾詞和預設值

先前提案的更新

在先前提案中,函式類型 規格需要下列加法(粗體):

如果方法 群組中的所有候選方法 包括預設值和 params 修飾詞,則 方法群組具有自然類型。 (如果方法群組可能包含擴充方法,則候選專案會包含包含類型和所有擴充方法範圍。

匿名函式表示式或方法群組的自然類型是 function_typefunction_type 代表方法簽章:參數類型、預設值、ref 類型、params 修飾詞,以及傳回型別和 ref 種類。 具有相同簽章的匿名函式運算式或方法組,其 function_type也相同。

在先前提案中的 委派類型 規格,需要新增以下加粗的內容:

匿名函式或方法群組的委派類型,其參數類型 P1, ..., Pn 且傳回類型 R 為:

  • 如果任何參數或傳回值不是依值、或任何參數為選擇性或 params,或有超過 16 個參數,或任何參數類型或傳回不是有效的類型自變數(例如,(int* p) => { }),則委派是具有符合匿名函式或方法群組簽章的合成 internal 匿名委派類型, 如果單一參數,則具有參數名稱 arg1, ..., argnarg;[...]

系結器變更

合成新的委派類型

如同具有 refout 參數的委派行為,委派類型會針對使用選擇性或 params 參數定義的 Lambda 或方法群組進行合成。 請注意,在下列範例中,表示法 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 修飾詞在轉換中不同時,如果來源的預設值或修飾詞位於 Lambda 運算式中,則不會被使用,因為 Lambda 無法以任何其他方式被呼叫。 這似乎與使用者相反,因此當來源預設值或 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 本質上與針對具有 refout 參數之 Lambda 發出的 IL 非常類似。 將產生繼承自 System.Delegate 或類似項目的類別,Invoke 方法會包含 .param 指示詞來設定預設參數值或 System.ParamArrayAttribute ,就像具有選擇性或 params 參數的標準具名委派的情況一樣。

這些委派類型可以在執行時正常地進行檢查。 在程式代碼中,使用者可以使用相關聯的 MethodInfo來反省與 Lambda 或方法群組相關聯的 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 屬性互動?

建議的答案: 為了保持一致性,允許在 Lambda 表達式上使用 DefaultParameterValue 屬性,並確保委派生成的行為符合通過語法支援的預設參數值。

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

公開問題: 首先,請注意,這不在目前提案的範圍內,但未來可能值得討論。 我們想要支援具有隱含類型 Lambda 參數的預設值嗎? 亦即

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 = 一詞不可能是 Lambda 表達式的開頭。 如果 lambda 預設值允許此語法,剖析器將需要更大的前瞻範圍(一直掃描到 => 令牌),才能判斷字詞是否為 lambda。

設計會議

  • LDM 2022-10-10:決定以與預設參數值相同的方式新增對 params 的支援。