1. 接口

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

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

1.1 接口声明

interface-declaration 是用于声明新的接口类型的 type-declaration(第 9.6 节)。

interface-declaration:
attributesopt  
interface-modifiersopt  
partialopt   interface  
          identifier   variant-type-parameter-listopt   interface-baseopt
          type-parameter-constraints-clausesopt   interface-body   ;opt

interface-declaration 由以下内容组成:一组可选 attributes(第 17 章),然后依次是一组可选 interface-modifiers(第 13.1.1 节)、可选 partial 修饰符、关键字 interface 和用于命名接口的 identifier、可选 variant-type-parameter-list 规范(第 13.1.3 节)、可选 interface-base 规范(第 13.1.4 节)、可选 type-parameter-constraints-clauses 规范(第 10.1.5 节)、interface-body(第 13.1.5 节),最后是一个分号(可选)。

1.1.1 接口修饰符

interface-declaration 可以根据需要包含一个接口修饰符序列:

interface-modifiers:
interface-modifier
interface-modifiers   interface-modifier

interface-modifier:
new
public
protected
internal
private

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

new 修饰符仅允许在类中定义的接口中使用。它指定接口隐藏同名的继承成员,详见第 10.3.4 节中的介绍。

public、protected、internal 和 private 修饰符将控制接口的可访问性。根据接口声明所在的上下文,只允许使用这些修饰符中的一部分(第 3.5.1 节)。

1.1.2 分部修饰符

partial 修饰符指示此 interface-declaration 为分部类型声明。封闭命名空间或类型声明中的多个同名分部接口声明按照第 10.2 节中指定的规则组合成一个接口声明。

1.1.3 Variant 类型形参列表

Variant 类型形参列表只能在接口和委托类型上出现。与普通 type-parameter-list 的差异在于每个类型形参上的可选 variance-annotation。

variant-type-parameter-list:
<  
variant-type-parameters   >

variant-type-parameters:
attributesopt 
variance-annotationopt  type-parameter
variant-type-parameters   ,  
attributesopt  
variance-annotationopt 
type-parameter

variance-annotation:
in
out

如果变化批注为 out,则类型形参称为协变 (covariant)。如果变化批注为 in,则类型形参称为逆变 (contravariant)。如果不存在变化批注,则类型形参称为是固定的 (invariant)。

在下面的示例中

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

Z P
{ get; set; }
}

X 为协变,Y 为逆变,而 Z 为固定的。

1.1.3.1 变化安全性

类型的类型形参列表中出现变化批注会限制类型在类型声明中可以出现的位置。

如果存在以下情况之一,则类型 T 是输出不安全
(output-unsafe) 的:

  • T 为逆变类型形参
  • T 为具有输出不安全元素类型的数组类型
  • T 为从泛型类型 S<A1,… AK> 构造的接口或委托类型 S<X1, .. XK>,其中,对于至少一个 Ai,存在以下情况之一:
  • Xi  是协变或固定的,而 Ai 是输出不安全的。
  • Xi  是逆变或固定的,而 Ai 是输入安全的。

如果存在以下情况之一,则类型 T 是输入不安全
(input-unsafe) 的 :

  • T 是协变类型形参
  • T 是具有输入不安全元素类型的数组类型
  • T 为从泛型类型 S<A1,… AK> 构造的接口或委托类型 S<X1, .. XK>,其中,对于至少一个 Ai,存在以下情况之一:
  • Xi  是协变或固定的,而 Ai 是输入不安全的。
  • Xi  是逆变或固定的,而 Ai 是输出不安全的。

从直观上看,应在输出位置上禁止使用输出不安全类型,在输入位置上禁止使用输入不安全类型。

类型如果不是输出不安全的,则是输出安全 (output-safe) 的,如果不是输入不安全的,则是输入安全 (input-safe) 的。

1.1.3.2 变化转换

变化批注用于提供到接口和委托类型的更加宽松(仍然类型安全)的转换。为此,隐式转换(第 6.1 节)和显式转换(第 6.2 节)的定义利用了变化可转换性的概念,该概念定义如下:

