接口中的静态抽象成员

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。

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

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

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

总结

允许接口指定抽象的静态成员,这些成员需要由实现类和结构体通过显式或隐式的方式进行实现。 成员可以通过受接口限制的类型参数访问。

动力

目前无法对静态成员进行抽象化,并编写跨定义这些静态成员的类型应用的通用代码。 对于以静态形式存在的成员类型(尤其是运算符)来说,这个问题尤其严重。

此功能允许对数值类型使用泛型算法,这些算法由指定给定运算符存在的接口约束表示。 因此,可以使用此类运算符来表示算法:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
    static Int32 IAddable.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

语法

接口成员

此功能允许将静态接口成员声明为虚拟。

C# 11 之前的规则

在 C# 11 之前,接口中的实例成员是隐式抽象的(或虚拟的(如果有默认实现),但可以选择具有 abstract(或 virtual)修饰符。 非虚拟实例成员必须显式标记为 sealed

静态接口成员目前是隐式非虚拟成员,不允许 abstractvirtualsealed 修饰符。

建议

抽象静态成员

允许除字段以外的静态接口成员也具有 abstract 修饰符。 不允许抽象静态成员具有主体(或者在属性的情况下,不允许访问器具有主体)。

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
    static abstract bool operator ==(T l, T r);
    static abstract bool operator !=(T l, T r);
    static abstract implicit operator T(string s);
    static abstract explicit operator string(T t);
}
虚拟静态成员

允许除字段以外的静态接口成员也具有 virtual 修饰符。 虚拟静态成员必须有一个主体。

interface I<T> where T : I<T>
{
    static virtual void M() {}
    static virtual T P { get; set; }
    static virtual event Action E;
    static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
显式非虚拟静态成员

对于非虚拟实例成员的对称性,静态成员(字段除外)应允许可选的 sealed 修饰符,即使默认情况下它们是非虚拟成员:

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}

接口成员的实现

今天的规则

类和结构可以隐式或显式实现接口的抽象实例成员。 隐式实现的接口成员是类或结构中的普通成员声明(可以是虚拟的或非虚拟的),它碰巧也实现了接口成员。 该成员甚至可以从基类继承,因此甚至不存在于类声明中。

显式实现的接口成员使用了限定名称来标识相关的接口成员。 实现不能作为类或结构上的成员直接访问,但只能通过接口访问。

建议

类和结构中不需要新的语法,以方便静态抽象接口成员的隐式实现。 现有的静态成员声明用于该目的。

静态抽象接口成员的显式实现使用限定名称以及 static 修饰符。

class C : I<C>
{
    string _s;
    public C(string s) => _s = s;
    static void I<C>.M() => Console.WriteLine("Implementation");
    static C I<C>.P { get; set; }
    static event Action I<C>.E // event declaration must use field accessor syntax
    {
        add { ... }
        remove { ... }
    }
    static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
    static bool I<C>.operator ==(C l, C r) => l._s == r._s;
    static bool I<C>.operator !=(C l, C r) => l._s != r._s;
    static implicit I<C>.operator C(string s) => new C(s);
    static explicit I<C>.operator string(C c) => c._s;
}

语义学

运算符限制

今天,所有一元运算符和二元运算符声明都有一些要求,其中至少有一个操作数的类型为 TT?,其中 T 是封闭类型的实例类型。

需要放宽这些要求,允许受限操作数的类型参数被视为“封闭类型的实例类型”。

为了使类型参数 T 算作“封闭类型的实例类型”,它必须满足以下要求:

  • T 是声明运算符的接口中的直接类型参数,并且
  • T 直接受规范中所谓的“实例类型”的限制,即周围的接口及其作为类型参数的类型参数。

等式运算符和转换

接口中将允许 ==!= 运算符的抽象/虚拟声明以及隐式和显式转换运算符的抽象/虚拟声明。 也允许派生接口实现它们。

对于 ==!= 运算符,至少一个参数类型必须是一个类型参数,该参数被视为“封闭类型的实例类型”,如上一节中定义。

实现静态抽象成员

关于类或结构体中的静态成员声明何时被视为实现静态抽象接口成员,以及实现时适用的要求,其规则与实例成员相同。

TBD: 这里可能还需要一些我们尚未想到的其他或不同的规则。

作为类型参数的接口

我们讨论了 https://github.com/dotnet/csharplang/issues/5955 提出的问题,并决定添加关于将接口用作类型参数(https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts)的限制。 以下是 https://github.com/dotnet/csharplang/issues/5955 提出的限制,并获 LDM 批准。

包含或继承接口中没有大多数特定实现的静态抽象/虚拟成员的接口不能用作类型参数。 如果所有静态抽象/虚拟成员都具有最具体的实现,则可以将接口用作类型参数。

访问静态抽象接口成员

T 受接口 I 约束并且 MI可访问的静态抽象成员时,可以使用 T.M 表达式 T 在类型参数上访问静态抽象成员 M

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t + T.P;
}

