扩展成员

注释

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

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

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

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

声明

语法

class_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    | extension_declaration // add
    ;

extension_declaration // add
    : 'extension' type_parameter_list? '(' receiver_parameter ')' type_parameter_constraints_clause* extension_body
    ;

extension_body // add
    : '{' extension_member_declaration* '}' ';'?
    ;

extension_member_declaration // add
    : method_declaration
    | property_declaration
    | indexer_declaration
    | operator_declaration
    ;

receiver_parameter // add
    : attributes? parameter_modifiers? type identifier?
    ;

扩展声明只能在非泛型非嵌套静态类中声明。
对于要命名 extension的类型,这是一个错误。

作用域界定规则

扩展声明的类型参数和接收方参数位于扩展声明的正文范围内。 从静态成员(表达式中 nameof 除外)引用接收方参数是错误的。 成员声明类型参数或参数(以及成员正文中的局部变量和本地函数)与扩展声明的类型参数或接收方参数相同,这是一个错误。

public static class E
{
    extension<T>(T[] ts)
    {
        public bool M1(T t) => ts.Contains(t);        // `T` and `ts` are in scope
        public static bool M2(T t) => ts.Contains(t); // Error: Cannot refer to `ts` from static context
        public void M3(int T, string ts) { }          // Error: Cannot reuse names `T` and `ts`
        public void M4<T, ts>(string s) { }           // Error: Cannot reuse names `T` and `ts`
    }
}

封闭扩展声明中,成员与类型参数或接收方参数同名,并不构成错误。 成员名称不能直接在扩展声明中的简单名称查找中找到;因此,查找将查找该名称的类型参数或接收方参数,而不是该成员。

成员的确会导致直接声明在封闭静态类上的静态方法,并且可以通过简单的名称查找找到;但是,同名的扩展声明类型参数或接收方参数会首先被找到。

public static class E
{
    extension<T>(T[] ts)
    {
        public void T() { M(ts); } // Generated static method M<T>(T[]) is found
        public void M() { T(ts); } // Error: T is a type parameter
    }
}

作为扩展容器的静态类

扩展在顶级非泛型静态类中声明,就像当前扩展方法一样,因此可与经典扩展方法和非扩展静态成员共存:

public static class Enumerable
{
    // New extension declaration
    extension(IEnumerable source) { ... }
    
    // Classic extension method
    public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
    
    // Non-extension member
    public static IEnumerable<int> Range(int start, int count) { ... } 
}

扩展声明

扩展声明是匿名的,它提供一个接收器规范,其中包含所有关联的类型参数和约束,随后是一组扩展成员声明。 接收方规范可以采用参数的形式,或者(如果仅声明静态扩展成员)类型:

public static class Enumerable
{
    extension(IEnumerable source) // extension members for IEnumerable
    {
        public bool IsEmpty { get { ... } }
    }
    extension<TSource>(IEnumerable<TSource> source) // extension members for IEnumerable<TSource>
    {
        public IEnumerable<T> Where(Func<TSource, bool> predicate) { ... }
        public IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) { ... }
    }
    extension<TElement>(IEnumerable<TElement>) // static extension members for IEnumerable<TElement>
        where TElement : INumber<TElement>
    {
        public static IEnumerable<TElement> operator +(IEnumerable<TElement> first, IEnumerable<TElement> second) { ... }
    }
}

接收方规范中的类型称为 接收方类型和 参数名称(如果存在),称为 接收方参数

如果命名 接收方参数则接收方类型 可能不是静态的。
如果 接收方参数 未命名,则不允许使用任何修饰符;如果命名了,则只允许使用下面列出的修饰符以及 scoped 中的修饰符。
接收方参数具有与经典扩展方法的第一个参数相同的限制。
[EnumeratorCancellation]如果属性放置在接收方参数上,则忽略该属性。

扩展成员

