19 个接口

19.1 常规

接口定义协定。 实现接口的类或结构必须遵守其协定。 接口可以从多个基接口继承,类或结构可以实现多个接口。

接口可能包含各种类型的成员,如 §19.4 中所述。 接口本身可以为声明的某些或全部函数成员提供实现。 接口不提供实现的成员是抽象的。 它们的实现必须由实现接口的类或结构提供,或者由提供重写定义的派生接口提供。

注意:从历史上看,向接口添加新函数成员会影响该接口类型的所有现有使用者;这是一个中断性的变化。 添加接口函数成员实现允许开发人员升级接口,同时仍允许任何实现者重写该实现。 接口的用户可以将实现接受为非中断性变更;但是,如果它们的要求不同,则可以替代提供的实现。 尾注

19.2 接口声明

19.2.1 常规

interface_declaration 是声明新接口类型的 type_declaration (§14.7)。

interface_declaration
    : attributes? interface_modifier* 'partial'? 'interface'
      identifier variant_type_parameter_list? interface_base?
      type_parameter_constraints_clause* interface_body ';'?
    ;

interface_declaration由一组可选的属性§23)组成,后跟一组可选的interface_modifier§19.2.2),后跟可选的部分修饰符(§15.2.7),后跟关键字interface和一个用于命名接口的标识符, 后跟可选variant_type_parameter_list规范(§19.2.3),后跟可选interface_base规范(§19.2.4))、后跟可选type_parameter_constraints_clause规范(§15.2.5),后跟interface_body§19.3),可选后跟分号。

接口声明不应提供 type_parameter_constraints_clause,除非它还提供 variant_type_parameter_list

提供 variant_type_parameter_list 的接口声明是泛型接口声明。 此外,嵌套在泛型类声明或泛型结构声明中的任何接口本身都是泛型接口声明,因为需要提供包含类型的类型实参才能创建构造类型 (§8.4)。

19.2.2 接口修饰符

interface_declaration 可以选择性地包含一系列接口修饰符:

interface_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier§24.2)仅在不安全的代码(§24)中可用。

同一修饰符在接口声明中多次出现是编译时错误。

new 修饰符仅允许在类中定义的接口上使用。 它指定该接口隐藏具有相同名称的继承成员,如 §15.3.5 中所述。

publicprotectedinternalprivate 修饰符控制接口的可访问性。 根据接口声明出现的上下文,可能只允许其中一些修饰符 (§7.5.2)。 当分部类型声明 (§15.2.7) 包含可访问性规范(通过 publicprotectedinternalprivate 修饰符)时,§15.2.2 中的规则适用。

19.2.3 Variant 类型参数列表

19.2.3.1 常规

变体类型参数列表只能出现在接口和委托类型上。 与普通 type_parameter_list 的区别在于每个类型参数上都有可选的 variance_annotation

variant_type_parameter_list
    : '<' variant_type_parameter (',' variant_type_parameter)* '>'
    ;

variant_type_parameter
    : attributes? variance_annotation? type_parameter
    ;

variance_annotation
    : 'in'
    | 'out'
    ;

如果变体注释是 out,则该类型参数称为协变的。 如果变体注释是 in,则该类型参数称为逆变的。 如果没有变体注释,则该类型参数称为不变的

示例:在以下示例中:

interface C<out X, in Y, Z>
{
    X M(Y y);
    Z P { get; set; }
}

X 是协变的,Y 是逆变的,Z 是不变的。

结束示例

如果泛型接口在多个部分中声明 (§15.2.3),则每个分部声明必须为每个类型参数指定相同的变体。

19.2.3.2 方差安全

类型参数列表中变体注释的出现在类型声明中限制了类型可以出现的位置。

如果满足以下条件之一,则类型 T 是输出不安全的

  • T 是逆变类型参数
  • T 是具有输出不安全元素类型的数组类型
  • T 是由泛型 Sᵢ,... Aₑ 构建的接口或委托类型 S<Xᵢ, ... Xₑ>,其中至少有一个 Aᵢ 符合以下条件之一:
    • Xᵢ 是协变或不变的,且 Aᵢ 是输出不安全的。
    • Xᵢ 是逆变或不变的,且 Aᵢ 是输入不安全的。

如果满足以下条件之一,则类型 T 是输入不安全的

  • T 是协变类型参数
  • T 是具有输入不安全元素类型的数组类型
  • T 是由泛型 S<Aᵢ,... Aₑ> 构建的接口或委托类型 S<Xᵢ, ... Xₑ>,其中至少有一个 Aᵢ 符合以下条件之一:
    • Xᵢ 是协变或不变的,且 Aᵢ 是输入不安全的。
    • Xᵢ 是逆变或不变的,且 Aᵢ 是输出不安全的。

直观地说,输出不安全类型在输出位置被禁止,输入不安全类型在输入位置被禁止。

如果类型不是输出不安全的,则它是输出安全的;如果不是输入不安全的,则它是输入安全的

19.2.3.3 方差转换

变异注解的目的是为了实现更宽松(但仍然类型安全)的接口和委托类型转换。 为此,隐式转换 (§10.2) 和显式转换 (§10.3) 的定义使用变体可转换性的概念,其定义如下:

如果 T<Aᵢ, ..., Aᵥ> 是用变体类型参数 T<Bᵢ, ..., Bᵥ> 声明的接口或委托类型,且对于每个变体类型参数 T 满足以下条件之一,则类型 T<Xᵢ, ..., Xᵥ> 可变体转换为类型 Xᵢ

  • Xᵢ 是协变的,且存在从 AᵢBᵢ 的隐式引用转换或标识转换
  • Xᵢ 是逆变的,且存在从 BᵢAᵢ 的隐式引用转换或标识转换
  • Xᵢ 是不变的,并且存在从 AᵢBᵢ 的标识转换