如果类型 T 是使用变化类型形参 T<X1, …, Xn> 声明的接口或委托类型,并且对于每个变体类型形参 Xi,都存在以下情况之一,则类型 T<A1, …, An> 可变化转换为类型 T<B1, …, Bn>:

  • Xi 是协变的,且存在从 Ai 到 Bi 的隐式引用或标识转换
  • Xi 是协变的,且存在从 Bi 到 Ai 的隐式引用或标识转换
  • Xi 是固定的,且存在从 Ai 到 Bi 的标识转换

1.1.4 基接口

接口可以继承自零个或多个接口类型,这些类型称为接口的显式基接口 (explicit base interface)。当接口具有一个或多个显式基接口时,在该接口声明中,接口标识符后就要紧跟一个冒号以及一个由逗号分隔的基接口类型列表。

interface-base:
:  
interface-type-list

对于构造接口类型,显式基接口的组成方式为:采用泛型类型声明上的显式基接口声明,并将基接口声明中的每个 type-parameter 替换为构造类型的对应 type-argument。

接口的显式基接口的可访问性必须至少与接口本身相同(第
3.5.4 节)。例如,在 public
接口的 interface-base 中指定 private 或 internal
接口会导致编译时错误。

接口不能从自身直接或间接继承,否则会发生编译时错误。

接口的基接口是显式基接口及其基接口。换言之,基接口集是显式基接口、后者的显式基接口(依此类推)的完全可传递的闭包。接口继承其基接口的所有成员。在下面的示例中

interface
IControl
{
void Paint();
}

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

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

interface
IComboBox: ITextBox, IListBox {}

IComboBox 的基接口是
IControl、ITextBox 和 IListBox。

换言之,上面的 IComboBox 接口继承成员 SetText
和 SetItems 以及 Paint。

接口的每个基接口都必须是输出安全(第 13.1.3.1 节)的。如果一个类或结构实现某接口,则它还隐式实现该接口的所有基接口。

1.1.5 接口体

接口的 interface-body 定义接口的成员。

interface-body:
{  
interface-member-declarationsopt   }

1.2 接口成员

接口的成员包括从基接口继承的成员和由接口本身声明的成员。

interface-member-declarations:
interface-member-declaration
interface-member-declarations  
interface-member-declaration

interface-member-declaration:
interface-method-declaration
interface-property-declaration
interface-event-declaration
interface-indexer-declaration

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

所有接口成员都隐式地具有
public 访问属性。接口成员声明中包含任何修饰符都属于编译时错误。具体来说,不能使用修饰符 abstract、public、protected、internal、private、virtual、override
或 static 来声明接口成员。

下面的示例

public
delegate void StringListEvent(IStringList sender);

public
interface IStringList
{
void Add(string s);

int Count { get; }

event StringListEvent Changed;

string this[int index] { get; set; }
}

声明了一个接口,该接口的成员涵盖了所有可能作为接口成员的种类:方法、属性、事件和索引器。

interface-declaration 创建新的声明空间(第 3.3 节),并且 interface-declaration 直接包含的 interface-member-declarations 将新成员引入了该声明空间。以下规则适用于 interface-member-declaration:

  • 方法的名称必须与同一接口中声明的所有属性和事件的名称不同。此外,方法的签名(第 3.6 节)不能与在同一接口中声明的其他所有方法的签名相同,并且在同一接口中声明的两种方法的签名不能只有 ref 和 out 不同。
  • 属性或事件的名称必须与同一接口中声明的所有其他成员的名称不同。
  • 一个索引器的签名必须区别于在同一接口中声明的其他所有索引器的签名。

准确地说,接口所继承的成员不是该接口的声明空间的一部分。因此,允许接口用与它所继承的成员相同的名称或签名来声明新的成员。发生这种情况时,则称派生的接口成员隐藏了基接口成员。隐藏一个继承的成员不算是错误,但这确实会导致编译器发出警告。为了避免出现上述警告,派生接口成员的声明中必须包含一个 new 修饰符,以指示该派生成员将要隐藏对应的基成员。第 3.7.1.2 节对本主题进行了进一步讨论。