扩展成员声明在语法上与类和结构声明中的对应实例和静态成员相同(除构造函数除外)。 实例成员引用具有接收方参数名称的接收方:

public static class Enumerable
{
    extension(IEnumerable source)
    {
        // 'source' refers to receiver
        public bool IsEmpty => !source.GetEnumerator().MoveNext();
    }
}

如果封闭扩展声明未指定接收方参数,则指定实例扩展成员(方法、属性、索引器或事件)是错误的:

public static class Enumerable
{
    extension(IEnumerable) // No parameter name
    {
        public bool IsEmpty => true; // Error: instance extension member not allowed
    }
}

在扩展声明的成员上指定以下修饰符是错误的:abstractvirtualoverridenewsealedexternpartialprotected(以及相关的可访问性修饰符)。
扩展声明中的属性可能没有 init 访问器。
如果 接收器参数 未命名,则不允许实例成员。

Refness

默认情况下,接收方按值传递给实例扩展成员,就像其他参数一样。 但是,参数形式的扩展声明接收器可以指定 refref readonly 并且 in,只要接收器类型已知为值类型。

如果 ref 已指定,可以声明 readonly实例成员或其访问器之一,从而阻止它改变接收方:

public static class Bits
{
    extension(ref ulong bits) // receiver is passed by ref
    {
        public bool this[int index]
        {
            set => bits = value ? bits | Mask(index) : bits & ~Mask(index); // mutates receiver
            readonly get => (bits & Mask(index)) != 0;                // cannot mutate receiver
        }
    }
    static ulong Mask(int index) => 1ul << index;
}

可为 Null 性和属性

接收方类型可以是或包含可为 null 的引用类型,并且采用参数形式的接收器规范可以指定属性:

public static class NullableExtensions
{
    extension(string? text)
    {
        public string AsNotNull => text is null ? "" : text;
    }
    extension([NotNullWhen(false)] string? text)
    {
        public bool IsNullOrEmpty => text is null or [];
    }
    extension<T> ([NotNull] T t) where T : class?
    {
        public void ThrowIfNull() => ArgumentNullException.ThrowIfNull(t);
    }
}

与经典扩展方法的兼容性

实例扩展方法生成的工件与经典扩展方法生成的相匹配。

具体而言,生成的静态方法具有声明的扩展方法的属性、修饰符和名称,以及类型参数列表、参数列表和约束列表,以及从扩展声明和方法声明串联的顺序:

public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source) // Generate compatible extension methods
    {
        public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
        public IEnumerable<TSource> Select<TResult>(Func<TSource, TResult> selector)  { ... }
    }
}

生成:

[Extension]
public static class Enumerable
{
    [Extension]
    public static IEnumerable<TSource> Where<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate) { ... }

    [Extension]
    public static IEnumerable<TSource> Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector)  { ... }
}

运营商

尽管扩展运算符具有显式作数类型,但它们仍需要在扩展声明中声明:

public static class Enumerable
{
    extension<TElement>(IEnumerable<TElement>) where TElement : INumber<TElement>
    {
        public static IEnumerable<TElement> operator *(IEnumerable<TElement> vector, TElement scalar) { ... }
        public static IEnumerable<TElement> operator *(TElement scalar, IEnumerable<TElement> vector) { ... }
    }
}

这允许声明和推断类型参数,类似于在某个作数类型中必须声明常规用户定义运算符的方式。

检查

推理性: 扩展声明的所有类型参数都必须在接收器类型中使用。 这样,在应用于给定接收方类型的接收器时,始终可以推断类型参数。

唯一性: 在给定封闭的静态类中,具有相同接收方类型(模态标识转换和类型参数名称替换)的扩展成员声明集被视为与类或结构声明中的成员类似的单个声明空间,并且受到与唯一 性相同的规则

public static class MyExtensions
{
    extension<T1>(IEnumerable<int>) // Error! T1 not inferrable
    {
        ...
    }
    extension<T2>(IEnumerable<T2>)
    {
        public bool IsEmpty { get ... }
    }
    extension<T3>(IEnumerable<T3>?)
    {
        public bool IsEmpty { get ... } // Error! Duplicate declaration
    }
}