19.2.4 基本接口

接口可以从零个或多个接口类型继承,这些接口类型称为该接口的显式基接口。 当接口有一个或多个显式基接口时,在该接口的声明中,接口标识符后面跟一个冒号和一个逗号分隔的基接口类型列表。

派生接口可以声明隐藏基接口中声明的继承成员(§7.7.2.3)或显式实现在基接口中声明的继承成员(§19.6.2)。的新成员。

interface_base
    : ':' interface_type_list
    ;

显式基接口可以构造接口类型 (§8.4§19.2)。 基接口本身不能是类型参数,尽管它可以涉及在范围内的类型参数。

对于构造的接口类型,显式基接口是通过在泛型类型声明中采用显式基接口声明,并用构造类型的对应 type_argument 替换基接口声明中的每个 type_parameter 来形成的。

接口的显式基接口必须至少与其自身具有相同的可访问性 (§7.5.5)。

注意:例如,在 private 接口的 internal 中指定public 接口是编译时错误。 尾注

如果接口直接或间接地从自身继承,则属于编译时错误。

接口的基接口是显式基接口及其基接口。 换而言之,基接口集是显式基接口、它们的显式基接口等的完全传递闭包。 接口继承其基接口的所有成员。

示例:在以下代码中

interface IControl
{
    void Paint();
}

interface ITextBox : IControl
{
    void SetText(string text);
}

interface IListBox : IControl
{
    void SetItems(string[] items);
}

interface IComboBox: ITextBox, IListBox {}

IComboBox 的基接口是 IControlITextBoxIListBox。 换句话说,上面的 IComboBox 接口继承成员 SetTextSetItems 以及 Paint

结束示例

在类型替换后,会继承从构造的泛型类型继承的成员。 也就是说,成员中的任何组成类型都将基类声明的类型参数替换为在 class_base 规范中使用的相应类型实参。

示例:在以下代码中

interface IBase<T>
{
    T[] Combine(T a, T b);
}

interface IDerived : IBase<string[,]>
{
    // Inherited: string[][,] Combine(string[,] a, string[,] b);
}

接口 IDerived 在类型参数 CombineT 替换后继承 string[,] 方法。

结束示例

实现接口的类或结构也隐式实现该接口的所有基接口。

分部接口声明的多个部分上的接口处理 (§15.2.7) 在 §15.2.4.3 中进一步讨论。

接口的每个基接口应是输出安全的(§19.2.3.2)。

19.3 接口正文

接口的 interface_body 定义接口的成员。

interface_body
    : '{' interface_member_declaration* '}'
    ;

19.4 接口成员

19.4.1 常规

接口的成员是从基接口继承的成员和接口本身声明的成员。

interface_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | static_constructor_declaration
    | operator_declaration
    | type_declaration
    ;

此子句使用接口的限制来增强类(§15.3)中成员的说明。 接口成员通过以下附加规则 使用 member_declaration声明:

  • 不允许 finalizer_declaration
  • 不允许实例构造函数 (constructor_declarations)。
  • 所有接口成员都隐式具有公共访问权限;但是,除了静态构造函数(§15.12)外,允许显式访问修饰符(§7.5.2)。
  • 修饰 abstract 符对于没有主体的接口函数成员隐含;该修饰符可以显式提供。
  • 接口实例函数成员,其声明包括正文是隐式 virtual 成员,除非 sealed 使用或 private 修饰符。 virtual可以显式提供修饰符。
  • private接口的或sealed函数成员应具有正文。
  • private函数成员不应具有修饰符sealed
  • 派生接口可以替代在基接口中声明的抽象或虚拟成员。
  • 显式实现的函数成员不应具有修饰符 sealed

某些声明(如 constant_declaration§15.4)在接口中没有限制。

接口的继承成员明确不属于该接口的声明空间。 因此,允许接口声明与继承成员具有相同名称或签名的成员。 发生这种情况时,派生接口成员被称为隐藏基接口成员。 隐藏继承的成员不被视为错误,但会导致警告(§7.7.2.3)。

如果在不隐藏继承成员的声明中包含 new 修饰符,会发出相应的警告。

注意:类 object 中的成员不是任何接口的成员(§19.4)。 但是,类 object 中的成员可通过任何接口类型中的成员查找获得 (§12.5)。 尾注

在多个部分中声明的接口 (§15.2.7) 的成员集是每个部分中声明的成员的并集。 接口声明的所有部分的主体共享相同的声明空间 (§7.3),每个成员的范围 (§7.7) 扩展到所有部分的主体。

示例:考虑具有成员M和属性P实现的接口IA。 实现类型C不为任MP一实现或提供实现。 必须通过编译时类型为可隐式转换为或IB隐式转换的IA接口的引用来访问它们。 无法通过成员查找类型 C变量找到这些成员。

interface IA
{
    public int P { get { return 10; } }
    public void M()
    {
        Console.WriteLine("IA.M");
    }
}

interface IB : IA
{
    public new int P { get { return 20; } }
    void IA.M()
    {
        Console.WriteLine("IB.M");
    }
}

class C : IB { }

class Test
{
    public static void Main()
    {
        C c = new C();
        ((IA)c).M();                               // cast needed
        Console.WriteLine($"IA.P = {((IA)c).P}");  // cast needed
        Console.WriteLine($"IB.P = {((IB)c).P}");  // cast needed
    }
}

在接口IA中,成员IBM可通过名称直接访问。 但是,在方法 Main中,我们不能写入 c.M()c.P,因为这些名称不可见。 若要查找它们,需要强制转换为适当的接口类型。 使用IB显式接口实现语法的M声明。 这是使该方法重写其中 IA一个方法所必需的;修饰符 override 可能不应用于函数成员。 结束示例