在运行时,实际使用的成员实现是作为类型参数提供的实际类型上存在的实现。

C c = M<C>(); // The static members of C get called

由于查询表达式被规范为语法重写,C# 实际上允许你使用 类型 作为查询源,只要它具有使用的查询运算符的静态成员! 换句话说,如果 语法 合适,我们允许它! 我们认为,此行为在原始 LINQ 中不是有意或重要的,我们不想在类型参数上执行支持它的工作。 如果有这样的方案,我们会听说,并可以在以后选择接受。

方差安全 §18.2.3.2

差异安全规则应适用于静态抽象成员的签名。 https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety 中建议增加的内容应从

这些限制不适用于静态成员声明中类型的出现。

接收方

这些限制不适用于在非虚拟、非抽象静态成员的声明中出现类型。

§10.5.4 用户定义的隐式转换

以下要点

  • 确定类型 SS₀T₀
    • 如果 E 具有类型,请让 S 为该类型。
    • 如果 ST 是可为 null 的值类型,则令 SᵢTᵢ 为其基础类型,否则令 SᵢTᵢ 分别为 ST
    • 如果 SᵢTᵢ 是类型参数,则让 S₀T₀ 成为其有效的基类,否则分别让 S₀T₀ 分别 SₓTᵢ
  • 查找类型集 D,从中考虑用户定义的转换运算符。 此集由 S0(如果 S0 为类或结构)、S0 的基类(如果 S0 为类),T0(如果 T0 为类或结构)。
  • 查找适用的用户定义和提升转换运算符集 U。 该集合包括 D 中的类或结构体声明的用户定义和解除的隐式转换运算符,这些运算符可将包含 S 的类型转换为包含 T 的类型。 如果 U 为空,则转换未定义且发生编译时错误。

调整如下:

  • 确定类型 SS₀T₀
    • 如果 E 具有类型,请让 S 为该类型。
    • 如果 ST 是可为空的值类型,则 SᵢTᵢ 为其基础类型,否则分别让 SᵢTᵢST
    • 如果 SᵢTᵢ 是类型参数,则让 S₀T₀ 成为其有效的基类,否则分别让 S₀T₀ 分别 SₓTᵢ
  • 查找适用的用户定义和提升转换运算符集 U
    • 查找类型集 D1,从中考虑用户定义的转换运算符。 此集由 S0(如果 S0 为类或结构)、S0 的基类(如果 S0 为类),T0(如果 T0 为类或结构)。
    • 查找适用的用户定义和提升转换运算符集 U1。 该集合包括 D1 中的类或结构体声明的用户定义和解除的隐式转换运算符,这些运算符可将包含 S 的类型转换为包含 T 的类型。
    • 如果 U1 不为空,则 UU1。 否则,
      • 查找类型集 D2,从中考虑用户定义的转换运算符。 此集由 Sᵢ有效接口集 及其基接口(如果 Sᵢ 为类型参数),Tᵢ有效接口集(如果 Tᵢ 为类型参数)。
      • 查找适用的用户定义和提升转换运算符集 U2。 该集合包括 D2 中的接口声明的用户定义和解除的隐式转换运算符,这些运算符可将包含 S 的类型转换为包含 T 的类型。
      • 如果 U2 不为空,则 UU2
  • 如果 U 为空,则转换未定义且发生编译时错误。

§10.3.9 用户定义的显式转换

以下要点

  • 确定类型 SS₀T₀
    • 如果 E 具有类型,请让 S 为该类型。
    • 如果 ST 是可为 null 的值类型,则令 SᵢTᵢ 为其基础类型,否则令 SᵢTᵢST,分别。
    • 如果 SᵢTᵢ 是类型参数,则让 S₀T₀ 成为其有效的基类,否则分别让 S₀T₀ 分别 SᵢTᵢ
  • 查找类型集 D,从中考虑用户定义的转换运算符。 此集由 S0(如果 S0 为类或结构)、S0 的基类(如果 S0 为类)、T0(如果 T0 为类或结构),以及 T0 的基类(如果 T0 为类)。
  • 查找适用的用户定义和提升转换运算符集 U。 该集合由 D 中的类或结构体声明的用户定义和解除的隐式或显式转换操作符组成,这些操作符将 S 包含或涵盖的类型转换为 T 包含或涵盖的类型。 如果 U 为空,则转换未定义且发生编译时错误。