此唯一性规则的应用程序包括同一静态类中的经典扩展方法。 为了与扩展声明中的方法进行比较,该 this 参数将被视为接收方规范以及该接收器类型中提到的任何类型参数,其余类型参数和方法参数用于方法签名:

public static class Enumerable
{
    public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
    
    extension(IEnumerable source) 
    {
        IEnumerable<TResult> Cast<TResult>() { ... } // Error! Duplicate declaration
    }
}

消费

尝试扩展成员查找时,导入的静态类 using中的所有扩展声明都将其成员作为候选项提供,而不考虑接收方类型。 仅作为解决方案的一部分,丢弃了不兼容的接收方类型的候选项。
在参数类型(包括实际接收方)与任何类型参数(结合扩展声明和扩展成员声明中的参数)之间尝试进行全面的泛型类型推理。
提供显式类型参数时,它们用于替换扩展声明的类型参数和扩展成员声明。

string[] strings = ...;

var query = strings.Select(s => s.Length); // extension invocation
var query2 = strings.Select<string, int>(s => s.Length); // ... with explicit full set of type arguments

var query3 = Enumerable.Select(strings, s => s.Length); // static method invocation
var query4 = Enumerable.Where<string, int>(strings, s => s.Length); // ... with explicit full set of type arguments
 
public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source)
    {
        public IEnumerable<TResult> Select<TResult>(Func<T, TResult> predicate) { ... }
    }
}

与经典扩展方法类似,可以静态调用被生成的实现方法。
这允许编译器在具有相同名称和 arity 的扩展成员之间消除歧义。

object.M(); // ambiguous
E1.M();

new object().M2(); // ambiguous
E1.M2(new object());

_ = _new object().P; // ambiguous
_ = E1.get_P(new object());

static class E1
{
    extension(object)
    {
        public static void M() { }
        public void M2() { }
        public int P => 42;
    }
}

static class E2
{
    extension(object)
    {
        public static void M() { }
        public void M2() { }
        public int P => 42;
    }
}

静态扩展方法将像实例扩展方法一样解析(我们将考虑接收方类型的额外参数)。
扩展属性将像扩展方法一样处理,它包含了单个参数(接收者参数)和单个参数值(实际接收者值)。

重载解析优先级属性

封闭静态类中的扩展成员会根据 ORPA 值进行优先级排序。 封闭静态类被视为 ORPA 规则所考虑的“包含类型”。
扩展属性上的任何 ORPA 属性都会复制到属性访问器的实现方法上,以便当通过消除歧义的语法使用这些访问器时,能够遵循优先级。

降低

扩展声明的降低策略不是语言级别决策。 但是,除了实现语言语义之外,它必须满足某些要求:

  • 应在所有情况下明确指定生成的类型、成员和元数据的格式,以便其他编译器可以使用和生成它。
  • 生成的项目应稳定,因为合理的后续修改不应破坏针对早期版本编译的使用者。

随着实施进度,这些要求需要更细化,并且可能需要在角落案例中受到损害,以便采用合理的实施方法。

声明的元数据

每个扩展声明都作为嵌套的私有静态类发出,其中包含标记方法和框架成员。
每个主干成员都附带一个具有修改签名的顶级静态实现方法。
扩展声明所在的静态类使用 [Extension] 属性标记。

骨骼

源中的每个扩展声明都作为元数据中的扩展声明发出。

  • 其名称不可辨,并根据程序中的词法顺序确定。
    无法保证名称在重新编译时保持稳定。 下面使用 <>E__ 后跟索引。 例如: <>E__2
  • 其类型参数是在源中声明的(包括属性)。
  • 它的可访问性是公共的。