19.4.2 接口字段

对于接口中声明的字段,此子句增强了类 §15.5 中字段的说明。

接口字段使用 field_declarations (§15.5.1) 声明,并具有以下附加规则:

  • field_declaration声明实例字段是编译时错误

示例:以下程序包含各种类型的静态成员:

public interface IX
{
    public const int Constant = 100;
    protected static int field;

    static IX()
    {
        Console.WriteLine("static members initialized");
        Console.WriteLine($"constant = {IX.Constant}, field = {IX.field}");
        field = 50;
        Console.WriteLine("static constructor has run");
    }
}

public class Test: IX
{
    public static void Main()
    {
        Console.WriteLine($"constant = {IX.Constant}, field = {IX.field}");
    }
}

生成的输出为

static members initialized
constant = 100, field = 0
static constructor has run
constant = 100, field = 50

结束示例

有关静态字段的分配和初始化的信息,请参阅 §19.4.8

19.4.3 接口方法

对于接口中声明的方法,此子句增强了类 §15.6 中方法的说明。

接口方法是使用 method_declarations (§15.6) 声明的。 接口方法声明的属性、return_typeref_return_type标识符parameter_list与类中方法声明的属性的含义相同。 接口方法具有以下附加规则:

  • method_modifier 不得包括 override

  • 正文为分号(;)的方法 abstract; abstract 修饰符不是必需的,但允许。

  • 将块正文或表达式正文作为 method_body 的接口方法声明为 virtual; virtual 不需要修饰符,但允许。

  • 除非type_parameter_list,否则method_declaration不得具有type_parameter_constraints_clause

  • 扩展了对类方法所声明的修饰符的有效组合的要求列表,如下所示:

    • 非 extern 的静态声明应将块体或表达式正文作为 method_body
    • 非 extern 的虚拟声明应将块体或表达式正文作为 method_body
    • 非 extern 的私有声明应将块体或表达式正文作为 method_body
    • 非 extern 的密封声明应将块体或表达式体作为 method_body
    • 异步声明应将块正文或表达式正文作为 method_body
  • 接口方法的所有参数类型应为输入安全(§19.2.3.2),返回类型应 void 为输入安全类型或输出安全类型。

  • 任何输出或引用参数类型也应是输出安全的。

    注意:由于常见的实现限制,输出参数需要是输入安全的。 尾注

  • 该方法的任何类型参数上的每个类类型约束、接口类型约束和类型参数约束应是输入安全的。

这些规则确保接口的任何协变或逆变用法都保持类型安全。

示例

interface I<out T>
{
    void M<U>() where U : T;     // Error
}

格式不正确,因为将 T 用作 U 上的类型参数约束不是输入安全的。

如果没有此限制,可能会以以下方式违反类型安全:

interface I<out T>
{
    void M<U>() where U : T;
}
class B {}
class D : B {}
class E : B {}
class C : I<D>
{
    public void M<D>() {...} 
}

...

I<B> b = new C();
b.M<E>();

这实际上是对 C.M<E> 的调用。 但该调用要求 ED 派生,因此此处会违反类型安全。

结束示例

注意:有关示例 ,请参阅 §19.4.2 ,该示例不仅显示了具有实现的静态方法,而且当该方法被调用 Main 并具有正确的返回类型和签名时,它也是入口点。 尾注

在接口中声明的实现的虚拟方法可以重写为派生接口中的抽象方法。 这称为 重新缩进

示例

interface IA
{
    void M() { Console.WriteLine("IA.M"); }
}

interface IB: IA
{
    abstract void IA.M();    // reabstraction of M
}

这在派生接口中非常有用,其中方法的实现不合适,并且应通过实现类来提供更适当的实现。 结束示例

19.4.4 接口属性

对于接口中声明的属性,此子句增加了类 §15.7 中属性的说明。

接口属性使用 property_declarations (§15.7.1) 声明,并具有以下附加规则:

  • property_modifier 不得包括 override

  • 显式接口成员实现不应包含 accessor_modifier§15.7.3)。

  • 派生接口可以显式实现在基接口中声明的抽象接口属性。

    注意:由于接口不能包含实例字段,因此接口属性不能是实例自动属性,因为需要声明隐式隐藏实例字段。 尾注

  • 如果存在 get 访问器,接口属性的类型必须是输出安全的;如果存在 set 访问器,必须是输入安全的。

  • 将块正文或表达式正文作为 method_body 的接口方法声明为 virtual; virtual 不需要修饰符,但允许。

  • 没有实现的实例property_declaration;abstractabstract不需要修饰符,但允许。 它 从不 被视为自动实现的属性(§15.7.4)。

19.4.5 接口事件

对于接口中声明的事件,此子句增强了类 §15.8 中事件的说明。

接口事件使用 event_declarations (§15.8.1)进行声明,并使用以下附加规则:

  • event_modifier 不得包括 override
  • 派生接口可以实现在基接口(§15.8.5)中声明的抽象接口事件。
  • 实例中variable_declarators包含任何variable_initializerevent_declaration编译时错误。
  • 具有或sealed修饰符的virtual实例事件必须声明访问器。 它 从不 被视为自动实现的类似字段的事件(§15.8.2)。
  • 具有修饰符的 abstract 实例事件不得声明访问器。
  • 接口事件的类型必须是输入安全的。

19.4.6 接口索引器

对于接口中声明的索引器,此子句将增强类中索引器 的说明 §15.9