如果在不隐藏所继承成员的声明中包含 new 修饰符,将对此状况发出警告。通过移除 new 修饰符可取消显示此警告。

请注意,严格来讲,类 object 中的成员不是任何接口的成员(第 13.2 节)。但是,通过在任何接口类型中进行成员查找,可获得类 object 中的成员(第 7.4 节)。

1.2.1 接口方法

接口方法是使用 interface-method-declaration 来声明的:

interface-method-declaration:
attributesopt   newopt  
return-type   identifier   type-parameter-list
          (  
formal-parameter-listopt  
)  
type-parameter-constraints-clausesopt   ;

接口方法声明中的 attributes、return-type、identifier 和 formal-parameter-list 与类中方法声明的对应项(第 10.6 节)具有相同的意义。不允许接口方法声明指定方法体,因此,声明总是以分号结尾。

接口方法的每个形参类型都必须是输入安全(第 13.1.3.1 节)的,返回类型都必须为 void 或输出安全的。而且,方法的任何类型形参上的每个类类型约束、接口类型约束和类型形参约束都必须是输入安全的。

这些规则可确保接口的任何协变或逆变使用都保持类型安全。例如,

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

是非法的,因为将 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>.  的调用。但是该调用要求  E 派生自  D,因此会违反类型安全。

1.2.2 接口属性

接口属性是使用 interface-property-declaration 来声明的:

interface-property-declaration:
attributesopt   newopt  
type   identifier   {  
interface-accessors   }

interface-accessors:
attributesopt   get   ;
attributesopt   set  
;
attributesopt   get   ;  
attributesopt   set   ;
attributesopt   set   ;  
attributesopt   get   ;

接口属性声明中的 attributes、type 和 identifier 与类中属性声明的对应项(第 10.7 节)具有相同的意义。

接口属性声明的访问器与类属性声明(第 10.7.2 节)的访问器相对应,不同之处在于接口属性声明的访问器体必须始终是一个分号。因此,访问器在这里只用于表示该属性为读写、只读还是只写。

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

1.2.3 接口事件

接口事件是使用 interface-event-declarations 来声明的:

interface-event-declaration:
attributesopt   newopt   event   type   identifier  
;

接口事件声明中的 attributes、type 和 identifier 与类中事件声明的对应项(第 10.8 节)具有相同的意义。

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

1.2.4 接口索引器

接口索引器是使用 interface-indexer-declaration 来声明的:

interface-indexer-declaration:
attributesopt   newopt  
type   this   [  
formal-parameter-list   ]   {  
interface-accessors   }

接口索引器声明中的 attributes、type 和 formal-parameter-list 与类中索引器声明的对应项(第 10.9 节)具有相同的意义。

接口索引器声明的访问器与类索引器声明(第 10.9 节)的访问器相对应,不同之处在于接口索引器声明的访问器体必须始终是一个分号。因此,访问器在这里只用于表示该索引器为读写、只读还是只写。

接口索引器的所有形参类型都必须是输入安全的。此外,所有 out 或 ref 形参类型也必须是输出安全的。请注意,由于基础执行平台的限制,甚至 out 形参也必须是输入安全的。

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

1.2.5 接口成员访问

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

对于严格单一继承(继承链中的每个接口均恰巧有零个或一个直接基接口)的接口,成员查找(第 7.4 节)、方法调用(第 7.6.5.1 节)和索引器访问(第 7.6.6.2 节)规则的效果与类和结构的完全相同:派生程度较大的成员隐藏具有相同名称或签名的派生程度较小的成员。然而,对于多重继承接口,当两个或更多个不相关(互不继承)的基接口中声明了具有相同名称或签名的成员时,就会发生多义性。本节列出了此类情况的几个示例。在所有情况下,都可以使用显式强制转换来解决这种多义性。

在下面的示例中

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

interface
ICounter
{
void Count(int i);
}

interface
IListCounter: IList, ICounter {}

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

由于在 IListCounter 中对 Count 的成员查找(第 7.4 节)所获得的结果是不明确的,因此前两个语句将引起编译时错误。如示例所阐释的,将 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
}
}