源中扩展声明中的方法/属性/索引器声明表示为元数据中的主干成员。
原始方法的签名是保留的(包括属性),但其主体将被 throw null替换为。
不应在 IL 中引用这些值。

注意:这类似于 ref 程序集。 使用 throw null 主体(而不是无主体)的原因是,IL 验证可以运行和传递(从而验证元数据的完整性)。

扩展标记方法对接收方参数进行编码。

  • 它是静态和私有的,名为 <Extension>$
  • 它具有扩展声明上接收方参数的属性、引用、类型和名称。
  • 如果接收方参数未指定名称,则参数名称为空。

注意:这支持通过元数据(完整程序集和引用程序集)进行扩展声明符号的往返操作。

注意:在源中找到重复扩展声明时,我们只能选择在元数据中发出一个扩展框架类型。

实现

在源代码中的扩展声明中,对于方法/属性/索引器声明,其方法主体被作为实现静态方法在顶级静态类中发出。

  • 实现方法的名称与原始方法相同。
  • 它具有派生自原始方法的类型参数(包括属性)的扩展声明的类型参数。
  • 它具有与原始方法相同的辅助功能和属性。
  • 如果它实现静态方法,则它具有相同的参数和返回类型。
  • 如果实现实例方法,则它具有原始方法签名的前面附加参数。 此参数的属性、refness、type 和 name 派生自相关扩展声明中声明的接收方参数。
  • 实现方法中的参数是指实现方法拥有的类型参数,而不是扩展声明的类型参数。
  • 如果原始成员是一个实例普通方法,则实现方法使用 [Extension] 属性进行标记。

例如:

static class IEnumerableExtensions
{
    extension<T>(IEnumerable<T> source)
    {
        public void Method() { ... }
        internal static int Property { get => ...; set => ...; }
        public int Property2 { get => ...; set => ...; }
    }

    extension(IAsyncEnumerable<int> values)
    {
        public async Task<int> SumAsync() { ... }
    }

    public static void Method2() { ... }
}

发出为

[Extension]
static class IEnumerableExtensions
{
    public class <>E__1<T>
    {
        private static <Extension>$(IEnumerable<T> source) => throw null;
        public void Method() => throw null;
        internal static int Property { get => throw null; set => throw null; }
        public int Property2 { get => throw null; set => throw null; }
    }

    public class <>E__2
    {
        private static <Extension>$(IAsyncEnumerable<int> values) => throw null;
        public Task<int> SumAsync() => throw null;
    }

    // Implementation for Method
    [Extension]
    public static void Method<T>(IEnumerable<T> source) { ... }

    // Implementation for Property
    internal static int get_Property<T>() { ... }
    internal static void set_Property<T>(int value) { ... }

    // Implementation for Property2
    public static int get_Property2<T>(IEnumerable<T> source) { ... }
    public static void set_Property2<T>(IEnumerable<T> source, int value) { ... }

    // Implementation for SumAsync
    [Extension]
    public static int SumAsync(IAsyncEnumerable<int> values) { ... }

    public static void Method2() { ... }
}

每当在源中使用扩展成员时,我们将发出这些成员作为对实现方法的引用。
例如:将调用 enumerableOfInt.Method() 作为静态调用 IEnumerableExtensions.Method<int>(enumerableOfInt)发出。

XML 文档

在下面的示例中,针对不可描述的名称类型<>E__0'1 发出扩展块的文档注释。
针对框架成员发出有关扩展成员的文档注释。 允许使用<paramref><typeparamref>分别引用扩展参数和类型参数。
注意:不能在扩展成员上标注扩展参数或类型参数(如<param><typeparam>)。

处理 XML 文档的工具负责将 <param><typeparam> 从扩展块复制到扩展成员,根据需要仅为实例成员复制参数信息。

** 在实现方法上会发出一个<inheritdoc>,它引用具有cref属性的相关框架成员。 例如,getter 的实现方法是指框架属性的文档。 如果主干成员没有文档注释, <inheritdoc> 则省略该成员。