接口索引器使用 indexer_declarations(§15.9)进行声明,并使用以下附加规则:

  • indexer_modifier 不得包括 override

  • 具有表达式正文或包含具有块正文或表达式正文的访问器的indexer_declarationvirtual;virtual不需要修饰符,但允许。

  • 一个indexer_declaration,其访问器主体是分号(;);abstractabstract修饰符不是必需的,但允许。

  • 接口索引器的所有参数类型应为输入安全(§19.2.3.2)。

  • 任何输出或引用参数类型也应是输出安全的。

    注意:由于常见的实现限制,输出参数需要是输入安全的。 尾注

  • 如果存在 get 访问器,接口索引器的类型必须是输出安全的;如果存在 set 访问器,必须是输入安全的。

19.4.7 接口运算符

对于接口中声明的运算符,此子句扩充了类 §15.10operator_declaration成员的说明。

接口中的 operator_declaration 是实现(§19.1)。

它是接口声明转换、相等或不相等运算符的编译时错误。

19.4.8 接口静态构造函数

对于接口中声明的静态构造函数,此子句增强了类 §15.12 中静态构造函数的说明。

封闭的 (§8.4.3) 接口的静态构造函数在给定的应用程序域中最多执行一次。 静态构造函数的执行由以下作中的第一个作在应用程序域中触发:

  • 将引用接口的任何静态成员。
  • Main在调用该方法之前,为包含Main开始执行的方法(§7.1)的接口调用该方法。
  • 该接口为成员提供实现,该实现作为该成员的最具体实现(§19.4.10)进行访问。

注意:如果上述任何作均未发生,则接口的静态构造函数可能不会对创建和使用接口的类型实例的程序执行。 尾注

若要初始化新的封闭接口类型,请先为该特定封闭类型创建一组新的静态字段。 每个静态字段都初始化为其默认值。 接下来,为这些静态字段执行静态字段初始值设定项。 最后,执行静态构造函数。

注意:有关使用接口中声明的各种静态成员(包括 Main 方法)的示例,请参阅 §19.4.2尾注

19.4.9 接口嵌套类型

对于接口中声明的嵌套类型,此子句增强了类 §15.3.9 中嵌套类型的说明。

在使用 variance_annotation§19.2.3.1)声明的类型参数的范围内声明类类型、结构类型或枚举类型是错误的。

示例:下面的声明 C 是一个错误。

interface IOuter<out T>
{
    class C { } // error: class declaration within scope of variant type parameter 'T'
}

结束示例

19.4.10 最具体的实现

每个类和结构应为每个在类型中显示的实现或其直接和间接接口中由该类型实现的所有接口中声明的每个虚拟成员具有最具体的实现。 最具体的实现是比所有其他实现更具体的唯一实现。

注意:最具体的实现规则可确保在发生冲突时由程序员显式解决钻石接口继承导致的歧义性。 尾注

对于作为结构或实现接口I2I3的类的类型T,以及从I3I2声明成员M的接口直接或间接派生的类I,最具体的实现M是:

  • 如果 T 声明其 I.M实现,则实现是最具体的实现。
  • 否则,如果 T 类和直接或间接基类声明实现 I.M,则最派生的 T 基类是最具体的实现。
  • 否则,如果I2I3接口是直接或间接T实现I2I3接口,I3.M则比它更具体。I2.M
  • 否则,既I2.MI3.M不更具体,也不会发生错误。

示例

interface IA
{
    void M() { Console.WriteLine("IA.M"); }
}

interface IB : IA
{
    void IA.M() { Console.WriteLine("IB.M"); }
}

interface IC: IA
{
    void IA.M() { Console.WriteLine("IC.M"); }
}

abstract class C: IB, IC { } // error: no most specific implementation for 'IA.M'

abstract class D: IA, IB, IC // OK
{
    public abstract void M();
}

最具体的实现规则可确保在发生冲突时由程序员显式解决冲突(即钻石继承导致的歧义)。 结束示例

19.4.11 接口成员访问

接口成员通过表单I.MI[A]的成员访问(§12.8.7)和索引器访问(§12.8.12.4)表达式进行访问,其中I接口类型M是该接口类型的常量、字段、方法、属性或事件,并且A是索引器参数列表。

在具有直接或间接基类B的类D中,如果B直接或间接实现接口II定义方法M(),则表达式base.M()仅在静态 (§12.3) 绑定到类类型的实现M()base.M()有效。

对于严格单一继承的接口(继承链中的每个接口完全具有零个或一个直接基接口),成员查找(§12.5)、方法调用的影响(§12.8.10)。 2)和索引器访问(§12.8.12.4)规则与类和结构完全相同:更多派生成员隐藏具有相同名称或签名的派生成员。 但是,对于多重继承接口,当两个或多个不相关的基接口声明具有相同名称或签名的成员时,可能会出现歧义。 本小节展示了几个示例,其中一些会导致歧义,另一些则不会。 在所有情况下,都可以使用显式强制转换来解决歧义。

示例:在以下代码中

interface IList
{
    int Count { get; set; }
}

interface ICounter
{
    int Count { get; set; }
}

interface IListCounter : IList, ICounter {}

class C
{
    void Test(IListCounter x)
    {
        x.Count = 1;             // Error
        ((IList)x).Count = 1;    // Ok, invokes IList.Count.set
        ((ICounter)x).Count = 1; // Ok, invokes ICounter.Count
    }
}

第一个语句导致编译时错误,因为在 中对 Count 的成员查找 (IListCounter) 存在歧义。 如示例所示,通过将 x 强制转换为适当的基接口类型来解决歧义。 此类转换没有运行时成本 - 它们仅仅是在编译时将实例视为派生程度更小的类型。

结束示例

示例:在以下代码中

interface IInteger
{
    void Add(int i);
}