通过应用第 7.5.3 节的重载决策规则,在调用 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 的访问路径中隐藏。

简单地说,多重继承接口中的直观隐藏规则是:如果成员在任一访问路径中被隐藏,那么它在所有访问路径中都被隐藏。因为从 IDerived 到 ILeft 到 IBase 的访问路径隐藏了 IBase.F,所以从 IDerived 到 IRight 到 IBase 的访问路径中也隐藏了该成员。

1.3 完全限定接口成员名

接口成员有时也用它的完全限定名
(fully qualified name) 来引用。接口成员的完全限定名是这样组成的:声明该成员的接口的名称,后接一个点,再后接该成员的名称。成员的完全限定名将引用声明该成员的接口。例如,给定下列声明

interface
IControl
{
void Paint();
}

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

Paint 的完全限定名是 IControl.Paint,而 SetText 的完全限定名是 ITextBox.SetText。

在上面的示例中,不能用 ITextBox.Paint 来引用 Paint。

当接口是命名空间的组成部分时,该接口的成员的完全限定名需包含命名空间名称。例如

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

此处,Clone 方法的完全限定名是 System.ICloneable.Clone。

1.4 接口实现

接口可以由类和结构来实现。为了指示类或结构直接实现了某接口,在该类或结构的基类列表中应该包含该接口的标识符。例如:

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
同时实现了 IControl 和 ITextBox。

如果类 C 直接实现某个接口,则由 C 派生的所有类均隐式实现该接口。类声明中指定的基接口可以是构造接口类型(第 4.4 节)。基接口本身不能是类型形参,但在其作用域中可以包含类型形参。下面的代码演示类实现和扩展构造类型的方法:

class
C<U,V> {}

interface
I1<V> {}

class D:
C<string,int>, I1<string> {}

class
E<T>: C<int,T>, I1<T> {}

泛型类声明的基接口必须满足第 13.4.2 节中所述的唯一性规则。

1.4.1 显式接口成员实现

为了实现接口,类或结构可以声明显式接口成员实现 (explicit interface member implementation)。显式接口成员实现就是一种方法、属性、事件或索引器声明,它使用完全限定接口成员名称作为标识符。例如

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

interface
IDictionary<K,V>
{
V this[K key];

void Add(K key, V value);
}

class List<T>: IList<T>, IDictionary<int,T>
{
T[] IList<T>.GetElements() {...}

T IDictionary<int,T>.this[int index]
{...}

void IDictionary<int,T>.Add(int index,
T value) {...}
}

这里,IDictionary<int,T>.this 和 IDictionary<int,T>.Add 是显式接口成员实现。

某些情况下,接口成员的名称对于实现该接口的类可能是不适当的,此时,可以使用显式接口成员实现来实现该接口成员。例如,一个实现“文件抽象”的类一般会实现一个具有释放文件资源作用的 Close 成员函数,同时还可能使用显式接口成员实现来实现 IDisposable 接口的 Dispose 方法:

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

在方法调用、属性访问或索引器访问中,不能直接访问“显式接口成员实现”的成员,即使用它的完全限定名也不行。“显式接口成员实现”的成员只能通过接口实例来访问,并且在通过接口实例访问时,只能用该接口成员的名称来引用。

显式接口成员实现中包含访问修饰符属于编译时错误,而且如果包含 abstract、virtual、override
或 static 修饰符也属于编译时错误。

显式接口成员实现具有与其他成员不同的可访问性特征。由于显式接口成员实现永远不能在方法调用或属性访问中通过它们的完全限定名来访问,因此,它们似乎是 private(私有的)。但是,因为它们可以通过接口实例来访问,所以它们似乎又是 public(公共的)。

显式接口成员实现有两个主要用途:

  • 由于显式接口成员实现不能通过类或结构实例来访问,因此它们就不属于类或结构的自身的公共接口。当需在一个公用的类或结构中实现一些仅供内部使用(不允许外界访问)的接口时,这就特别有用。
  • 显式接口成员实现可以消除因同时含有多个相同签名的接口成员所引起的多义性。如果没有显式接口成员实现,一个类或结构就不可能为具有相同签名和返回类型的接口成员分别提供相应的实现,也不可能为具有相同签名和不同返回类型的所有接口成员中的任何一个提供实现。