对于扩展块和扩展成员,我们目前不会发出任何警告。

  • 扩展参数是有记录的,但扩展成员上的参数没有记录。
  • 反之亦然
  • 或在具有未记录类型参数的等效方案中

例如,以下文档注释:

/// <summary>Summary for E</summary>
static class E
{
    /// <summary>Summary for extension block</summary>
    /// <typeparam name="T">Description for T</typeparam>
    /// <param name="t">Description for t</param>
    extension<T>(T t)
    {
        /// <summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
        /// <typeparam name="U">Description for U</typeparam>
        /// <param name="u">Description for u</param>
        public void M<U>(U u) => throw null!;

        /// <summary>Summary for P</summary>
        public int P => 0;
    }
}

生成以下 xml:

<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Test</name>
    </assembly>
    <members>
        <member name="T:E">
            <summary>Summary for E</summary>
        </member>
        <member name="T:E.<>E__0`1">
            <summary>Summary for extension block</summary>
            <typeparam name="T">Description for T</typeparam>
            <param name="t">Description for t</param>
        </member>
        <member name="M:E.<>E__0`1.M``1(``0)">
            <summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
            <typeparam name="U">Description for U</typeparam>
            <param name="u">Description for u</param>
        </member>
        <member name="P:E.<>E__0`1.P">
            <summary>Summary for P</summary>
        </member>
        <member name="M:E.M``2(``0,``1)">
            <inheritdoc cref="M:E.<>E__0`1.M``1(``0)"/>
        </member>
        <member name="M:E.get_P``1(``0)">
            <inheritdoc cref="P:E.<>E__0`1.P"/>
        </member>
    </members>
</doc>

重大变化

类型和别名不能命名为“extension”。

打开议题

  • 确认 extensionextensions 作为关键字 (答案: extension,LDM 2025-03-24)

nameof

  • 我们是否应该像禁止经典扩展方法和新的扩展方法一样,在 nameof 中禁止使用扩展属性? (答:不,这是引用属性名称的唯一方法)
C c = null;
_ = nameof(c.M); // Extension method groups are not allowed as an argument to 'nameof'.
_ = nameof(c.M2); // Extension method groups are not allowed as an argument to 'nameof'.
_ = nameof(c.P);

_ = nameof(C.M3); // Extension method groups are not allowed as an argument to 'nameof'.
_ = nameof(C.P2);

class C { }

static class E
{
    public static void M(this C c) { }
    extension(C c)
    {
        public void M2() { }
        public int P => 42;

        public static void M3() { }
        public static int P2 => 42;
    }
}

入口点

  • 在查找入口点时,我们是否应该跳过扩展块?

基于模式的构造

方法

  • 新的扩展方法应该在何处发挥作用? (答:经典扩展方法发挥作用的地方,LDM 2025-05-05)这包括:
  • GetEnumerator / GetAsyncEnumeratorforeach
  • Deconstruct 在解构、位置模式和 foreach 中
  • Add 集合初始值设定项中的
  • GetPinnableReference 中的 fixed
  • GetAwaiter 中的 await

这不包括:

  • Dispose / DisposeAsyncusingforeach
  • MoveNext / MoveNextAsyncforeach
  • Sliceint 隐式索引器中的索引器(以及可能的列表模式?)
  • GetResult 中的 await

属性和索引器

  • 扩展属性和索引器应该在何处发挥作用? (答:让我们从以下四个项目开始,LDM 2025-05-05)我们将包括:
  • 对象初始值设定项: new C() { ExtensionProperty = ... }
  • 字典初始化器: new C() { [0] = ... }
  • with: x with { ExtensionProperty = ... }
  • 属性模式: x is { ExtensionProperty: ... }

我们会排除:

  • Current 中的 foreach

  • IsCompleted 中的 await

  • Count / Length 列表模式中的属性和索引器

  • Count / Length 隐式索引器中的属性和索引器

  • 委托返回属性应该在何处发挥作用?

