Lambda 改进

注释

本文是功能规格说明。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的 语言设计会议(LDM)记录中。

可以在有关 规范的文章中详细了解将功能规范采用 C# 语言标准的过程。

支持者问题: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 MapActionMapAction() 采用 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 驱动方案为目标。 检查与MethodInfo实例关联的对象,以确定 lambda 表达式或本地函数的签名,包括编译器发出的任何显式属性和附加元数据(如默认参数)是否有效Delegate。 这样,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

分析器允许在表达式中使用返回类型的 lambda 表达式 ref ,而无需附加括号。

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.19)(lambda 表达式匿名方法)具有自然类型,并且返回类型为显式或可推断(请参阅 §12.6.3.13)。

如果方法组中的所有候选方法都具有通用签名,则方法组具有自然类型。 (如果方法组可能包含扩展方法,则候选项包括包含类型和所有扩展方法范围。

匿名函数表达式或方法组的自然类型是 function_typefunction_type表示方法签名:参数类型和 ref 类型,以及返回类型和 ref 类型。 具有相同签名的匿名函数表达式或方法组具有相同的 function_type

Function_types仅在几个特定上下文中使用:

  • 隐式转换和显式转换
  • 方法类型推理(§12.6.3)和最佳常见类型(§12.6.3.15
  • var 初始值设定项

仅在编译时存在function_typefunction_types不会出现在源或元数据中。

转换

从function_typeF存在隐式function_type转换:

  • 对于function_typeG参数和返回类型的参数和返回类型F是否可转换为参数和返回类型G
  • System.MulticastDelegate 类或接口 System.MulticastDelegate
  • To 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推理,而Ti
  • 否则,如果有Ei一个类型U并且xi是一个值参数,则U到 。Ti
  • 否则,如果有Ei一个类型U并且xi是一个或一个ref参数out,则Ti
  • 否则,不会对此参数进行推定。

显式返回类型推理

以下列方式将显式返回类型推理表达式E类型T

  • 如果E匿名函数具有显式返回类型,并且是具有返回类型的UrT委托类型或表达式树类型,则Vr

修正

修复(§12.6.3.12)可确保其他转换优先于 function_type 转换。 (Lambda 表达式和方法组表达式仅有助于下限,因此仅下限需要处理 function_types

具有一组绑定的非固定类型变量 Xi固定如下:

  • 候选类型Uj作为一组所有类型的集合Xi开始,如果有任何类型不是函数类型,则函数类型在下限中被忽略。
  • 然后,我们将逐个检查每个绑定Xi:对于所有类型的U确切边界XiUj,这些类型与从候选集中删除的不完全相同U。 对于 U 的每个下限 Xi,候选集中会删除所有类型 Uj,它们不会U 进行隐式转换。 对于所有类型的上限UXi不会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 表达式或方法组推断类型的类型参数。

更好的函数成员

... 给定一个参数列表 A,其中包含一组参数表达式 {E1, E2, ..., En},以及两个适用的函数成员 MpMq(参数类型分别为 {P1, P2, ..., Pn}{Q1, Q2, ..., Qn}),如果符合以下条件,则 Mp 会被定义为比

  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. 对于每个参数,从 ExQx 的隐式转换并不优于从 ExPx 的隐式转换,对于至少一个参数,从 ExPx 的转换优于从 ExQx 的转换。

更好的从表达式转换(§12.6.4.5)更新为首选转换,这些转换不涉及来自 lambda 表达式或方法组的推断类型。

更好的从表达式转换

给定从表达式 C1 转换为类型 E 的隐式转换 T1 和从表达式 C2 转换为类型 E 的隐式转换 T2,如果符合以下条件,则 C1 是比 C2

  1. C1不是function_type_conversion,是C2function_type_conversion,或者
  2. E 是非常量的 插值字符串表达式C1隐式字符串处理器转换T1适用的插值字符串处理器类型,而 C2 不是 隐式字符串处理器转换
  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?

除了生成的委托类型之外,编译器 API 是否应提供function_type

目前,推断的委托类型使用 System.Action<>System.Func<> 参数和返回类型是有效的类型参数 并且没有超过 16 个参数,如果缺少预期 Action<>Func<> 类型,则报告错误。 相反,编译器应该使用 System.Action<> 还是 System.Func<> 不考虑 arity? 如果缺少预期的类型,则合成委托类型,否则合成委托类型?