扩展成员

注释

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

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

使用[ModuleInitializer]特性修饰扩展成员是一个错误。

引用指向性

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

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

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

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

OverloadResolutionPriorityAttribute

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

入口点

扩展块的方法不符合入口点候选对象(请参阅“7.1 应用程序启动”)。 注意:实现方法可能仍然是候选方法。

降低

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

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

随着实施不断进展,需要更加细化这些要求,并且可能需要在极端情况下做出妥协,以便采取合理的实施方案。

声明的元数据

各扩展声明作为具有标记方法和扩展成员的扩展类型发出。
每个扩展成员都附带一个具有修改签名的顶级静态实现方法。
扩展声明所在的静态类使用 [Extension] 属性标记。

扩展类型

源代码中的每个扩展声明都作为元数据(有时称为框架类型)中的扩展声明发出。

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

源中扩展声明中的方法/属性声明表示为元数据中扩展类型的成员。
保留原始方法的签名(包括属性),但其主体将被替换为 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 文档

扩展块的文档注释针对无法命名的类型(在下面的示例中,扩展块的 DocID 为 <>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.&lt;&gt;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.&lt;&gt;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.&lt;&gt;E__0`1.P">
            <summary>Summary for P</summary>
        </member>
        <member name="M:E.M``2(``0,``1)">
            <inheritdoc cref="M:E.&lt;&gt;E__0`1.M``1(``0)"/>
        </member>
        <member name="M:E.get_P``1(``0)">
            <inheritdoc cref="P:E.&lt;&gt;E__0`1.P"/>
        </member>
    </members>
</doc>

CREF 参考

我们可以像嵌套类型一样处理扩展块,这些块可以通过其签名进行寻址(就好像它是具有单个扩展参数的方法)。 示例:E.extension(ref int).M()

static class E
{
  extension(ref int i)
  {
    void M() { } // can be addressed by cref="E.extension(ref int).M()"
  }
  extension(ref  int i)
  {
    void M(int i2) { } // can be addressed by cref="E.extension(ref int).M(int)"
  }
}

查找过程已知会查看所有匹配的扩展块。
由于我们不允许对扩展成员进行非限定引用,因此 cref 也不允许。

语法为:

member_cref
  : conversion_operator_member_cref
  | extension_member_cref // added
  | indexer_member_cref
  | name_member_cref
  | operator_member_cref
  ;

extension_member_cref // added
 : 'extension' type_argument_list? cref_parameter_list '.' member_cref
 ;

qualified_cref
  : type '.' member_cref
  ;

cref
  : member_cref
  | qualified_cref
  | type_cref
  ;

在顶级 (extension(int).M) 使用 extension_member_cref 或嵌套在另一个扩展 (E.extension(int).extension(string).M) 中会出错。
注意:这不允许 cref 连接到扩展块,因为 E.extension(int) 引用 E 类型中名为“extension”的方法。

中断性变更

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

打开议题

  • 确认 extensionextensions 作为关键字 (答案: extension,LDM 2025-03-24)
  • 确认我们想要禁止 [ModuleInitializer] (答:是的,禁止,LDM 2025-06-11)
  • 确认我们可以放弃扩展块作为入口点候选项(答:是,放弃,LDM 2025-06-11)
  • 确认 LangVer 逻辑(跳过新扩展,而不是在选择时考虑并报告它们)(答:无条件绑定并报告 LangVer 错误(实例扩展方法除外),LDM 2025-06-11)
  • 在访问扩展成员时,我们是否应调整接收方要求? (评论)例如, new Struct() { Property = 42 }.
  • 鉴于可移植性问题重新审视分组/冲突规则:https://github.com/dotnet/roslyn/issues/79043

nameof

  • 我们是否应该像禁止经典扩展方法和新的扩展方法一样,在 nameof 中禁止使用扩展属性? (答:我们希望使用'nameof(EnclosingStaticClass.ExtensionMember)。 需要设计,可能推迟到 .NET 10 之后。 LDM 2025-06-11)

基于模式的构造

方法

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

这不包括:

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

属性和索引器

  • 扩展属性和索引器应该在何处发挥作用? (答:让我们从这四个开始,LDM 2025-05-05)

我们将包括:

  • 对象初始值设定项:new C() { ExtensionProperty = ... }
  • 字典初始值设定项:new C() { [0] = ... }
  • with: x with { ExtensionProperty = ... }
  • 属性模式:x is { ExtensionProperty: ... }

我们会排除:

  • foreach 中的 Current
  • await 中的 IsCompleted
  • Count/Length 列表模式中的属性和索引器
  • Count/Length 隐式索引器中的属性和索引器
委托返回属性
  • 确认此形状的扩展属性应仅在 LINQ 查询中发挥作用,以匹配实例属性的作用。 (答:有意义,LDM 2025-04-06)
列表和分布模式
  • 确认扩展 Index/Range 索引器应在列表模式中发挥作用
重新考虑扩展属性应用于何处Count/Length

集合表达式

  • 扩展 Add 有效
  • 扩展 GetEnumerator 适用于传播
  • 扩展 GetEnumerator 不会影响元素类型的确定(必须是实例)
  • 静态 Create 扩展方法不应算作保护的创建方法
  • 扩展可计数属性是否会影响集合表达式?

params 集合

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

字典表达式

  • 确认扩展索引器不会在字典表达式中起作用,因为索引器的存在是定义字典类型的关键部分。

extern

扩展类型的命名/编号方案

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

我们是否应该做出以下更改之一? (答:我们将调整工具并完善编号的实现,LDM 2025-05-05)

  1. 调整工具
  2. 使用一些基于内容的命名方案 (TBD)
  3. 允许通过某些语法控制名称

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

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

我们是否应该更改扩展功能以适应此方案? (答:不,这不会导致我们重新审视扩展解析设计,LDM 2025-05-05)

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

我们应该允许以下事项吗? (答:不,以后可以添加)

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> { }

为 Null 性

  • 确认当前设计,即最大可移植性/兼容性 (答案:是,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)
  • 确认扩展类型应标记为特殊名称,编译器在元数据中需要此标志(这是预览版的重大更改)( 答案:已批准,LDM 2025-06-23)
  • 确认 ref 不应包含在扩展类型名称中(需要在 WG 重审分组/冲突规则后进一步讨论,LDM 2025-06-23)
public static class E
{
  extension(ref int)
  {
    public static void M()
  }
}

发出为:

public static class E
{
  public static class <>ExtensionTypeXYZ
  {
    .. marker method ...
    void M()
  }
}

E.extension(ref int).M 的第三方 CREF 引用被发出为 M:E.<>ExtensionTypeXYZ.M()

如果 ref 从扩展参数中被移除或添加,我们可能不希望 CREF 因此中断。

静态工厂方案

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

Lookup

  • 现在,有了可读的实现名称后,我们应该如何解析实例方法调用? 我们更喜欢骨架方法相比于其对应的实现方法。
  • 如何解析静态扩展方法? (答:就像实例扩展方法一样,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;
    }
}
  • 确认即使接收方是类型,我们也希望应用优化规则(答案:在解析静态扩展成员时,应考虑仅类型扩展参数,LDM 2025-06-23)。
int.M();

static class E1
{
    extension(int)
    {
        public static void M() { }
    }
}
static class E2
{
    extension(in int i)
    {
        public static void M() => throw null;
    }
}
  • 确认当方法和属性都适用时,我们可以接受存在歧义(答案::我们应该设计一个建议,以改进现状,而不是阻止 .NET 10,LDM 2025-06-23)
  • 在确定获胜成员类型之前,确认我们不希望所有成员都优化
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-04-16)