集合表达式

  • 扩展 Add 工作原理
  • 扩展 GetEnumerator 适用于传播
  • 扩展 GetEnumerator 不会影响元素类型的确定(必须是实例)
  • 扩展 Create 不算作被认可的 创建 方法
  • 扩展可计数属性是否会影响集合表达式?

params 集合

  • 扩展 Add 不会影响与 params 一同允许的类型

字典表达式

  • 扩展索引器?

骨架类型的命名/编号方案

问题
当前编号系统会导致 验证公共 API 时出现问题,这可确保仅引用程序集与实现程序集之间的公共 API 匹配。

我们可以:

  • 调整工具
  • 使用相对于声明的扩展块的数字(第一个、第二个等语法顺序)
  • 使用一些哈希方案 (TBD)
  • 允许通过某些语法控制名称

新的泛型扩展 Cast 方法仍无法在 LINQ 中工作

问题
在角色/扩展的早期设计中,只能显式指定方法的类型参数。
但是,现在我们专注于从经典扩展方法进行看似无形的转换,必须显式提供所有类型参数。
这无法解决 LINQ 中扩展 Cast 方法用法的问题。

对扩展成员的扩展参数进行约束

我们应该允许以下事项吗?

static class E
{
    extension<T>(T t)
    {
        public void M<U>(U u) where T : C<U>  { } // error: 'E.extension<T>(T).M<U>(U)' does not define type parameter 'T'
    }
}

public class C<T> { }

可空性

  • 确认当前设计,即最大可移植性/兼容性 (答案:是,LDM 2025-04-17)
    extension([System.Diagnostics.CodeAnalysis.DoesNotReturnIf(false)] bool b)
    {
        public void AssertTrue() => throw null!;
    }
    extension([System.Diagnostics.CodeAnalysis.NotNullIfNotNull("o")] ref int? i)
    {
        public void M(object? o)  => throw null!;
    }

元数据

  • 骨架方法应该抛出 NotSupportedException 还是其他一些标准异常(当前我们采用 throw null;)? (答:是的,LDM 2025-04-17)
  • 在元数据中是否应接受标记方法中的多个参数(如果新版本添加更多信息) ? (答:我们可以保持严格,LDM 2025-04-17)
  • 扩展标记或可说的实现方法是否应使用特殊名称进行标记? (答:标记方法应标记为特殊名称,我们应该检查它,但不检查实现方法,LDM 2025-04-17)
  • 即使内部没有实例扩展方法,我们也应该在静态类上添加 [Extension] 属性? (答:是的,LDM 2025-03-10)
  • 请确认我们是否也应该将[Extension]属性添加到实现的getter和setter中。 (答:否,LDM 2025-03-10)