interface IDouble
{
    void Add(double d);
}

interface INumber : IInteger, IDouble {}

class C
{
    void Test(INumber n)
    {
        n.Add(1);             // Invokes IInteger.Add
        n.Add(1.0);           // Only IDouble.Add is applicable
        ((IInteger)n).Add(1); // Only IInteger.Add is a candidate
        ((IDouble)n).Add(1);  // Only IDouble.Add is a candidate
    }
}

调用 n.Add(1) 通过应用 IInteger.Add 的重载解析规则来选择 。 同样,调用 n.Add(1.0) 将选择 IDouble.Add。 当插入显式转换后,只有一种候选方法,因此不会产生歧义。

结束示例

示例:在以下代码中

interface IBase
{
    void F(int i);
}

interface ILeft : IBase
{
    new void F(int i);
}

interface IRight : IBase
{
    void G();
}

interface IDerived : ILeft, IRight {}

class A
{
    void Test(IDerived d)
    {
        d.F(1);           // Invokes ILeft.F
        ((IBase)d).F(1);  // Invokes IBase.F
        ((ILeft)d).F(1);  // Invokes ILeft.F
        ((IRight)d).F(1); // Invokes IBase.F
    }
}

IBase.F 成员被 ILeft.F 成员隐藏。 因此,调用 d.F(1) 选择 ILeft.F,即使在通过 IBase.F 的访问路径中 IRight 似乎没有被隐藏。

多重继承接口中隐藏的直观规则很简单:如果成员在任何访问路径中被隐藏,则它在所有访问路径中都被隐藏。 因为从 IDerivedILeftIBase 的访问路径隐藏了 IBase.F,所以该成员在从 IDerivedIRightIBase 的访问路径中也被隐藏。

结束示例

19.5 限定接口成员名称

接口成员有时用其限定的接口成员名称来引用。 接口成员的限定名称由声明该成员的接口的名称、后跟一个点、再后跟该成员的名称组成。 成员的限定名称引用声明该成员的接口。

示例:假设有以下声明:

interface IControl
{
    void Paint();
}

interface ITextBox : IControl
{
    void SetText(string text);
}

Paint 的限定名称是 IControl.Paint,SetText 的限定名称是 ITextBox.SetText。 在上面的示例中,不能将 Paint 称为 ITextBox.Paint

结束示例

当接口是命名空间的一部分时,限定接口成员名称可以包含命名空间名称。

示例

namespace System
{
    public interface ICloneable
    {
        object Clone();
    }
}

System 命名空间中,ICloneable.CloneSystem.ICloneable.Clone 都是 Clone 方法的限定接口成员名称。

结束示例

19.6 接口实现

19.6.1 常规

接口可由类和结构实现。 为了指示类或结构直接实现接口,该接口包含在类或结构的基类列表中。

实现接口I的类或结构C必须为该C接口中I声明的每个成员提供或继承实现。 公共成员 I 可以在公共成员 C中定义。 可以在使用显式接口实现(§19.6.2)中C定义在其中声明IC的非公共成员。

派生类型中满足接口映射(§19.6.5)但未实现匹配基接口成员的成员引入了新成员。 当需要显式接口实现来定义接口成员时,会出现这种情况。

示例

interface ICloneable
{
    object Clone();
}

interface IComparable
{
    int CompareTo(object other);
}

class ListEntry : ICloneable, IComparable
{
    public object Clone() {...}    
    public int CompareTo(object other) {...}
}

结束示例

直接实现接口的类或结构也隐式实现该接口的所有基接口。 即使类或结构未在基类列表中显式列出所有基接口,也是如此。

示例

interface IControl
{
    void Paint();
}

interface ITextBox : IControl
{
    void SetText(string text);
}

class TextBox : ITextBox
{
    public void Paint() {...}
    public void SetText(string text) {...}
}

此处,类 TextBox 同时实现 IControlITextBox

结束示例

当类 C 直接实现接口时,从 C 派生的所有类也隐式实现该接口。

类声明中指定的基接口可以构造接口类型 (§8.4§19.2)。

示例:以下代码说明类如何实现构造接口类型:

class C<U, V> {}
interface I1<V> {}
class D : C<string, int>, I1<string> {}
class E<T> : C<int, T>, I1<T> {}

结束示例

泛型类声明的基接口应满足 §19.6.3 中所述的唯一性规则。

19.6.2 显式接口成员实现

为了实现接口,类、结构或接口可以声明 显式接口成员实现。 显式接口成员实现是引用限定接口成员名称的方法、属性、事件或索引器声明。 在基接口中实现非公共成员的类或结构必须声明显式接口成员实现。 在基接口中实现成员的接口必须声明显式接口成员实现。

满足接口映射(§19.6.5)的派生接口成员隐藏基接口成员(§7.7.2)。 除非存在修饰符, new 否则编译器应发出警告。

示例

interface IList<T>
{
    T[] GetElements();
}

interface IDictionary<K, V>
{
    V this[K key] { get; }
    void Add(K key, V value);
}

class List<T> : IList<T>, IDictionary<int, T>
{
    public T[] GetElements() {...}
    T IDictionary<int, T>.this[int index] {...}
    void IDictionary<int, T>.Add(int index, T value) {...}
}

此处 IDictionary<int,T>.thisIDictionary<int,T>.Add 是显式接口成员实现。

结束示例

示例:在某些情况下,接口成员的名称可能不适合实现类,在这种情况下,可以使用显式接口成员实现来实现接口成员。 例如,实现文件抽象的类可能会实现具有释放文件资源效果的 Close 成员函数,并使用显式接口成员实现来实现 Dispose 接口的 IDisposable 方法:

