共用方式為


Lambda 改善

備註

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

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

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

Champion 期數:https://github.com/dotnet/csharplang/issues/4934

總結

建議的變更:

  1. 允許具有屬性的 Lambda
  2. 允許具有明確傳回類型的 Lambda
  3. 推斷 Lambda 和方法群組的自然委派類型

動機

支援 Lambda 上的屬性可提供與方法和區域函式的同位。

支持明確傳回型別會提供對稱與可指定明確型別的 Lambda 參數。 允許明確傳回類型也會在巢狀 Lambda 中提供編譯程式效能的控制,其中多載解析目前必須系結 Lambda 主體以判斷簽章。

Lambda 運算式和方法群組的自然類型將允許更多案例,其中 Lambda 和方法群組可以不使用明確的委派類型,包括宣告中的 var 初始化表達式。

需要 Lambda 和方法群組的明確委派類型是客戶的摩擦點,並且已成為在 MapAction 上最近工作 ASP.NET 進展的障礙。

ASP.NET MapAction 沒有建議的變更(MapAction() 採用自 System.Delegate 變數):

[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction((Func<Todo>)GetTodo);

[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction((Func<Todo, Todo>)PostTodo);

使用方法群組的自然類型 ASP.NET MapAction

[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);

[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo;
app.MapAction(PostTodo);

ASP.NET 具有 Lambda 運算式屬性和自然類型的 MapAction

app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);

屬性

屬性可以新增至 Lambda 運算式和 Lambda 參數。 若要避免方法屬性和參數屬性之間的模棱兩可,具有屬性的 Lambda 運算式必須使用括號化參數清單。 不需要參數類型。

f = [A] () => { };        // [A] lambda
f = [return:A] x => x;    // syntax error at '=>'
f = [return:A] (x) => x;  // [A] lambda
f = [A] static x => x;    // syntax error at '=>'

f = ([A] x) => x;         // [A] x
f = ([A] ref int x) => x; // [A] x

您可以指定多個屬性,在相同的屬性清單中以逗號分隔,或指定為個別的屬性清單。

var f = [A1, A2][A3] () => { };    // ok
var g = ([A1][A2, A3] int x) => x; // ok

使用語法宣告的delegate { }屬性。

f = [A] delegate { return 1; };         // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['

剖析器會進一步探討如何區分集合初始化表達式與具有 Lambda 運算式之集合初始化表達式的專案指派。

var y = new C { [A] = x };    // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x

剖析器將被視為 ?[ 條件式專案存取的開頭。

x = b ? [A];               // ok
y = b ? [A] () => { } : z; // syntax error at '('

Lambda 表達式或 Lambda 參數上的屬性將會發出至對應至 Lambda 之方法上的元數據。

一般而言,客戶不應取決於 Lambda 運算式和本機函式如何從來源對應至元數據。 Lambda 和本機函式的發出方式可以在編譯程式版本之間變更。

此處提議的 Delegate 變更是以驅動案例為目標。 檢查 MethodInfoDelegate 實例相關聯的 ,以判斷 Lambda 運算式或本機函式的簽章,包括編譯程式發出的任何明確屬性和其他元數據,例如預設參數。 這可讓 ASP.NET 等小組提供與一般方法相同的 Lambda 和本機函式行為。

明確傳回類型

在括弧化參數清單之前,可以指定明確的傳回型別。

f = T () => default;                    // ok
f = short x => 1;                       // syntax error at '=>'
f = ref int (ref int x) => ref x;       // ok
f = static void (_) => { };             // ok
f = async async (async async) => async; // ok?

剖析器會進一步瞭解方法呼叫 T() 與 Lambda 運算式 T () => e

使用語法宣告的 delegate { } 匿名方法不支持明確傳回類型。

f = delegate int { return 1; };         // syntax error
f = delegate int (int x) { return x; }; // syntax error

方法類型推斷應該從明確的 Lambda 傳回型別進行確切推斷。

static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>

不允許從 Lambda 傳回型別到委派傳回型別的變異數轉換(符合參數類型的類似行為)。

Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x;   // warning

剖析器允許在表達式內傳 ref 回型別的 Lambda 表達式,而不需要其他括弧。

d = ref int () => x; // d = (ref int () => x)
F(ref int () => x);  // F((ref int () => x))

var 不能做為 Lambda 表達式的明確傳回型別。

class var { }

d = var (var v) => v;              // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = @var (var v) => v;             // ok
d = ref var (ref var v) => ref v;  // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = ref @var (ref var v) => ref v; // ok

自然 (函式) 類型

如果參數類型明確且傳回型別明確,或可以推斷傳回類型,則匿名函式表達式 (~12.6.19) (Lambda 表達式匿名方法)具有自然類型。

如果方法群組中的所有候選方法都有通用簽章,則方法群組具有自然類型。 (如果方法群組可能包含擴充方法,則候選專案會包含包含類型和所有擴充方法範圍。

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

Function_types僅適用於幾個特定內容:

  • 隱含和明確轉換
  • 方法類型推斷 (~12.6.3) 和最佳一般類型 (\12.6.3.15
  • var 初始化表達式

function_type只存在於編譯階段:function_types不會出現在來源或元數據中。

轉換次數

從function_typeF有隱含function_type轉換:

  • G F G
  • 的 或 System.MulticastDelegate 基類或介面 System.MulticastDelegate
  • System.Linq.Expressions.ExpressionSystem.Linq.Expressions.LambdaExpression

匿名函式表達式和方法群組已經從表達式轉換成委派類型和表達式樹狀結構類型(請參閱匿名函式轉換 •10.7 和方法群組轉換 •10.8)。 這些轉換足以轉換成強型別委派類型和表達式樹狀結構類型。 上述function_type轉換只會將型別的轉換新增至基底類型:System.MulticastDelegateSystem.Linq.Expressions.Expression等。

function_type以外的類型不會轉換至function_type function_types沒有明確的轉換,因為來源中無法參考function_types

轉換至 System.MulticastDelegate 或 基底類型或介面,可讓匿名函式或方法群組成為適當委派類型的實例。 轉換成 System.Linq.Expressions.Expression<TDelegate> 或 基底類型,可讓 Lambda 運算式成為具有適當委派類型的表達式樹狀結構。

Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => "";                // Expression<Func<string>>
object o = "".Clone;                    // Func<object>

Function_type轉換不是隱含或明確的標準轉換 :10.4,而且在判斷使用者定義轉換運算符是否適用於匿名函式或方法群組時,不會考慮轉換。 從使用者定義轉換 的評估 •10.5.3

若要讓轉換運算元適用,必須能夠執行從來源類型到運算符作數類型的標準轉換 (^10.4),而且必須能夠執行從運算符結果型別到目標型別的標準轉換。

class C
{
    public static implicit operator C(Delegate d) { ... }
}

C c;
c = () => 1;      // error: cannot convert lambda expression to type 'C'
c = (C)(() => 2); // error: cannot convert lambda expression to type 'C'

將方法群組 object隱含轉換成 的警告會回報,因為轉換有效,但可能無意中。

Random r = new Random();
object obj;
obj = r.NextDouble;         // warning: Converting method group to 'object'. Did you intend to invoke the method?
obj = (object)r.NextDouble; // ok

類型推斷

類型推斷的現有規則大多不會變更(請參閱 \12.6.3)。 不過 ,下面有幾個變更 類型推斷的特定階段。

第一階段

第一個階段 (~12.6.3.2) 允許匿名函式系結至 Ti ,即使 Ti 不是委派或表達式樹狀結構類型(例如限制為 System.Delegate 的型別參數)。

針對每個方法自變數 Ei

  • 如果 Ei 是匿名函式,而且Ti是委派型別或表達式樹狀結構類型則會從 進行EiTi,而明確傳回型別推斷則是從 Ei 到 。Ti
  • 否則,如果 Ei 具有 型U別且 xi 是 value 參數,則會將下限推斷UTi
  • 否則,如果 Ei 具有 型U別且 xirefout 參數,則會Ti
  • 否則,不會針對這個參數進行推斷。

明確傳回型別推斷

明確傳回型別推斷是透過下列方式從表達式ET別:

  • 如果 E 是具有明確傳回類型的Ur匿名函式,而且T是具有傳回型Vr別的委派型別或表達式樹狀結構類型,則會Ur確切推斷Vr

修正

修正 (~12.6.3.12) 可確保其他轉換優先於 function_type 轉換。 (Lambda 運算式和方法群組表達式只有助於下限,因此只有下限才需要處理 function_types

具有一組界限 的未固定 類型變數 Xi修正 ,如下所示:

  • 如果有任何類型不是函式類型,則候選型Uj會以一組Xi範圍來啟動,其中函式型別會在下限中忽略所有型別。
  • 接著,我們會檢查每個系結Xi:針對所有與 不相同U之類型的Xi確切系結UjU,都會從候選集合中移除。 針對所有不能從U隱含轉換而來的Xi型別,這些型別的Uj下限將從候選集移除。 針對所有型U別的Xi上限Uj,不會從候選集合中移除隱含轉換U
  • 如果其餘候選型別中有唯一類型UjV,從中隱含轉換成所有其他候選型別,則會Xi固定為 V
  • 否則,類型推斷會失敗。

最佳常見類型

最佳一般類型 (~12.6.3.15) 是以類型推斷來定義,因此上述類型推斷變更也適用於最佳通用類型。

var fs = new[] { (string s) => s.Length, (string s) => int.Parse(s) }; // Func<string, int>[]

var

具有函式類型的匿名函式和方法群組可以當做宣告中的 var 初始化運算式使用。

var f1 = () => default;           // error: cannot infer type
var f2 = x => x;                  // error: cannot infer type
var f3 = () => 1;                 // System.Func<int>
var f4 = string () => null;       // System.Func<string>
var f5 = delegate (object o) { }; // System.Action<object>

static void F1() { }
static void F1<T>(this T t) { }
static void F2(this string s) { }

var f6 = F1;    // error: multiple methods
var f7 = "".F1; // error: the delegate type could not be inferred
var f8 = F2;    // System.Action<string> 

函式類型不會用於指派以捨棄。

d = () => 0; // ok
_ = () => 1; // error

委派類型

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

  • 如果任何參數或傳回值不是依值,或有超過 16 個參數,或任何參數類型或傳回不是有效的類型自變數 (例如, (int* p) => { }),則委派是具有符合匿名函式或方法群組之簽章的合成 internal 匿名委派類型,以及參數名稱 arg1, ..., argnarg 單一參數;
  • 如果 Rvoid,則委派類型為 System.Action<P1, ..., Pn>;
  • 否則委派型態為 System.Func<P1, ..., Pn, R>

編譯程式可能會允許更多簽章在未來系結至 System.Action<>System.Func<> 型別(如果實例允許 ref struct 類型自變數)。

modopt() 在對應的委派類型中會忽略 方法群組簽章中的 或 modreq()

如果相同編譯中的兩個匿名函式或方法群組需要具有相同參數類型和修飾詞和相同傳回型別和修飾詞的合成委派類型,編譯程式會使用相同的合成委派類型。

重載解析

更好的函式成員 (≦12.6.4.3) 會更新為偏好成員,其中沒有任何轉換,也沒有涉及從 Lambda 運算式或方法群組推斷型別的型別自變數。

較佳的功能成員

...假設自變數清單 具有一組自變數表達式 和兩個適用的函式成員 ,並使用參數類型 定義為比 更好的函式成員

  1. 針對每個自變數,從 到的隱含轉換ExPx不是function_type_conversion,而且
    • Mp是非泛型方法,或是Mp具有型別參數{X1, X2, ..., Xp}的泛型方法,而且針對每個類型參數Xi,類型自變數是從表達式推斷,或是從function_type以外的類型推斷,
    • 對於至少一個自變數,從 隱含轉換ExQx為 function_type_conversion,或是Mq具有型別參數{Y1, Y2, ..., Yq}的泛型方法,而且至少針對一個類型參數Yi,類型自變數是從function_type推斷, 或
  2. 對於每個自變數,從 Ex 隱含轉換成 Qx,不比從 Ex 隱含轉換成 Px更好,而且對於至少一個自變數,從 Ex 轉換成 Px 比從 Ex 轉換成 Qx更好。

較佳的表達式轉換 (≦12.6.4.5) 會更新為偏好從 Lambda 運算式或方法群組推斷類型的轉換。

從表達式進行更好的轉換

假設有一個隱含轉換 C1,將表達式 E 轉換為類型 T1,還有一個隱含轉換 C2,將表達式 E 轉換為類型 T2,若以下條件成立,則 C1 是一個 C2

  1. C1 不是 function_type_conversion ,而且 C2function_type_conversion
  2. E 是一個非常數 interpolated_string_expressionC1 是一個 implicit_string_handler_conversionT1 是一個 applicable_interpolated_string_handler_type,而 C2 不是一個 implicit_string_handler_conversion,或者
  3. ET2 不完全相符,且至少保留下列其中一項:

語法

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

lambda_parameters
  : lambda_parameter
  | '(' (lambda_parameter (',' lambda_parameter)*)? ')'
  ;

lambda_parameter
  : identifier
  | attribute_list* modifier* type? identifier equals_value_clause?
  ;

開啟問題

Lambda 運算式參數是否應該支持預設值以取得完整性?

應該 System.Diagnostics.ConditionalAttribute 不允許在 Lambda 表達式上,因為有幾個案例可以有條件地使用 Lambda 表達式?

([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?

除了產生的委派類型之外,function_type是否應該從編譯程式 API 取得?

目前,推斷的委派類型會使用 System.Action<>System.Func<> 參數和傳回型別是有效的型別自變數 ,而且 沒有超過16個參數,如果預期 Action<>Func<> 類型遺失,則會報告錯誤。 相反地,編譯程式應該使用 System.Action<> 還是 System.Func<> 不論arity為何? 如果遺漏預期的類型,請合成委派類型,否則為 ?