为了使显式接口成员实现有效,声明它的类或结构必须在它的基类列表中指定一个接口,而该接口必须包含一个成员,该成员的完全限定名、类型和参数类型与该显式接口成员实现所具有的完全相同。因此,在下列类中

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
}

Ellipse 中的 ICloneable.Clone 声明也将导致编译时错误,因为 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。

1.4.2 所实现接口的唯一性

泛型类型声明所实现的接口必须对所有可能的构造类型都保持唯一。如果没有此规则,则无法确定要为某些构造类型调用的正确方法。例如,假设允许以如下形式声明某个泛型类:

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 中的所有重复接口。
  • 在将类型实参替换到 L 中之后,如果从 C 创建的任何可能的构造类型导致 L 中的两个接口完全相同,则 C 的声明无效。在确定所有可能的构造类型时不考虑约束声明。

在上面的类声明 X 中,接口列表 L 由 I<U> 和 I<V> 组成。该声明无效,因为任何 U 和 V 属于相同类型的构造类型都将导致这两个接口成为完全相同的类型。

可以将不同继承级别指定的接口进行统一:

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>(第 13.4.6 节)。

1.4.3 泛型方法实现

当泛型方法隐式地实现接口方法时,为每个方法类型形参提供的约束必须在两个声明中是等效的(在将任何接口类型形参替换为相应的类型实参之后),其中方法的类型形参按序号位置从左到右进行标识。

但是,如果泛型方法显式实现接口方法,则不允许对实现方法使用任何约束。而是从接口方法继承约束

interface I<A,B,C>
{
void F<T>(T t) where T: A;
void G<T>(T t) where T: B;
void H<T>(T t) where T: C;
}

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: string 约束无法在源代码中表示,但从 t 到 s 的赋值是有效的,因为 T 继承该约束。

1.4.4 接口映射

类或结构必须为它的基类列表中所列出的接口的所有成员提供它自己的实现。在进行实现的类或结构中定位接口成员的实现的过程称为接口映射 (interface mapping)。

关于类或结构 C 的接口映射就是查找 C 的基类列表中指定的每个接口的每个成员的实现。对某个特定接口成员 I.M 的实现(其中 I 是声明了成员 M 的接口)的定位按下述规则执行:从 C 开始,按继承顺序,逐个检查 C 的每个后续基类(下面用 S 表示每个进行检查的类或结构),直到找到匹配项:

  • 如果 S 包含一个与 I 和 M 匹配的显式接口成员实现的声明,那么此成员就是 I.M 的实现。
  • 否则,如果
    S 包含与 M 匹配的非静态的 public 成员声明,则此成员就是 I.M 的实现。如果找到多个匹配成员,则无法确定哪个成员是 I.M 的实现。只有 S 是构造类型(在此情况下,泛型类型中声明的两个成员具有不同的签名,但类型参数却使他们的签名相同)时,才会出现此情况。

如果不能为在 C 的基类列表中指定的所有接口的所有成员找到实现,则将发生编译时错误。请注意,接口的成员包括那些从基接口继承的成员。

根据接口映射的含义,类成员 A 在下列情况下与接口成员 B 匹配:

  • A 和 B 都是方法,并且 A 和 B 的名称、类型和形参表都相同。
  • A 和 B 都是属性,A 和 B 的名称和类型相同,并且 A 与 B 具有相同的访问器(如果 A 不是显式接口成员实现,则它可以具有其他访问器)。
  • A 和 B 都是事件,并且 A 和 B 的名称和类型相同。
  • A 和 B 都是索引器,A 和 B 的类型和形参表相同,并且 A 与 B 具有相同的访问器(如果 A 不是显式接口成员实现,则它可以具有其他访问器)。

接口映射算法中隐含着下列值得注意的特征:

  • 在类或结构成员中确定哪个实现了接口成员时,显式接口成员实现比同一个类或结构中的其他成员具有更高的优先级。
  • 接口映射不涉及非公共成员和静态成员。