interface IDisposable
{
    void Dispose();
}

class MyFile : IDisposable
{
    void IDisposable.Dispose() => Close();

    public void Close()
    {
        // Do what's necessary to close the file
        System.GC.SuppressFinalize(this);
    }
}

结束示例

无法通过方法调用、属性访问、事件访问或索引器访问中的限定接口成员名称来访问显式接口成员实现。 显式接口实例成员实现只能通过接口实例进行访问,并且在这种情况下,只需由其成员名称引用即可。 只能通过接口名称访问显式接口静态成员实现。

显式接口成员实现包含除 extern 之外的任何修饰符 (async) 是一个编译时错误。

显式接口方法实现从接口继承任何类型参数约束。

显式接口方法实现中的type_parameter_constraints_clause只能由应用于classstruct组成,根据继承的约束,这些类型参数已知分别为引用类型或值类型。 显式接口方法实现的签名中任何形式为 T? 的类型(其中 T 是类型参数)的解释如下:

  • 如果为类型参数 class 添加了 T 约束,则 T? 是可空引用类型;否则
  • 无论是没有添加约束,还是添加了struct约束,对于类型参数TT?都是可为 null 的值类型。

示例:下面演示了在涉及类型参数时规则的工作原理:

#nullable enable
interface I
{
    void Foo<T>(T? value) where T : class;
    void Foo<T>(T? value) where T : struct;
}

class C : I
{
    void I.Foo<T>(T? value) where T : class { }
    void I.Foo<T>(T? value) where T : struct { }
}

如果没有类型参数约束 where T : class,则具有引用类型参数的基方法无法被重写。 结束示例

注意:显式接口成员实现与其他成员具有不同的可访问性特征。 由于显式接口成员实现永远不能通过方法调用或属性访问中的限定接口成员名称来访问,因此从某种意义上说它们是专用的。 但是,由于它们可以通过接口访问,因此从某种意义上说它们也与声明它们的接口一样公共。 显式接口成员实现具有以下两个主要用途:

  • 由于显式接口成员实现不能通过类或结构实例访问,因此它们允许将接口实现从类或结构的公共接口中排除。 当类或结构实现对该类或结构的使用者无关紧要的内部接口时,这特别有用。
  • 显式接口成员实现允许消除具有相同签名的接口成员的歧义。 如果没有显式接口成员实现,类、结构或接口不可能具有具有相同签名和返回类型的接口成员的不同实现,类、结构或接口不可能在所有具有相同签名但具有不同返回类型的接口成员上拥有任何实现。

尾注

要使显式接口成员实现有效,类、结构或接口应在其基类或基接口列表中命名一个接口,该接口包含其限定接口成员名称、类型、类型、类型参数数和参数类型与显式接口成员实现的接口完全匹配。 如果接口函数成员有参数数组,则关联的显式接口成员实现的相应参数允许但不要求具有 params 修饰符。 如果接口函数成员没有参数数组,则关联的显式接口成员实现不得有参数数组。

示例:因此,在以下类中

class Shape : ICloneable
{
    object ICloneable.Clone() {...}
    int IComparable.CompareTo(object other) {...} // invalid
}

IComparable.CompareTo 的声明导致编译时错误,因为 IComparable 未在 Shape 的基类列表中列出,也不是 ICloneable 的基接口。 同样,在声明中

class Shape : ICloneable
{
    object ICloneable.Clone() {...}
}

class Ellipse : Shape
{
    object ICloneable.Clone() {...} // invalid
}

ICloneable.CloneEllipse 的声明导致编译时错误,因为 ICloneable 未在 Ellipse 的基类列表中显式列出。

结束示例

显式接口成员实现的限定接口成员名称应引用在其中声明该成员的接口。

示例:因此,在声明中

interface IControl
{
    void Paint();
}

interface ITextBox : IControl
{
    void SetText(string text);
}

class TextBox : ITextBox
{
    void IControl.Paint() {...}
    void ITextBox.SetText(string text) {...}
}

Paint 的显式接口成员实现必须编写为 IControl.Paint,而不是 ITextBox.Paint

结束示例

19.6.3 实现接口的唯一性

泛型类型声明实现的接口对于所有可能的构造类型必须保持唯一。 如果没有此规则,将无法确定某些构造类型要调用的正确方法。

示例:假设允许编写如下泛型类声明:

interface I<T>
{
    void F();
}

class X<U ,V> : I<U>, I<V> // Error: I<U> and I<V> conflict
{
    void I<U>.F() {...}
    void I<V>.F() {...}
}

如果允许这样做,在以下情况下将无法确定要执行的代码:

I<int> x = new X<int, int>();
x.F();

结束示例

要确定泛型类型声明的接口列表是否有效,执行以下步骤:

  • L 为泛型类、结构或接口声明 C 中直接指定的接口列表。
  • L 中已有接口的任何基接口添加到 L 中。
  • L 中删除任何重复项。
  • 如果从 C 创建的任何可能的构造类型在将类型实参替换到 L 中后,导致 L 中的两个接口相同,则 C 的声明无效。 确定所有可能的构造类型时,不考虑约束声明。

注意:在上面的类声明 X 中,接口列表 Ll<U>I<V> 组成。 该声明无效,因为任何 UV 为相同类型的构造类型都会导致这两个接口成为相同类型。 尾注

在不同继承级别指定的接口可能会统一:

interface I<T>
{
    void F();
}

class Base<U> : I<U>
{
    void I<U>.F() {...}
}

class Derived<U, V> : Base<U>, I<V> // Ok
{
    void I<V>.F() {...}
}

即使 Derived<U,V> 同时实现 I<U>I<V>,此代码也是有效的。 代码

