C#接口用法详解(附带实例)
接口和类相似,但接口只提供行为定义而不会持有任何状态(数据),因此:
接口声明和类声明很相似。但接口不提供成员的实现,这是因为它的所有成员都是隐式抽象的。这些成员将由实现接口的类或结构体实现。接口只能包含函数,即方法、属性、事件、索引器(而这些正是类中可以定义为抽象的成员类型)。
以下是 System.Collections 命名空间下的 IEnumerator 接口的定义:
接口成员总是隐式 public 的,并且不能用访问权限修饰符声明。实现接口意味着它将为所有的成员提供 public 实现:
可以把对象隐式转换为它实现的任意一个接口:
尽管 CountDown 是 internal 权限的类,通过把 CountDown 实例转换为 IEnu-merator,其内部实现 IEnumerator 接口的成员就可以作为 public 成员访问。例如,如果同程序集中的一个公有类型定义了如下的方法:
另一个程序集的调用者可以执行:
接口还可以从其他接口派生,例如:
请看下面的例子:
I1 和 I2 都有相同签名的 Foo 成员。Widget 显式实现了 I2 的 Foo 方法,使得同一个类中同时存在两个同名的方法。调用显式实现成员的唯一方式是先将其转换为对应的接口:
另一个使用显式实现接口成员的原因是隐藏那些高度定制化的或对类的正常使用干扰很大的接口成员。例如,实现了 ISerializable 接口的类通常会选择隐藏 ISerializable 成员,除非显式转换成这个接口。
在下面的例子中,TextBox 显式实现 IUndoable.Undo,所以不能标识为 virtual。为了重写,RichTextBox 必须重新实现 IUndoable 的 Undo 方法:
从接口调用重新实现的成员时,调用的是子类的实现:
假定 RichTextBox 定义不变,如果 TextBox 隐式实现 Undo:
那么我们就有了另外一种调用 Undo 的方法,如下面的 “Case 3” 所示,它将“切断”整个系统:
重新实现是在子类不期望被重写时的最后选择。而更好的选择是在定义基类时,无须令子类使用重新实现的方式就能够完成重写,以下两种方法可以做到这一点:
例如:
默认实现永远是显式的。因而假设一个类实现了 ILogger 接口但并未定义 Log 方法,那么要调用 Log 方法必须通过接口来进行调用:
除此之外,接口中还能定义静态成员(包括静态字段)。接口的默认实现可以访问以下静态成员:
由于接口成员是隐式 public 成员,因此在外部访问其静态成员也是可行的:
接口中(仍然)禁止定义实例字段,这和接口的目的是一致的,它定义的应该是行为而非状态。
观察下面的类:
遵照一般原则,我们看出所有的昆虫和飞鸟类共享实现,所以 Insect 和 Bird 仍然使用类的形式。而“能飞的生物”的“飞”是独立的机制,“食肉动物”的“食肉”是独立的机制,所以我们将 FlyingCreature 和 Carnivore 转换为接口:
- 接口只能定义函数而不能定义字段。
- 接口的成员都是隐式抽象的。(虽然 C# 8 支持在接口中声明非抽象函数,但这应当视为一种特殊情况。)
- 一个类(或者结构体)可以实现多个接口,而一个类只能够继承一个类,结构体则完全不支持继承(只能从 System.ValueType 派生)。
接口声明和类声明很相似。但接口不提供成员的实现,这是因为它的所有成员都是隐式抽象的。这些成员将由实现接口的类或结构体实现。接口只能包含函数,即方法、属性、事件、索引器(而这些正是类中可以定义为抽象的成员类型)。
以下是 System.Collections 命名空间下的 IEnumerator 接口的定义:
public interface IEnumerator { bool MoveNext(); object Current { get; } void Reset(); }
接口成员总是隐式 public 的,并且不能用访问权限修饰符声明。实现接口意味着它将为所有的成员提供 public 实现:
internal class Countdown : IEnumerator { int count = 11; public bool MoveNext() => count-- > 0; public object Current => count; public void Reset() { throw new NotSupportedException(); } }
可以把对象隐式转换为它实现的任意一个接口:
IEnumerator e = new Countdown(); while (e.MoveNext()) Console.Write(e.Current); // 109876543210
尽管 CountDown 是 internal 权限的类,通过把 CountDown 实例转换为 IEnu-merator,其内部实现 IEnumerator 接口的成员就可以作为 public 成员访问。例如,如果同程序集中的一个公有类型定义了如下的方法:
public static class Util { public static object GetCountDown() => new Countdown(); }
另一个程序集的调用者可以执行:
IEnumerator e = (IEnumerator)Util.GetCountDown(); e.MoveNext();如果 IEnumerator 定义为 internal,那么以上方法就不能使用了。
接口还可以从其他接口派生,例如:
public interface IUndoable { void Undo(); } public interface IRedoable : IUndoable { void Redo(); }IRedoable “继承”了 IUndoable 接口的所有成员。换言之,实现 IRedoable 的类型也必须实现 IUndoable 的成员。
C#显式接口实现
当实现多个接口时,有时会出现成员签名的冲突。显式实现(explicitly implementing)接口成员可以解决冲突。请看下面的例子:
interface I1 { void Foo(); } interface I2 { int Foo(); } public class Widget : I1, I2 { public void Foo() => Console.WriteLine("Widget's implementation of I1.Foo"); int I2.Foo() { Console.WriteLine("Widget's implementation of I2.Foo"); return 42; } }
I1 和 I2 都有相同签名的 Foo 成员。Widget 显式实现了 I2 的 Foo 方法,使得同一个类中同时存在两个同名的方法。调用显式实现成员的唯一方式是先将其转换为对应的接口:
Widget w = new Widget(); w.Foo(); // Widget's implementation of I1.Foo ((I1)w).Foo(); // Widget's implementation of I1.Foo ((I2)w).Foo(); // Widget's implementation of I2.Foo
另一个使用显式实现接口成员的原因是隐藏那些高度定制化的或对类的正常使用干扰很大的接口成员。例如,实现了 ISerializable 接口的类通常会选择隐藏 ISerializable 成员,除非显式转换成这个接口。
C#用虚成员实现接口
默认情况下,隐式实现的接口成员是密封的。如需重写,必须在基类中将其标识为 virtual 或者 abstract:public interface IUndoable { void Undo(); } public class TextBox : IUndoable { public virtual void Undo() => Console.WriteLine("TextBox.Undo"); } public class RichTextBox : TextBox { public override void Undo() => Console.WriteLine("RichTextBox.Undo"); }不管是从基类还是从接口中调用接口成员,调用的都是子类的实现:
RichTextBox r = new RichTextBox(); r.Undo(); // RichTextBox.Undo ((IUndoable)r).Undo(); // RichTextBox.Undo ((TextBox)r).Undo(); // RichTextBox.Undo显式实现的接口成员不能标识为 virtual,也不能实现通常意义的重写,但是它可以被重新实现(reimplemented)。
C#在子类中重新实现接口
子类可以重新实现基类实现的任意一个接口成员。不管基类中该成员是否为 virtual,当通过接口调用时,重新实现都能够劫持成员的实现。它对接口成员的隐式和显式实现都有效,但后者效果更好。在下面的例子中,TextBox 显式实现 IUndoable.Undo,所以不能标识为 virtual。为了重写,RichTextBox 必须重新实现 IUndoable 的 Undo 方法:
public interface IUndoable { void Undo(); } public class TextBox : IUndoable { void IUndoable.Undo() => Console.WriteLine("TextBox.Undo"); } public class RichTextBox : TextBox, IUndoable { public void Undo() => Console.WriteLine("RichTextBox.Undo"); }
从接口调用重新实现的成员时,调用的是子类的实现:
RichTextBox r = new RichTextBox(); r.Undo(); // RichTextBox.Undo Case 1 ((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
假定 RichTextBox 定义不变,如果 TextBox 隐式实现 Undo:
public class TextBox : IUndoable { public void Undo() => Console.WriteLine("TextBox.Undo"); }
那么我们就有了另外一种调用 Undo 的方法,如下面的 “Case 3” 所示,它将“切断”整个系统:
RichTextBox r = new RichTextBox(); r.Undo(); // RichTextBox.Undo Case 1 ((IUndoable)r).Undo(); // RichTextBox.Undo Case 2 ((TextBox)r).Undo(); // TextBox.Undo Case 3从“Case 3”可以看到,通过重新实现来劫持调用的方式仅在通过接口调用成员时有效,而从基类调用时无效。这个特性通常不尽如人意,因为它们的语义是不一致的。因此,重新实现主要适合于重写显式实现的接口成员。
接口重新实现的替代方案
即使是显式实现的成员,接口重新实现还是容易出问题,这是因为:- 子类无法调用基类的方法;
- 基类的作者在定义基类时也许并非期望重新实现其中的方法,或无法接受重新实现后带来的潜在问题。
重新实现是在子类不期望被重写时的最后选择。而更好的选择是在定义基类时,无须令子类使用重新实现的方式就能够完成重写,以下两种方法可以做到这一点:
- 当隐式实现成员时,尽可能将其标记为 virtual。
- 当显式实现成员时,如果能够预测子类可能要重写某些逻辑,则使用下面的模式:
public class TextBox : IUndoable { void IUndoable.Undo() => Undo(); // 调用下面的方法 protected virtual void Undo() => Console.WriteLine("TextBox.Undo"); } public class RichTextBox : TextBox { protected override void Undo() => Console.WriteLine("RichTextBox.Undo"); }如果你不希望添加任何的子类,则可以把类标记为 sealed 以制止接口的重新实现。
C# 接口和装箱
将结构体转换为接口会引发装箱,而调用结构体的隐式实现接口成员不会引发装箱。例如:
interface I { void Foo(); } struct S : I { public void Foo() { } } S s = new S(); s.Foo(); // No boxing I i = s; // Box occurs when casting to interface i.Foo();
C#默认接口成员
从 C# 8 开始,我们可以在接口成员中添加默认实现,而该成员就不必进行实现了:interface ILogger { void Log(string text) => Console.WriteLine(text); }若要在一个广为人知的程序库中为接口添加一个成员,又想避免破坏现有的成千上万的实现,这个特性就显得尤为重要了。
默认实现永远是显式的。因而假设一个类实现了 ILogger 接口但并未定义 Log 方法,那么要调用 Log 方法必须通过接口来进行调用:
class Logger : ILogger { } ...... ((ILogger)new Logger()).Log("message");这避免了接口实现的多继承问题:如果两个接口中添加了相同的默认成员,那么在决定应该调用哪一个成员的时候是不会存在二义性问题的。
除此之外,接口中还能定义静态成员(包括静态字段)。接口的默认实现可以访问以下静态成员:
interface ILogger { void Log(string text) => Console.WriteLine(Prefix + text); static string Prefix = ""; }
由于接口成员是隐式 public 成员,因此在外部访问其静态成员也是可行的:
ILogger.Prefix = "File log: ";如需限制这一行为,可在接口的静态成员上添加访问修饰符(例如 private、protected 和 internal)。
接口中(仍然)禁止定义实例字段,这和接口的目的是一致的,它定义的应该是行为而非状态。
C#接口和类的对比
类与接口使用的指导原则如下:- 若类型间能够自然地共享实现,则使用类和子类;
- 若各个实现是独立的,则定义接口。
观察下面的类:
abstract class Animal { } abstract class Bird : Animal { } abstract class Insect : Animal { } abstract class FlyingCreature : Animal { } abstract class Carnivore : Animal { } // Concrete classes: class Ostrich : Bird { } class Eagle : Bird, FlyingCreature, Carnivore { } // Illegal class Bee : Insect, FlyingCreature { } // Illegal class Flea : Insect, Carnivore { } // IllegalEagle、Bee 和 Flea 类是无法编译的,因为继承多个类是非法的。为了解决这个问题,我们需要将其中的某些类型转换为接口。问题是转换哪个类型呢?
遵照一般原则,我们看出所有的昆虫和飞鸟类共享实现,所以 Insect 和 Bird 仍然使用类的形式。而“能飞的生物”的“飞”是独立的机制,“食肉动物”的“食肉”是独立的机制,所以我们将 FlyingCreature 和 Carnivore 转换为接口:
interface IFlyingCreature { } interface ICarnivore { }在特定的语义中,Bird 和 Insect 可以对应 Windows 控件和 Web 控件,而 Flying-Creature 和 Carnivore 对应 IPrintable 和 IUndoable。