在下面的示例中

interface
ICloneable
{
object Clone();
}

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

public object Clone() {...}
}

C 的 ICloneable.Clone 成员将成为 ICloneable 中 Clone 的实现,这是因为显式接口成员实现优先于其他成员。

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

interface
IControl
{
void Paint();
}

interface
IForm
{
void Paint();
}

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

在此,IControl 和 IForm 的 Paint 方法均将映射到 Page 中的 Paint 方法。当然也可以为这两个方法提供单独的显式接口成员实现。

如果类或结构实现一个包含被隐藏成员的接口,那么一些成员必须通过显式接口成员实现来实现。例如

interface
IBase
{
int P { get; }
}

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

此接口的实现将至少需要一个显式接口成员实现,可采取下列形式之一

class C:
IDerived
{
int IBase.P { get {...} }

int IDerived.P() {...}
}

class C:
IDerived
{
public int P { get {...} }

int IDerived.P() {...}
}

class C:
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、由 ITextBox
继承的 IControl 和由 IListBox 继承的
IControl 不可能有各自不同的实现。事实上,没有为这些接口提供单独实现的打算。相反,ITextBox
和 IListBox 的实现共享相同的 IControl 实现,因而可以简单地认为 ComboBox 实现了三个接口:IControl、ITextBox 和 IListBox。

基类的成员参与接口映射。在下面的示例中

interface
Interface1
{
void F();
}

class Class1
{
public void F() {}

public void G() {}
}

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

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

1.4.5 接口实现继承

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

如果不显式地重新实现
(re-implementing) 接口,派生类就无法以任何方式更改它从其基类继承的接口映射。例如,在下面的声明中

interface
IControl
{
void Paint();
}

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

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

TextBox 中的 Paint 方法将隐藏 Control 中的 Paint 方法,但这种隐藏并不变更 Control.Paint 到 IControl.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
派生的类可通过重写 PaintControl 方法来专用化 IControl.Paint 的实现。

1.4.6 接口重新实现

一个类若继承了某个接口的实现,则只要将该接口列入它的基类列表中,就可以重新实现 (re-implement) 该接口。

接口的重新实现与接口的初始实现遵循完全相同的接口映射规则。因此,继承的接口映射不会对为重新实现该接口而建立的接口映射产生任何影响。例如,在下面的声明中

interface
IControl
{
void Paint();
}

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

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

Control 将 IControl.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() {}
}

在此,Derived 中 IMethods
的实现将接口方法映射到 Derived.F、Base.IMethods.G、Derived.IMethods.H 和 Base.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。

1.4.7 抽象类和接口

与非抽象类类似,抽象类也必须为在该类的基类列表中列出的接口的所有成员提供它自己的实现。但是,允许抽象类将接口方法映射到抽象方法上。例如

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

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

这里,IMethods 的实现将
F 和 G 映射到抽象方法上,这些抽象方法必须在从 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();
}

此处,要求从 C 派生的非抽象类重写 FF 和 GG,从而提供
IMethods 的实际实现。

