扩展成员

注释

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

Refness

默认情况下,与其他参数一样,接收方按值传递给实例扩展成员。 但是,参数形式的扩展声明接收方可以指定 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)  { ... }
    }
}

Generates:

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

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

Checking

可推断性: 对于每个非方法扩展成员,其扩展块中的所有类型参数都必须在扩展与成员的组合参数集中得到应用。

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

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

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

重载解析优先级属性

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

入口点

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

降低

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

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

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

声明的元数据

目标

以下设计允许:

  • 通过元数据(完整程序集和引用程序集)往返扩展声明符号,
  • 对扩展成员 (xml docs) 的稳定引用,
  • 在本地确定生成名称(有助于 EnC)
  • 公共 API 监控。

对于 xml 文档,扩展成员的 docID 是元数据中扩展成员的 docID。 例如,在 cref="Extension.extension(object).M(int)" 中使用的 docID 是 M:Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32),该 docID 在重新编译和重新排序扩展块时保持稳定。 理想情况下,当扩展块的约束发生变化时,它也会保持稳定,但我们没有找到一种能够在不对成员冲突的语言设计产生不利影响的情况下实现该目标的设计。

对于 EnC 来说,非常实用的一点是,能在本地确定更新后的扩展成员在元数据中的生成位置(仅需查看修改后的扩展成员)。

对于公共 API 跟踪,更稳定的名称可降低干扰。 但在技术上,扩展分组类型名称不应在此类方案中发挥作用。 在查看扩展成员 M 时,扩展分组类型的名称并不重要,重要的是它所属扩展块的签名。 不要将公共 API 签名视为 Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32),而应视为 Extension.extension(object).M(int)。 换句话说,扩展成员应被视为具有两组类型参数和两组参数。

概述

扩展块按其 CLR 级签名进行分组。 每个 CLR 等效性组都作为具有基于内容的名称的 扩展分组类型 发出。 然后,CLR 等效性组中的扩展块按 C# 等效性进行子分组。 每个 C# 等效性组作为具有基于内容命名的扩展标记类型生成,并嵌套在其相应的扩展分组类型中。 扩展标记类型包含对扩展参数进行编码的单个 扩展标记方法 。 扩展标记方法及其包含的扩展标记类型会采用全保真方法对扩展块的签名进行编码。 每个扩展成员的声明都会在正确的扩展组类型中生成,通过特性按其名称反向引用扩展标记类型,并附有一个修改了签名的顶级静态实现方法

下面是元数据编码的架构化概述:

[Extension]
static class EnclosingStaticClass
{
    [Extension]
    public sealed class ExtensionGroupingType1 // has type parameters with minimal constraints sufficient to keep extension member declarations below valid
    {
        public static class ExtensionMarkerType1 // has re-declared type parameters with full fidelity of C# constraints
        {
            public static void <Extension>$(... extension parameter ...) // extension marker method
        }
        ... ExtensionMarkerType2, etc ...

        ... extension members for ExtensionGroupingType1, each points to its corresponding extension marker type ...
    }

    ... ExtensionGroupingType2, etc ...

    ... implementation methods ...
}

封闭静态类生成时带有 [Extension] 特性。

CLR 级签名与 C#级签名

扩展块的 CLR 级别签名由以下产生:

  • 将类型参数名称规范化为 T0T1等等...
  • 删除属性
  • 擦除参数名称
  • 擦除参数修饰符(如 refinscoped...)
  • 擦除元组名称
  • 移除可空性批注
  • 擦除notnull限制

注意:保留其他约束,例如 new()structclassallows ref structunmanaged类型约束。

扩展组类型

对于源中每个具有相同 CLR 级签名的扩展块集,会将扩展分组类型生成到元数据中。

  • 其名称无法表述,并根据 CLR 级签名的内容来确定。 下面提供了更多详细信息。
  • 其类型参数具有规范化名称(T0、、 T1...)且没有属性。
  • 它是公共的和密封的。
  • 它标有 specialname 标志和 [Extension] 属性。