I<int> x = new Derived<int, int>();
x.F();

调用方法 Derived,因为 Derived<int,int>' 有效地重新实现 I<int>§19.6.7)。

19.6.4 泛型方法的实现

当泛型方法隐式实现接口方法时,每个方法类型参数的给定约束在两个声明中必须等效(在任何接口类型参数被替换为适当的类型实参之后),其中方法类型参数按从左到右的顺序通过序号位置标识。

示例:在以下代码中:

interface I<X, Y, Z>
{
    void F<T>(T t) where T : X;
    void G<T>(T t) where T : Y;
    void H<T>(T t) where T : Z;
}

class C : I<object, C, string>
{
    public void F<T>(T t) {...}                  // Ok
    public void G<T>(T t) where T : C {...}      // Ok
    public void H<T>(T t) where T : string {...} // Error
}

方法 C.F<T> 隐式实现 I<object,C,string>.F<T>。 在这种情况下,C.F<T> 不需要(也不允许)指定约束 T: object,因为 object 是所有类型参数的隐式约束。 方法 C.G<T> 隐式实现 I<object,C,string>.G<T>,因为在接口类型参数被替换为相应的类型实参后,约束与接口中的约束匹配。 方法 C.H<T> 的约束是错误的,因为密封类型(在这种情况下为 string)不能用作约束。 省略约束也会出错,因为隐式接口方法实现的约束必须匹配。 因此,无法隐式实现 I<object,C,string>.H<T>。 此接口方法只能使用显式接口成员实现来实现:

class C : I<object, C, string>
{
    ...
    public void H<U>(U u) where U : class {...}

    void I<object, C, string>.H<T>(T t)
    {
        string s = t; // Ok
        H<T>(t);
    }
}

在这种情况下,显式接口成员实现调用具有严格较弱约束的公共方法。 从 t 到 s 的赋值有效,因为 T 继承了 T: string 的约束,即使此约束在源代码中无法表示。 结束示例

注意:当泛型方法显式实现接口方法时,不允许对实现方法使用约束(§15.7.1§19.6.2)。 尾注

19.6.5 接口映射

类或结构应提供类或结构基类列表中所列接口的所有抽象成员的实现。 在实现类或结构中查找接口成员的实现的过程称为接口映射

类或结构 C 的接口映射会为 C 的基类列表中指定的每个接口的每个成员定位一个实现。 特定接口成员 I.M的实现(其中 I 声明成员 M 的接口)通过检查每个类、接口或结构 S来确定,从每个连续基类开始 C 并重复每个基类并实现接口 C,直到匹配位于:

  • 如果 S 包含与 IM 匹配的显式接口成员实现的声明,则此成员是 I.M 的实现。
  • 否则,如果 S 包含与 M 匹配的非静态公共成员的声明,则此成员是 I.M 的实现。 如果多个成员匹配,则未指定哪个成员是 I.M 的实现。 这种情况只能发生在 S 是构造类型的情况下,其中在泛型类型中声明的两个成员具有不同的签名,但类型实参使其签名相同。

如果无法为 C 基类列表中指定的所有接口的所有成员找到实现,则会发生编译时错误。 接口的成员包括从基接口继承的那些成员。

构造接口类型的成员被视为具有按照 §15.3.3 中指定的用相应类型实参替换的任何类型参数。

示例:例如,给定泛型接口声明:

interface I<T>
{
    T F(int x, T[,] y);
    T this[int y] { get; }
}

构造接口 I<string[]> 具有以下成员:

string[] F(int x, string[,][] y);
string[] this[int y] { get; }

结束示例

为了进行接口映射,类、接口或结构成员AB在:

  • AB 是方法,且 AB 的名称、类型和参数列表相同。
  • AB 是属性,AB 的名称和类型相同,AB 具有相同的访问器(如果 A 不是显式接口成员实现,则允许它具有额外的访问器)。
  • AB 是事件,AB 的名称和类型相同。
  • AB 是索引器,AB 的类型和参数列表相同,且 AB 具有相同的访问器(如果不是显式接口成员实现,则允许 A 具有额外的访问器)。

接口映射算法的显著含义是:

  • 在确定实现接口成员的类或结构成员时,显式接口成员实现优先于同一类或结构中的其他成员。
  • 非公共成员和静态成员都不参与接口映射。

示例:在以下代码中

interface ICloneable
{
    object Clone();
}

class C : ICloneable
{
    object ICloneable.Clone() {...}
    public object Clone() {...}
}

ICloneable.CloneC 成员成为 CloneICloneable 的实现,因为显式接口成员实现优先于其他成员。

结束示例

如果类或结构实现两个或多个包含具有相同名称、类型和参数类型的成员的接口,则可以将每个这些接口成员映射到单个类或结构成员。

示例

interface IControl
{
    void Paint();
}

interface IForm
{
    void Paint();
}

class Page : IControl, IForm
{
    public void Paint() {...}
}

此处,PaintIControlIForm 方法都映射到 Paint 中的 Page 方法。 当然,也可以为这两个方法提供单独的显式接口成员实现。

结束示例

如果类或结构实现包含隐藏成员的接口,则某些成员可能需要通过显式接口成员实现来实现。

示例

interface IBase
{
    int P { get; }
}

interface IDerived : IBase
{
    new int P();
}

此接口的实现至少需要一个显式接口成员实现,并且采用以下形式之一

class C1 : IDerived
{
    int IBase.P { get; }
    int IDerived.P() {...}
}
class C2 : IDerived
{
    public int P { get; }
    int IDerived.P() {...}
}
class C3 : IDerived
{
    int IBase.P { get; }
    public int P() {...}
}

结束示例

当类实现多个具有相同基接口的接口时,基接口只能有一个实现。