静态工厂方案

  • 静态方法的冲突规则是什么? (答:对封闭静态类型使用现有的 C# 规则,不放松,LDM 2025-03-17)

查找

  • 现在,有了可读的实现名称后,我们应该如何解析实例方法调用? 我们更喜欢骨架方法相比于其对应的实现方法。
  • 如何解析静态扩展方法? (答:就像实例扩展方法一样,LDM 2025-03-03)
  • 如何解析属性? (大致回答 LDM 2025-03-03,但需要进一步跟进以改善)
  • 扩展参数和类型参数的作用域和遮蔽规则 (答案:在扩展块作用域内,不允许遮蔽,LDM 2025-03-10)
  • ORPA 应如何应用于新的扩展方法? (答:将扩展块视为透明,ORPA 的“包含类型”是封闭静态类 LDM 2025-04-17)
public static class Extensions
{
    extension(Type1)
    {
        [OverloadResolutionPriority(1)]
        public void Overload(...)
    }
    extension(Type2)
    {
        public void Overload(...)
    }
}
  • ORPA 是否适用于新的扩展属性? (答:是的,ORPA 应复制到实施方法中,LDM 2025-04-23)
public static class Extensions
{
    extension(int[] i)
    {
        public P { get => }
    }
    extension(ReadOnlySpan<int> r)
    {
       [OverloadResolutionPriority(1)]
       public P { get => }
    }
}
  • 如何重新设定传统扩展解析规则? 我们是否
    1. 更新经典扩展方法的标准,并使用它来描述新的扩展方法,
    2. 保留经典扩展方法的现有语言,使用该语言来描述新的扩展方法,但两者都有已知的规范偏差,
    3. 继续使用经典扩展方法的现有语言,而对新的扩展方法使用不同的语言,并且仅在经典扩展方法中存在已知的规范偏差。
  • 确认我们希望禁止对属性访问使用显式类型参数 (答案:没有具有显式类型参数的属性访问,WG 中讨论)
string s = "ran";
_ = s.P<object>; // error

static class E
{
    extension<T>(T t)
    {
        public int P => 0;
    }
}
  • 确认我们希望,即使接收方是某种类型,也要应用更优规则。
int.M();

static class E1
{
    extension(int)
    {
        public static void M() { }
    }
}
static class E2
{
    extension(in int i)
    {
        public static void M() => throw null;
    }
}
  • 确认在确定获胜成员类别之前,我们不会希望在所有成员中追求一些整体改进。
string s = null;
s.M(); // error

static class E
{
    extension(string s)
    {
        public System.Action M => throw null;
    }
    extension(object o)
    {
        public string M() => throw null;
    }
}
  • 扩展声明中是否有隐式接收器? (答:不,以前在 LDM 中讨论过)
static class E
{
    extension(object o)
    {
        public void M() 
        {
            M2();
        }
        public void M2() { }
    }
}
  • 重新审视类型参数查找的问题(讨论

可及性

  • 扩展声明中可访问性的含义是什么? (答:扩展声明不算作可访问性范围,LDM 2025-03-17)
  • 我们是否应该对接收参数应用“不一致的可访问性”检查,即使是静态成员? (答:是的,LDM 2025-04-17)
public static class Extensions
{
    extension(PrivateType p)
    {
        // We report inconsistent accessibility error, 
        //   because we generate a `public static void M(PrivateType p)` implementation in enclosing type
        public void M() { } 

        public static void M2() { } // should we also report here, even though not technically necessary?
    }

    private class PrivateType { }
}

扩展声明验证

  • 我们是否应该放宽类型参数验证(可推断性:所有类型参数都必须出现在扩展参数的类型中),在仅涉及方法的情况下? 这将允许移植 100% 经典扩展方法。
    如果有 TResult M<TResult, TSource>(this TSource source),则可以将其移植为 extension<TResult, TSource>(TSource source) { TResult M() ... }
    (答:否,LDM 2025-03-17)
  • 确认是否应在扩展中允许仅初始化访问器(答案:目前可以不允许,LDM 2025-04-17)
  • 是否应允许 extension(int receiver) { public void M2() {} }extension(ref int receiver) { public void M2() {} }接收方 ref-ness 的唯一差异? (答:不,保留规范规则,LDM 2025-03-24)
  • 我们应该抱怨这样的 extension(object receiver) { public int P1 => 1; }extension(object receiver) { public int P1 {set{}} }冲突吗? (答:是的,保留规范规则,LDM 2025-03-24)
  • 我们是否应该抱怨那些并非实现方法之间冲突的框架方法之间的冲突? (答:是的,保留规范规则,LDM 2025-03-24)
static class E
{
    extension(object)
    {
        public void Method() {  }
        public static void Method() { }
    }
}

当前冲突规则为:1。 确认类似扩展中不存在冲突,使用类/结构规则(2)。 检查各个扩展声明中的实现方法是否存在冲突。

  • 我们是否仍然需要规则的第一部分? (答:是的,我们保留这种结构,因为它有助于利用 API,LDM 2025-03-24)

XML 文档

  • 扩展成员paramref是否支持接收者参数? 即使在静态界面上? 如何在输出中对其进行编码? 标准方式 <paramref name="..."/> 对人类可能适用,但存在一个风险,即某些现有工具在 API 的参数中找不到此标准方式时可能会不满。 (答:是,允许在扩展成员上使用扩展参数的 paramref,LDM 2025-05-05)
  • 我们是否应将文档注释复制到具有易读名称的实现方法中? (答:没有复制,LDM 2025-05-05)
  • 是否应从扩展容器中复制与接收者参数对应的元素到实例方法中? 其他内容是否都应从容器复制到实现方法(<typeparam> 等等)? (答:没有复制,LDM 2025-05-05)
  • 是否应允许扩展成员的<param>扩展参数作为重写? (答:目前不,LDM 2025-05-05)
  • 扩展块的摘要会出现在某个地方吗?
  • 扩展(骨架)成员可以通过 cref 引用吗?

添加对更多成员类型的支持

我们不需要一次实现所有这些设计,但一次可以采用一种或几种成员类型。 根据核心库中的已知方案,应按以下顺序工作:

  1. 属性和方法(实例和静态)
  2. 运营商
  3. 索引器(实例和静态)可能以机会方式在早期完成)
  4. 任何其他项目

我们希望为其他类型的成员提前加载设计多少?

extension_member_declaration // add
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

嵌套类型

如果我们确实选择继续扩展嵌套类型,下面是前面讨论的一些说明:

  • 如果两个扩展声明声明了具有相同名称和元数的嵌套扩展类型,则会出现冲突。 我们没有用于在元数据中表示此内容的解决方案。
  • 我们讨论的元数据的粗略方法:
    1. 我们将发出具有原始类型参数且无成员的框架嵌套类型
    2. 我们将生成一个实现嵌套类型,该类型在扩展声明中附加了类型参数,并且所有成员实现按在源代码中的顺序出现(除了对类型参数的引用)。

构造函数

构造函数通常被描述为 C# 中的实例成员,因为它们的正文可以通过 this 关键字访问新创建的值。 不过,这不适用于基于参数的实例扩展成员方法,因为以前没有作为参数传入的值。

相反,扩展构造函数的工作方式更像静态工厂方法。 它们被视为静态成员,因为它们不依赖于接收方参数名称。 他们的身体需要显式创建并返回构造结果。 成员本身仍使用构造函数语法声明,但不能具有 thisbase 初始值设定项,并且不依赖于具有可访问构造函数的接收方类型。

这也意味着,可以为没有自身构造函数的类型声明扩展构造函数,例如接口和枚举类型:

public static class Enumerable
{
    extension(IEnumerable<int>)
    {
        public static IEnumerable(int start, int count) => Range(start, count);
    }
    public static IEnumerable<int> Range(int start, int count) { ... } 
}

允许:

var range = new IEnumerable<int>(1, 100);

较短的表单

建议的设计避免了接收者规范每个成员的重复,但最终扩展成员在静态类和扩展声明中两层嵌套。 静态类可能只包含一个扩展声明或扩展声明只包含一个成员,并且我们似乎可以允许这些事例的语法缩写。

合并静态类和扩展声明:

public static class EmptyExtensions : extension(IEnumerable source)
{
    public bool IsEmpty => !source.GetEnumerator().MoveNext();
}

这最终看起来更像是我们所说的“基于类型”的方法,其中扩展成员的容器本身被命名。

合并扩展声明和扩展成员:

public static class Bits
{
    extension(ref ulong bits) public bool this[int index]
    {
        get => (bits & Mask(index)) != 0;
        set => bits = value ? bits | Mask(index) : bits & ~Mask(index);
    }
    static ulong Mask(int index) => 1ul << index;
}
 
public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source) public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
}

这最终看起来更像是我们所说的“基于成员”的方法,其中每个扩展成员都包含其自己的接收方规范。