C# 语言规范_版本5.0 (第13章 接口)的更多相关文章

  1. C# 语言规范_版本5.0 (第2章 词法结构)

    1. 词法结构 1.1 程序 C# 程序 (program) 由一个或多个源文件 (source file) 组成,源文件的正式名称是编译单元 (compilation unit)(第 9.1 节). ...

  2. C# 语言规范_版本5.0 (第10章 类)

    1. 类 类是一种数据结构,它可以包含数据成员(常量和字段).函数成员(方法.属性.事件.索引器.运算符.实例构造函数.静态构造函数和析构函数)以及嵌套类型.类类型支持继承,继承是一种机制,它使派生类 ...

  3. C# 语言规范_版本5.0 (第4章 类型)

    1. 类型 C# 语言的类型划分为两大类:值类型 (Value type) 和引用类型 (reference type).值类型和引用类型都可以为泛型类型 (generic type),泛型类型采用一 ...

  4. C# 语言规范_版本5.0 (第3章 基本概念)

    1. 基本概念 1.1 应用程序启动 具有入口点 (entry point) 的程序集称为应用程序 (application).应用程序运行时,将创建新的应用程序域 (application doma ...

  5. C# 语言规范_版本5.0 (第18章 不安全代码)

    1. 不安全代码 **(注:此章对于跨多语言编程开发非常重要,如遇异常无法完成跨语言,建议使用此种方式.) 如前面几章所定义,核心 C# 语言没有将指针列入它所支持的数据类型,从而与 C 和 C++ ...

  6. C# 语言规范_版本5.0 (第17章 特性)

    1. 特性 C# 语言的一个重要特征是使程序员能够为程序中定义的实体指定声明性信息.例如,类中方法的可访问性是通过使用 method-modifiers(public.protected.intern ...

  7. C# 语言规范_版本5.0 (第11章 结构)

    1. 结构 结构与类的相似之处在于,它们都表示可以包含数据成员和函数成员的数据结构.但是,与类不同,结构是一种值类型,并且不需要堆分配.结构类型的变量直接包含了该结构的数据,而类类型的变量所包含的只是 ...

  8. C# 语言规范_版本5.0 (第8章 语句)

    1. 语句 C# 提供各种语句.使用过 C 和 C++ 编程的开发人员熟悉其中大多数语句. statement: labeled-statement declaration-statement emb ...

  9. C# 语言规范_版本5.0 (第7章 表达式)

    1. 表达式 表达式是一个运算符和操作数的序列.本章定义语法.操作数和运算符的计算顺序以及表达式的含义. 1.1 表达式的分类 一个表达式可归类为下列类别之一: 值.每个值都有关联的类型. 变量.每个 ...

随机推荐

  1. 通过DialogFragment从DatePicker或TimePicker中获取日期数据

    通过DialogFragment从DatePicker或TimePicker中获取日期数据 一个activity类,里面存有date和time的变量,想通过dialogfragment的方式获取用户输 ...

  2. T-SQL的10个好习惯

    有关T-SQL的10个好习惯 1.在生产环境中不要出现Select * 这一点我想大家已经是比较熟知了,这样的错误相信会犯的人不会太多.但我这里还是要说一下. 不使用Select *的原因主要不是坊间 ...

  3. myeclipse 配置 resin-pro-4.0.34

    热部署: 在 resin.xml 文件下 增加 <host id="" root-directory="."> <!-- webapps ca ...

  4. Mathematics for Computer Graphics

    Mathematics for Computer Graphics 最近严重感觉到数学知识的不足! http://bbs.gameres.com/showthread.asp?threadid=105 ...

  5. 1.C#基础学习笔记3---C#字符串(转义符和内存存储无关)

    技术qq交流群:JavaDream:251572072  教程下载,在线交流:创梦IT社区:www.credream.com ------------------------------------- ...

  6. 使用 IDEA 创建 Maven Web 项目 (一)- 使用IEAD创建Maven项目

    创建IDEA项目 单击 Create New Project 按钮,弹出 New Project 对话框. 选择 Maven 选项,单击 Next 按钮. 输入 GroupId.ArtifactId. ...

  7. 关于javascript变量作用域的研究。

    开始 一个变量的作用域(scope)是程序源代码中定义这个变量的区域.全局变量具有全局作用域,在javascript中的任何地方都是有定义的.然而在函数内申明的变量只在函数体内有定义.他们是局部变量, ...

  8. 结构-行为-样式-Js排序算法之 直接插入排序

    最新因工作原因需要接触到算法,之前学习C++的时候有接触过算法,Javascript中实现算法其实也是大同小异.下面我讲下第一个实现的排序算法--直接插入排序.基本实现思路:假定一个数组中前n(n&g ...

  9. 通过python为zabbix发送告警邮件

    最近部署ZABBIX的邮件告警时,用刚学的python来写告警邮件脚本. 由于时间有限,我只对关键步骤做截图,对zabbix的基本配置略过. python代码如下 1 #!/usr/bin/pytho ...

  10. 【C#】【SHARE】The registering of global hotkeys

    I remember that when I was still using VB6 sereval years ago, if global hotkeys are required, a mass ...