18 接口

18.1 常规

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

接口可以包含方法、属性、事件和索引器。 接口本身并不提供它声明的成员的实现。 接口仅指定实现该接口的类或结构必须提供的成员。

18.2 接口声明

18.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 由一组可选的属性 (§22) 组成,后面依次跟着一组可选的 interface_modifier (§18.2.2)、一个可选的部分修饰符 (§15.2.7)、关键字 interface 以及命名接口的标识符 identifier、一个可选的 variant_type_parameter_list 规范 (§18.2.3)、一个可选的 interface_base 规范 (§18.2.4)、一个可选的 type_parameter_constraints_clause 规范 (§15.2.5)、一个 interface_body (§18.3)、一个分号(可选)。

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

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

18.2.2 接口修饰符

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

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

unsafe_modifier (§23.2) 仅在不安全代码中可用 (§23)。

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

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

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

18.2.3 变体类型参数列表

18.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),则每个分部声明必须为每个类型参数指定相同的变体。

18.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ᵢ 是输出不安全的。

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

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

18.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ᵢ 的标识转换

18.2.4 基接口

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

interface_base
    : ':' interface_type_list
    ;

显式基接口可以是构造接口类型(§8.4§18.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 中进一步讨论。

接口的每个基接口都必须是输出安全的 (§18.2.3.2)。

18.3 接口体

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

interface_body
    : '{' interface_member_declaration* '}'
    ;

18.4 接口成员

18.4.1 常规

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

interface_member_declaration
    : interface_method_declaration
    | interface_property_declaration
    | interface_event_declaration
    | interface_indexer_declaration
    ;

接口声明将声明零个或多个成员。 接口的成员必须是方法、属性、事件或索引器。 接口不能包含常量、字段、运算符、实例构造函数、终结器或类型,也不能包含任何种类的静态成员。

所有接口成员都隐式具有公共访问权限。 接口成员声明包含任何修饰符都是编译时错误。

interface_declaration 创建新的声明空间 (§7.3),interface_declaration 直接包含的类型参数和 interface_member_declaration 将新成员引入此声明空间。 以下规则适用于 interface_member_declaration

  • 接口声明的 variant_type_parameter_list 中的类型参数名称必须与同一 variant_type_parameter_list 中的所有其他类型参数名称不同,并且必须与接口的所有成员名称不同。
  • 方法的名称必须与同一接口中声明的所有属性和事件的名称不同。 此外,方法的签名 (§7.6) 必须与同一接口中声明的所有其他方法的签名不同,并且同一接口中声明的两个方法不得具有仅因 inoutref 不同的签名。
  • 属性或事件的名称必须与同一接口中声明的所有其他成员的名称不同。
  • 索引器的签名必须与同一接口中声明的所有其他索引器的签名不同。

接口的继承成员明确不属于该接口的声明空间。 因此,允许接口声明与继承成员具有相同名称或签名的成员。 发生这种情况时,派生接口成员被称为隐藏基接口成员。 隐藏继承的成员不被视为错误,但它确实会导致编译器发出警告。 要禁止警告,派生接口成员的声明必须包含 new 修饰符,以指示派生成员旨在隐藏基成员。 此主题在 §7.7.2.3 中进一步讨论。

如果在不隐藏继承成员的声明中包含 new 修饰符,会发出相应的警告。 通过删除 new 修饰符可以禁止此警告。

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

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

18.4.2 接口方法

接口方法使用 interface_method_declaration 来声明:

interface_method_declaration
    : attributes? 'new'? return_type interface_method_header
    | attributes? 'new'? ref_kind ref_return_type interface_method_header
    ;

interface_method_header
    : identifier '(' parameter_list? ')' ';'
    | identifier type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause* ';'
    ;

接口方法声明的 attributesreturn_typeref_return_typeidentifierparameter_list 与类中的方法声明具有相同的含义 (§15.6)。 接口方法声明不允许指定方法体,因此声明始终以分号结束。

接口方法的所有参数类型必须是输入安全的 (§18.2.3.2),返回类型必须是 void 或输出安全的。 此外,任何输出参数或引用参数类型也必须是输出安全的。

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

此外,方法的任何类型参数上的每个类类型约束、接口类型约束和类型参数约束都必须是输入安全的。

此外,方法的任何类型参数上的每个类类型约束、接口类型约束和类型参数约束都必须是输入安全的。

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

示例

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

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

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

class B {}
class D : B {}
class E : B {}
class C : I<D>
{
    public void M<U>() {...} 
}

...

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

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

结束示例

18.4.3 接口属性

接口属性使用 interface_property_declaration 来声明:

interface_property_declaration
    : attributes? 'new'? type identifier '{' interface_accessors '}'
    | attributes? 'new'? ref_kind type identifier '{' ref_interface_accessor '}'
    ;

interface_accessors
    : attributes? 'get' ';'
    | attributes? 'set' ';'
    | attributes? 'get' ';' attributes? 'set' ';'
    | attributes? 'set' ';' attributes? 'get' ';'
    ;

ref_interface_accessor
    : attributes? 'get' ';'
    ;

接口属性声明的 attributestypeidentifier 与类中的属性声明具有相同的含义 (§15.7)。

接口属性声明的访问器与类属性声明 (§15.7.3) 的访问器一致,只是 accessor_body 始终应为分号。 因此,这些访问器仅指示属性是读写、只读还是只写。

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

18.4.4 接口事件

接口事件使用 interface_event_declaration 来声明:

interface_event_declaration
    : attributes? 'new'? 'event' type identifier ';'
    ;

接口事件声明的 attributestypeidentifier 与类中的事件声明具有相同的含义 (§15.8)。

接口事件的类型必须是输入安全的。

18.4.5 接口索引器

接口索引器使用 interface_indexer_declaration 来声明:

interface_indexer_declaration
    : attributes? 'new'? type 'this' '[' parameter_list ']'
      '{' interface_accessors '}'
    | attributes? 'new'? ref_kind type 'this' '[' parameter_list ']'
      '{' ref_interface_accessor '}'
    ;

接口索引器声明的 attributestypeparameter_list 与类中的索引器声明具有相同的含义 (§15.9)。

接口索引器声明的访问器与类索引器声明的访问器对应 (§15.9),只是 accessor_body 必须始终是分号。 因此,这些访问器仅指示索引器是读写、只读还是只写。

接口索引器的所有参数类型必须是输入安全的 (§18.2.3.2)。 此外,任何输出参数或引用参数类型也必须是输出安全的。

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

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

18.4.6 接口成员访问

接口成员可通过形式为 的成员访问(I.M)和索引器访问(I[A])表达式进行访问,其中 I 是接口类型,M 是该接口类型的方法、属性或事件,A 是索引器参数列表。

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

示例:在以下代码中

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 的访问路径中也被隐藏。

结束示例

18.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 方法的限定接口成员名称。

结束示例

18.6 接口实现

18.6.1 常规

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

示例

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§18.2)。

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

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

结束示例

泛型类声明的基接口必须满足 §18.6.3 中描述的唯一性规则。

18.6.2 显式接口成员实现

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

示例

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

结束示例

18.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> (§18.6.7)。

18.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§18.6.2)。 尾注

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

结束示例

为了进行接口映射,当满足以下条件时,类或结构成员 A 与接口成员 B 匹配:

  • 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 实现。

结束示例

18.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

结束示例

18.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

结束示例

18.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 的实际实现。

结束示例