示例:在以下代码中

interface IControl
{
    void Paint();
}

interface ITextBox : IControl
{
    void SetText(string text);
}

interface IListBox : IControl
{
    void SetItems(string[] items);
}

class ComboBox : IControl, ITextBox, IListBox
{
    void IControl.Paint() {...}
    void ITextBox.SetText(string text) {...}
    void IListBox.SetItems(string[] items) {...}
}

不可能为基类列表中命名的 IControl、由 IControl 继承的 ITextBox 以及由 IControl 继承的 IListBox 提供单独的实现。 实际上,这些接口没有单独的标识概念。 相反,ITextBoxIListBox 的实现共享 IControl 的相同实现,并且 ComboBox 仅被视为实现三个接口:IControlITextBoxIListBox

结束示例

基类的成员参与接口映射。

示例:在以下代码中

interface Interface1
{
    void F();
}

class Class1
{
    public void F() {}
    public void G() {}
}

class Class2 : Class1, Interface1
{
    public new void G() {}
}

F 中的方法 Class1 用于 Class2'sInterface1 实现。

结束示例

19.6.6 接口实现继承

类继承其基类提供的所有接口实现。

派生类如果不显式重新实现接口,就无法以任何方式更改从其基类继承的接口映射。

示例:在声明中

interface IControl
{
    void Paint();
}

class Control : IControl
{
    public void Paint() {...}
}

class TextBox : Control
{
    public new void Paint() {...}
}

Paint 中的 TextBox 方法隐藏 Paint 中的 Control 方法,但它不会更改 Control.PaintIControl.Paint 的映射,通过类实例和接口实例对 Paint 的调用将产生以下效果

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();  // invokes Control.Paint();
t.Paint();  // invokes TextBox.Paint();
ic.Paint(); // invokes Control.Paint();
it.Paint(); // invokes Control.Paint();

结束示例

但是,当接口方法映射到类中的虚拟方法时,派生类可以重写该虚拟方法并更改接口的实现。

示例:将上面的声明重写为

interface IControl
{
    void Paint();
}

class Control : IControl
{
    public virtual void Paint() {...}
}

class TextBox : Control
{
    public override void Paint() {...}
}

现在将观察到以下效果

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();  // invokes Control.Paint();
t.Paint();  // invokes TextBox.Paint();
ic.Paint(); // invokes Control.Paint();
it.Paint(); // invokes TextBox.Paint();

结束示例

由于显式接口成员实现不能声明为虚拟的,因此不可能重写显式接口成员实现。 但是,显式接口成员实现调用另一个方法是完全有效的,并且该另一个方法可以声明为虚拟的,以允许派生类重写它。

示例

interface IControl
{
    void Paint();
}

class Control : IControl
{
    void IControl.Paint() { PaintControl(); }
    protected virtual void PaintControl() {...}
}

class TextBox : Control
{
    protected override void PaintControl() {...}
}

在此处,派生自 Control 的类可以通过替代 IControl.Paint 方法来专门实现 PaintControl

结束示例

19.6.7 接口重新实现

继承接口实现的类允许通过将接口包含在基类列表中来重新实现该接口。

接口的重新实现遵循与接口的初始实现完全相同的接口映射规则。 因此,继承接口映射对为接口的重新实现建立的接口映射没有任何影响。

示例:在声明中

interface IControl
{
    void Paint();
}

class Control : IControl
{
    void IControl.Paint() {...}
}

class MyControl : Control, IControl
{
    public void Paint() {}
}

ControlIControl.Paint 映射到 Control.IControl.Paint 这一事实并不影响 MyControl 中的重新实现,后者会将 IControl.Paint 映射到 MyControl.Paint

结束示例

继承的公共成员声明和继承的显式接口成员声明参与重新实现的接口的接口映射过程。

示例

interface IMethods
{
    void F();
    void G();
    void H();
    void I();
}

class Base : IMethods
{
    void IMethods.F() {}
    void IMethods.G() {}
    public void H() {}
    public void I() {}
}

class Derived : Base, IMethods
{
    public void F() {}
    void IMethods.H() {}
}

此处,IMethodsDerived 的实现将接口方法映射到 Derived.FBase.IMethods.GDerived.IMethods.HBase.I

结束示例

当类实现接口时,它也隐式实现该接口的所有基接口。 同样,接口的重新实现也隐式是该接口的所有基接口的重新实现。

示例

interface IBase
{
    void F();
}

interface IDerived : IBase
{
    void G();
}

class C : IDerived
{
    void IBase.F() {...}
    void IDerived.G() {...}
}

class D : C, IDerived
{
    public void F() {...}
    public void G() {...}
}

此处,IDerived 的重新实现也重新实现 IBase,将 IBase.F 映射到 D.F

结束示例

19.6.8 抽象类和接口

与非抽象类一样,抽象类应提供类基类列表中所列接口的所有抽象成员的实现。 但是,抽象类允许将接口方法映射到抽象方法。

示例

interface IMethods
{
    void F();
    void G();
}

abstract class C : IMethods
{
    public abstract void F();
    public abstract void G();
    }

在此处,IMethods 的实现会将 FG 映射到抽象方法,这些方法应在从 C 派生的非抽象类中被替代。

结束示例

显式接口成员实现不能是抽象的,但显式接口成员实现当然可以调用抽象方法。

示例

interface IMethods
{
    void F();
    void G();
}

abstract class C: IMethods
{
    void IMethods.F() { FF(); }
    void IMethods.G() { GG(); }
    protected abstract void FF();
    protected abstract void GG();
}

在此处,替代 CFF 需要派生自 GG 的非抽象类,因此会提供 IMethods 的实际实现。

结束示例