扩展分组类型的基于内容的名称基于 CLR 级签名,包括以下内容:

  • 扩展参数类型的完全限定 CLR 名称。
    • 引用的类型参数名称将规范化为 T0T1等等...基于它们在类型声明中显示的顺序。
    • 完全限定名称将不包括所在程序集。 在程序集之间移动类型很常见,不应中断 xml 文档引用。
  • 将包含和排序类型参数的约束,以便重新排序源代码中的参数不会更改名称。 具体如下:
    • 类型参数约束将按声明顺序列出。 Nth 类型参数的约束将出现在 Nth+1 类型参数之前。
    • 类型约束将通过按顺序比较全名来进行排序。
    • 非类型约束按确定性排序,并进行处理,以避免与类型约束有任何歧义或冲突。
  • 由于不包括属性,因此它有意忽略 C# 特性,例如元组名称、可空性等...

注意:保证名称在重新编译、重新排序和 C# 语言特性的更改之间(即不影响 CLR 级别签名)保持稳定。

扩展标记类型

标记类型重新声明其所属的分组类型(扩展组类型)的类型参数,以完整呈现 C# 对扩展块的视图。

对于源中每个具有相同 C# 级签名的扩展块集,会将扩展标记类型生成到元数据中。

  • 其名称无法言喻,并根据扩展块的 C# 级签名内容来确定。 下面提供了更多详细信息。
  • 它重新声明其容器类的类型参数,使其与源代码中声明的类型参数一致(包括名称和属性)。
  • 它是公共和静态的。
  • 它标有 specialname 标志。

扩展标记类型的基于内容的名称基于以下内容:

  • 类型参数的名称将按照它们在扩展声明中出现的顺序包含在内。
  • 将包含和排序类型参数的属性,以便重新排序源代码中的参数不会更改名称。
  • 将包含和排序类型参数的约束,以便重新排序源代码中的参数不会更改名称。
  • 扩展类型的完全限定 C# 名称
    • 这将包括可以为 null 的批注、元组名称等项...
    • 完全限定名称将不包括所在程序集
  • 扩展参数的名称
  • 扩展参数 (ref,, ref readonlyscoped, ...) 的修饰符(按确定性顺序)
  • 以确定性顺序应用于扩展参数的任何特性的完全限定名称及其特性参数

注意:保证名称在重新编译和重新排序之间保持稳定。
注意:扩展标记类型和扩展标记方法作为参考程序集的一部分被发出。

扩展标记方法

标记方法的目的是对扩展块的扩展参数进行编码。 因为它是扩展标记类型的成员,因此可以引用扩展标记类型的重新声明的类型参数。

每个扩展标记类型都包含一个方法,即扩展标记方法。

  • 它是静态的、非泛型的、void 返回的,并被调用 <Extension>$
  • 它的单个参数具有来自扩展参数的属性、引用性、类型和名称。
    如果扩展参数未指定名称,则参数名称为空。
  • 它标有 specialname 标志。

标记方法的可访问性将是相应声明的扩展成员中限制最少的可访问性。如果没有声明任何扩展成员,则使用private

扩展成员

源中扩展块中的方法/属性声明表示为元数据中扩展分组类型的成员。

  • 保留原始方法的签名(包括属性),但其主体将被替换为 throw NotImplementedException()
  • 不应在 IL 中引用这些值。
  • 方法、属性及其访问器均通过 [ExtensionMarkerName("...")] 进行标记,该标记指向与该成员的扩展块相对应的扩展标记类型的名称。

实现方法

在源代码中,扩展块内方法/属性声明的方法体被作为顶级静态类中的静态实现方法生成。

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

ExtensionMarkerName 属性

ExtensionMarkerNameAttribute 类型仅供编译器使用,在源代码中不允许使用。 如果编译器尚未包含该类型声明,则会对其进行合成。

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
public sealed class ExtensionMarkerNameAttribute : Attribute
{
    public ExtensionMarkerNameAttribute(string name)
        => Name = name;

    public string Name { get; }
}

注意:尽管包括某些属性目标以供将来证明(扩展嵌套类型、扩展字段、扩展事件),但 AttributeTargets.构造函数不包括,因为扩展构造函数不会是构造函数。

示例:

注意:为了可读性,我们使用了简化的基于内容的名称。 注意:由于 C# 不能表示类型参数重新声明,表示元数据的代码无效 C# 代码。

下面是一个示例,说明分组的工作原理,而不使用成员:

class E
{
    extension<T>(IEnumerable<T> source)
    {
        ... member in extension<T>(IEnumerable<T> source)
    }

    extension<U>(ref IEnumerable<U?> p)
    {
        ... member in extension<U>(ref IEnumerable<U?> p)
    }

    extension<T>(IEnumerable<U> source)
        where T : IEquatable<U>
    {
        ... member in extension<T>(IEnumerable<U> source) where T : IEquatable<U>
    }
}

发出为

[Extension]
class E
{
    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IEnumerable_T<T0>
    {
        [SpecialName]
        public static class <>E__ContentName1 // note: re-declares type parameter T0 as T
        {
            [SpecialName]
            public static void <Extension>$(IEnumerable<T> source) { }
        }

        [SpecialName]
        public static class <>E__ContentName2 // note: re-declares type parameter T0 as U
        {
            [SpecialName]
            public static void <Extension>$(ref IEnumerable<U?> p) { }
        }

        [ExtensionMarkerName("<>E__ContentName1")]
        ... member in extension<T>(IEnumerable<T> source)

        [ExtensionMarkerName("<>E__ContentName2")]
        ... member in extension<U>(ref IEnumerable<U?> p)
    }

    [Extension, SpecialName]
    public sealed class <>ContentName_For_IEnumerable_T_With_Constraint<T0>
       where T0 : IEquatable<T0>
    {
        [SpecialName]
        public static class <>E__ContentName3 // note: re-declares type parameter T0 as U
        {
            [SpecialName]
            public static void <Extension>$(IEnumerable<U> source) { }
        }

        [ExtensionMarkerName("ContentName3")]
        public static bool IsPresent(U value) => throw null!;
    }

    ... implementation methods
}

下面是一个示例,展示成员的生成方式:

static class IEnumerableExtensions
{
    extension<T>(IEnumerable<T> source) where T : notnull
    {
        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
{
    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IEnumerable_T<T0>
    {
        // Extension marker type is emitted as a nested type and re-declares its type parameters to include C#-isms
        // In this example, the type parameter `T0` is re-declared as `T` with a `notnull` constraint:
        // .class <>E__IEnumerableOfT<T>.<>E__ContentName_For_IEnumerable_T_Source
        // .typeparam T
        //     .custom instance void NullableAttribute::.ctor(uint8) = (...)
        [SpecialName]
        public static class <>E__ContentName_For_IEnumerable_T_Source
        {
            [SpecialName]
            public static <Extension>$(IEnumerable<T> source) => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        public void Method() => throw null;

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        internal static int Property
        {
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            get => throw null;
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            set => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        public int Property2
        {
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            get => throw null;
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            set => throw null;
        }
    }

    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IAsyncEnumerable_Int
    {
        [SpecialName]
        public static class <>E__ContentName_For_IAsyncEnumerable_Int_Values
        {
            [SpecialName]
            public static <Extension>$(IAsyncEnumerable<int> values) => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IAsyncEnumerable_Int_Values")]
        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.<>E__MarkerContentName_For_ExtensionOfT'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__MarkerContentName_For_ExtensionOfT`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__MarkerContentName_For_ExtensionOfT`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__MarkerContentName_For_ExtensionOfT`1.P">
            <summary>Summary for P</summary>
        </member>
        <member name="M:E.M``2(``0,``1)">
            <inheritdoc cref="M:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.M``1(``0)"/>
        </member>
        <member name="M:E.get_P``1(``0)">
            <inheritdoc cref="P:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.P"/>
        </member>
    </members>
</doc>

CREF 引用

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

但是 cref 无法解决扩展块本身的问题。 E.extension(int) 可以引用类型 E中名为“extension”的方法。

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

查找过程已知会查看所有匹配的扩展块。
当我们禁止对扩展成员的不合格引用时,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_member_cref)使用extension(int).M或嵌套在另一个扩展中(E.extension(int).extension(string).M)使用extension(int).M都是错误的。

重大变化

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

打开议题

与开放问题相关的文档的临时部分,包括讨论未完成的语法和替代设计
  • 在访问扩展成员时,我们是否应调整接收方要求? (评论
  • 确认 extensionextensions 作为关键字 (答案: extension,LDM 2025-03-24)
  • 确认我们想要禁止 [ModuleInitializer] (答:是的,禁止,LDM 2025-06-11)
  • 确认我们可以放弃扩展块作为入口点候选项(答:是,放弃,LDM 2025-06-11)
  • 确认 LangVer 逻辑(跳过新扩展和选择时考虑并报告它们) (答案:无条件绑定并报告 LangVer 错误(实例扩展方法除外),LDM 2025-06-11)
  • 合并的扩展块是否需要 partial,以及是否需要合并其文档注释? (答:文档注释在块合并时以无提示方式合并,无需 partial ,通过电子邮件 2025-09-03 确认)
  • 确认成员不应以所属类型或扩展类型命名。 (答:是的,通过电子邮件确认 2025-09-03)

根据可移植性问题重新访问分组/冲突规则: https://github.com/dotnet/roslyn/issues/79043

(答:此方案已解析为具有基于内容的类型名称的新元数据设计的一部分,因此允许)

当前逻辑是对具有相同接收方类型的扩展块进行分组。 这不考虑约束。 这会导致此方案出现可移植性问题:

static class E
{
   extension<T>(ref T) where T : struct
      void M()
   extension<T>(T) where T : class
      void M()
}

该提议是使用与我们为扩展分组类型设计计划的相同分组逻辑,即考虑 CLR 级约束(即忽略 notnull、元组名称、可为 null 性批注)。

是否应在分组类型名称中编码引用性?

  • 审查未包含在扩展分组类型名称中的建议 ref (需要进一步讨论 WG 重新访问分组/冲突规则后,LDM 2025-06-23) (答:通过电子邮件确认 2025-09-03)
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.<>ExtensionGroupingTypeXYZ.M() 如果从扩展参数中移除或添加 ref,我们可能不希望 CREF 中断。

我们不太喜欢这种情况,因为任何作为扩展的用法都会存在歧义。

public static class E
{
  extension(ref int)
    static void M()
  extension(int)
    static void M()
}

但是,我们关注此方案(对于可移植性和有用性),在调整冲突规则后,这应该适用于建议的元数据设计:

static class E
{
   extension<T>(ref T) where T : struct
      void M()
   extension<T>(T) where T : class
      void M()
}

不考虑引用性有一个缺点,因为在这种情况下我们会失去可移植性:

static class E
{
   extension<T>(ref T)
      void M()
   extension<T>(T)
      void M()
}
// portability issue: since we're grouping without accounting for refness, the emitted extension members conflict (not implementation members). Mitigation: keep as classic extensions or split to another static class

nameof

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

基于模式的构造

方法

  • 新的扩展方法应该在何处发挥作用? (答:经典扩展方法发挥作用的地方,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 隐式索引器中的属性和索引器
委托返回属性
  • 确认此形状的扩展属性应仅在 LINQ 查询中发挥作用,以匹配实例属性的作用。 (答:有意义,LDM 2025-04-06)
列表和分布模式
  • 确认扩展 Index/Range 索引器应在列表模式中播放(答案:与 C# 14 无关)
重新考虑扩展属性应用于何处Count/Length

集合表达式

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

params 集合

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

字典表达式

  • 确认扩展索引器不会在字典表达式中起作用,因为索引器的存在是定义字典类型的关键部分。 (答:与 C# 14 无关)

extern

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

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

我们是否应该做出以下更改之一? (答:我们采用基于内容的命名方案来提高公共 API 稳定性,并且仍需要更新这些工具来考虑标记方法)

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

可空性

  • 确认当前设计,即最大可移植性/兼容性 (答案:是,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!;
    }

Metadata

  • 骨架方法应该抛出 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)

静态工厂方案

  • 静态方法的冲突规则是什么? (答:对封闭静态类型使用现有的 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;
    }
}
  • 确认即使接收方是类型,我们也希望应用优化规则(答案:在解析静态扩展成员时,应考虑仅类型扩展参数,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)
  • 确认在确定获胜成员种类之前,我们不希望所有成员都有一些更好的特性(答案:推迟到 .NET 10 之后,WG 2025-07-02)
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() ... }

  • 确认是否应在扩展中允许仅初始化访问器(答案:目前可以不允许,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

  • 确认语法 (答案:建议良好,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) { ... } 
}

Allows:

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

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