调整如下:

  • 确定类型 SS₀T₀
    • 如果 E 具有类型,请让 S 为该类型。
    • 如果 ST 是可为 null 的值类型,则使 SᵢTᵢ 为其基础类型,否则使 SᵢTᵢST
    • 如果 SᵢTᵢ 是类型参数,则让 S₀T₀ 成为其有效的基类,否则分别让 S₀T₀ 分别 SᵢTᵢ
  • 查找适用的用户定义和提升转换运算符集 U
    • 查找类型集合D1,以便从中考虑用户定义的转换运算符。 此集由 S0(如果 S0 为类或结构)、S0 的基类(如果 S0 为类)、T0(如果 T0 为类或结构),以及 T0 的基类(如果 T0 为类)。
    • 查找适用的用户定义和提升转换运算符集 U1。 该集合由 D1 中的类或结构体声明的用户定义和解除的隐式或显式转换操作符组成,这些操作符将 S 包含或涵盖的类型转换为 T 包含或涵盖的类型。
    • 如果 U1 不为空,则 UU1。 否则,
      • 查找类型集 D2,从中考虑用户定义的转换运算符。 此集由 Sᵢ有效的接口集 及其基接口(如果 Sᵢ 为类型参数),Tᵢ有效接口集 及其基接口(如果 Tᵢ 为类型参数)。
      • 查找适用的用户定义和提升转换运算符集 U2。 该集合由 D2 中的接口声明的用户定义和解除的隐式或显式转换操作符组成,这些操作符将 S 包含或涵盖的类型转换为 T 包含或涵盖的类型。
      • 如果 U2 不为空,则 UU2
  • 如果 U 为空,则转换未定义且发生编译时错误。

默认实现

此建议的 附加 功能是允许接口中的静态虚拟成员具有默认实现,就像实例虚拟/抽象成员一样。

此处的一个复杂因素是,默认实现希望“虚拟”调用其他静态虚拟成员。 如果允许在接口上直接调用静态虚拟成员,就需要在当前静态方法真正被调用的“self”类型中加入一个隐藏类型参数。 这似乎很复杂,昂贵,可能令人困惑。

我们讨论了一个更简单的版本,该版本维护了当前建议的限制,即静态虚拟成员只能在类型参数上调用 。 由于具有静态虚拟成员的接口通常具有表示“self”类型的显式类型参数,因此这不会造成很大的损失:其他静态虚拟成员只能调用该自类型。 此版本要简单得多,似乎相当可行。

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics 中,我们决定支持静态成员的默认实现,并相应地遵循/扩展 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md 中制定的规则。

模式匹配

对于下面的代码,用户可能会合理地期望它打印“True”(就像在内联代码中编写常量模式时一样):

M(1.0);

static void M<T>(T t) where T : INumberBase<T>
{
    Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
    Console.WriteLine((t is int i) && (i is 1)); 
}

但是,由于模式的输入类型不是 double,因此常量 1 模式将首先对传入的 T 进行类型检查,检查是否符合 int。 这是不直观的,因此在未来的 C# 版本中添加更好的针对 INumberBase<T> 派生类型的数字匹配处理方法之前,不会使用该功能。 为实现这一点,我们将明确声明,将 INumberBase<T> 识别为所有“数字”派生的类型,并在尝试将数值常量模式与无法表示该模式的数字类型(例如,受限于 INumberBase<T>的类型参数,或从 INumberBase<T>继承的用户定义数字类型)进行匹配时,阻止该模式。

形式上,我们在模式兼容的定义中为常量模式添加了一个例外:

常量模式用于测试表达式的结果是否等于一个常量值。 常量可以是任何常量表达式,例如文本、声明 const 变量的名称或枚举常量。 当输入值不是开放类型时,常量表达式会隐式转换为匹配表达式的类型;如果输入值的类型与常量表达式的类型不模式兼容,则模式匹配操作会出错。 如果要匹配的常量表达式是数值,则输入值是继承自 System.Numerics.INumberBase<T>的类型,并且不存在从常量表达式到输入值类型的常量转换,则模式匹配操作是错误的。

我们还为关系模式添加了类似的异常:

当输入是某个类型,并且定义了适用的内置二进制关系运算符,该运算符可将输入作为左操作数、给定的常量作为右操作数来使用时,该运算符的求值即被视为关系模式的含义。 否则,我们使用显式可为 null 或取消装箱转换将输入转换为表达式的类型。 如果不存在此类转换,则为编译时错误。 如果输入类型是受约束的类型参数或从 System.Numerics.INumberBase<T> 继承的类型,并且输入类型未定义合适的内置二进制关系运算符,则为编译时错误。 如果转换失败,则模式被视为不匹配。 如果转换成功,则模式匹配操作的结果是计算表达式 e OP v 的结果,其中 e 是转换的输入,OP 是关系运算符,v 是常量表达式。

缺点

  • “静态抽象”是一个新概念,将有意义地添加到 C# 的概念加载中。
  • 开发这个功能并不便宜。 我们应该确保这样做是值得的。

替代方案

结构约束

另一种方法是直接具有“结构约束”,并显式要求在类型参数上存在特定运算符。 其缺点是:- 每次都必须重新书写。 命名约束似乎更好。 - 这是一种全新的约束,而建议的功能利用接口约束的现有概念。 - 它只适用于运算符,而不是(轻松地)应用于其他类型的静态成员。

未解决的问题

静态抽象接口和静态类

有关详细信息,请参阅 https://github.com/dotnet/csharplang/issues/5783https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes

设计会议