带修饰符的简单 lambda 参数

注释

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

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

可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/8637

概要

允许使用修饰符声明 lambda 参数,而无需其类型名称。 例如,使用 (ref entry) =>,而不使用 (ref FileSystemEntry entry) =>

作为另一示例,假定此委托:

delegate bool TryParse<T>(string text, out T result);

允许此简化的参数声明:

TryParse<int> parse1 = (text, out result) => Int32.TryParse(text, out result);

目前仅此有效:

TryParse<int> parse2 = (string text, out int result) => Int32.TryParse(text, out result);

详细设计

语法

无更改。 最新的 lambda 语法 为:

lambda_expression
  : modifier* identifier '=>' (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 default_argument?
  ;

根据这种语法,modifiers* identifier 已被认为在句法上是合法的。

注释

  1. 这不适用于没有参数列表的 lambda。 ref x => x.ToString() 不会是合法的。
  2. lambda 参数列表仍不能混合 implicit_anonymous_function_parameterexplicit_anonymous_function_parameter 参数。
  3. (ref readonly p) =>(scoped ref p) =>(scoped ref readonly p) => 将被允许,就像使用显式参数一样,原因如下:
  4. 存在/缺少类型不会影响修饰符是必需还是可选。 换句话说,如果存在类型的修饰符是必需的,则不存在该类型时仍需要该修饰符。 同样,如果修饰符在类型存在时是可选的,那么在类型不存在时也是可选的。

语义学

https://learn.microsoft.com/dotnet/csharp/language-reference/language-specification/expressions#12192-anonymous-function-signatures 更新如下:

lambda_parameter_list 中,所有 lambda_parameter 元素都必须具有 type 或不具有 type。 前者是“显式类型化参数列表”,而后者是“隐式类型化参数列表”。

隐式类型化参数列表中的参数不能有 default_argument。 它们可有一个 attribute_list

匿名函数转换要求进行以下更改:

[...]

如果 F 具有显式 或隐式类型化参数列表,则 D 中的每个参数的类型和修饰符与 F 中忽略参数修饰符和默认值中的相应参数相同。

笔记/说明

scopedparams 可以在 lambda 中作为修饰符使用,即便没有显式类型。 两者的语义保持不变。 具体而言,这两者都不是以下决定的一部分:

如果匿名函数具有显式匿名函数签名,那么兼容的委托类型和表达式树类型的集合被限制为具有相同顺序的相同参数类型和修饰符的类型。

限制兼容委托类型的唯一修饰符是refoutin以及ref readonly。 例如,在显式类型化 lambda 中,以下内容当前存在歧义:

delegate void D<T>(scoped T t) where T : allows ref struct;
delegate void E<T>(T t) where T : allows ref struct;

class C
{
    void M<T>() where T : allows ref struct
    {
        // error CS0121: The call is ambiguous between the following methods or properties: 'C.M1<T>(D<T>)' and 'C.M1<T>(E<T>)'
        // despite the presence of the `scoped` keyword.
        M1<T>((scoped T t) => { });
    }

    void M1<T>(D<T> d) where T : allows ref struct
    {
    }

    void M1<T>(E<T> d) where T : allows ref struct
    {
    }
}

使用隐式类型的 lambda 表达式时,这种情况依然存在。

delegate void D<T>(scoped T t) where T : allows ref struct;
delegate void E<T>(T t) where T : allows ref struct;

class C
{
    void M<T>() where T : allows ref struct
    {
        // This will remain ambiguous.  'scoped' will not be used to restrict the set of delegates.
        M1<T>((scoped t) => { });
    }

    void M1<T>(D<T> d) where T : allows ref struct
    {
    }

    void M1<T>(E<T> d) where T : allows ref struct
    {
    }
}

待讨论的问题

  1. 在 C# 14 中,是否scoped始终应作为 lambda 表达式中的修饰符? 这对于如下情况很重要:

    M((scoped s = default) => { });
    

    在此情况下,它确实属于“简单 lambda 参数”规范,因为“简单 lambda”无法包含初始值设定项 (= default)。 因此,这里的 scoped 被视为 type,就像在 C# 13 中一样。 我们想维护这一点吗? 如果拥有一条总是将scoped视为修饰符的全面规则,是不是更简单?这样一来,即使在无效的简单参数情况下,它仍然被视为修饰符。

    建议:将此设为修饰符。 我们已经劝阻人们不要使用全小写的类型名称了,而且我们也已经规定在 C# 中创建名为 scoped 的类型是不合法的。 因此,这只能是引用另一个库中的类型的某种情况。 如果你真的以某种方式遇到了这种情况,解决方法其实很简单。 只需使用 @scoped 将其作为类型名称而不是作为修饰符。

  2. 允许在简单的 lambda 参数中使用 params? 先前的 lambda 工作已添加对 lambda 中 params T[] values 的支持。 此修饰符为可选,且允许 lambda 和原始委托在此修饰符上不匹配(虽然当委托没有修饰符而 lambda 有时,我们会警告)。 我们是否应该继续允许使用简单的 lambda 参数? 例如,M((params values) => { ... })

    推荐:是。 允许此操作。 此规范的目的是允许只从 lambda 参数中删除“type”,同时保留修饰符。 这不过是同样情况的又一个例子。 它也正是此实现的自然结果(与这些参数的支持属性情况一样),因此尝试阻止该情况会增加工作量。

    结论 1/15/2025:否。 这始终是一个错误。 似乎没有任何有用的情况,没有人要求这样做。 从一开始就限制这种不明智的案例更容易和更安全。 如果提供了相关的用例,我们可以重新考虑。

  3. “scoped”是否会影响重载解析? 例如,如果委托有多个重载,且其中一个具有“scoped”参数,而另一重载没有,则“scoped”的存在会影响重载解析。

    推荐:否。 不让“scoped”影响重载解析。 这已是针对正常显式类型化 lambda 的处理方式。 例如:

    delegate void D<T>(scoped T t) where T : allows ref struct;
    delegate void E<T>(T t) where T : allows ref struct;
    
    class C
    {
        void M<T>() where T : allows ref struct
        {
            M1<T>((scoped T t) => { });
        }
    
        void M1<T>(D<T> d) where T : allows ref struct
        {
        }
    
        void M1<T>(E<T> d) where T : allows ref struct
        {
        }
    }
    

    今天这很模棱两可。 虽然在 lambda 参数中针对 D<T> 和“scoped”进行了“scoped”处理,但我们仍未解决此问题。 我们并不认为它会随隐式类型化 lambda 而改变。

    2025 年 1 月 15 日的结论:上述内容也适用于“简单 lamba”。 “scoped”不会影响重载解析(而 refout 将继续影响重载解析)。

  4. 是否允许“(scoped x) => ...”形式的lambda表达式?

    建议:是的。 如果我们不允许这种情况,那么最终可能会出现这样的情况:用户能够编写完整的显式类型化 lambda 表达式,但却无法编写隐式类型化版本。 例如:

    delegate ReadOnlySpan<int> D(scoped ReadOnlySpan<int> x);
    
    class C
    {
        static void Main(string[] args)
        {
            D d = (scoped ReadOnlySpan<int> x) => throw null!;
            D d = (ReadOnlySpan<int> x) => throw null!; // error! 'scoped' is required
        }
    }
    

    删除此处的“scoped”会导致错误,因为在此情况下,该语言要求 lambda 和委托之间存在对应关系。 由于我们希望用户能够编写如下所示的 lambda,而无需显式指定类型,这意味着 (scoped x) => ... 需要允许。

    结论 2025/1/15:我们将允许 (scoped x) => ... Lambda 表达式。