可及性

  • 扩展声明中可访问性的含义是什么? (答:扩展声明不算作可访问性范围,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 { }
}

扩展声明验证

  • 当只有方法时,我们是否应该放宽类型参数验证(可推断性:所有类型参数都必须出现在扩展参数的类型中)? (答:是的,LDM 2025-04-06)这将允许移植 100% 的经典扩展方法。
    如果有 TResult M<TResult, TSource>(this TSource source),则可以将其移植为 extension<TResult, TSource>(TSource source) { TResult M() ... }

  • 确认是否应在扩展中允许 init-only 访问器(答:暂时不允许,LDM 2025-04-17)

  • 是否应允许接收方 ref-ness 的唯一差异extension(int receiver) { public void M2() {} }extension(ref int receiver) { public void M2() {} } (答:不,保留规范规则,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 to 接收者参数? 即使处于静态? 如何在输出中对其进行编码? 标准方式 <paramref name="..."/> 对人类可能适用,但存在一个风险,即某些现有工具在 API 的参数中找不到此标准方式时可能会不满。 (答:是的,扩展成员允许 paramref to 扩展参数,LDM 2025-05-05)
  • 我们是否应将文档注释复制到具有易读名称的实现方法中? (答:没有复制,LDM 2025-05-05)
  • 是否应从扩展容器中复制与接收者参数对应的元素到实例方法中? 其他内容是否都应从容器复制到实现方法(<typeparam> 等等)? (答:没有复制,LDM 2025-05-05)
  • 扩展成员是否应允许 <param> for 扩展参数作为替代? (答:目前不,LDM 2025-05-05)
  • 扩展块的摘要会出现在某个地方吗?

CREF

  • 确认语法 (答案:建议良好,LDM 2025-06-09)
  • 是否应该引用扩展块(E.extension(int))? (答:否,LDM 2025-06-09)
  • 是否应使用非限定语法:extension(int).Member 引用成员? (答:是的,LDM 2025-06-09)
  • 是否应使用不同的字符来表示不可辨名称,以避免 XML 转义? (答复:推迟到 WG、LDM 2025-06-09)
  • 确认是否可以同时引用主干和实现方法: E.ME.extension(int).M。这两者似乎都是必需的(经典扩展方法的扩展属性和可移植性)。 (答:是的,LDM 2025-06-09)
  • 扩展元数据名称是否会对版本文档管理造成问题?

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

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

  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) { ... }
}

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