1. 引言
通常你实现接口的时候,只需要在自己的类里写出和接口声明一样签名的public方法就行了。这叫做隐式或者公开(public)实现。编译器很聪明,会明白:“哦,这个DoSomething()方法在MyClass类里,是用来实现IDoable.DoSomething()的!”
但如果遇到这种情况呢:
- 你的SmartDevice类实现了ICamera接口(有个TakePicture()方法)而且还实现了IScreen接口(也有TakePicture()方法——比如用来截图)?
- 或者你的Robot类已经有个public的Reset()方法用来完全重置所有系统,但你还想让它实现IDevice接口里的Reset()方法,这个方法只需要重置部分设置?
这种情况下就会有歧义,或者你想把功能分得更清楚。这时候就轮到显式接口实现登场了。
显式接口实现让你可以告诉编译器:“这个方法就是专门给这个接口用的,而且只有通过接口引用才能访问。”就像蜘蛛侠彼得帕克,只有当他扮演蜘蛛侠时才会发射蛛丝,平时当“普通”彼得帕克时就是摄影师技能。
2. 什么时候需要显式接口实现
你用普通方式实现接口时,接口的方法和属性会变成你类的public接口的一部分。但有时候你希望它们只能通过接口访问,而不是直接通过类。这种情况其实挺常见的!比如两个接口都要求有同名方法(但意义不同),或者你想让某些实现只对通过接口操作的用户开放。
这时候显式接口实现就派上用场了。这就像程序架构里的“隐藏通道”:外面看不到,懂门道的人才能进!
场景1:名字冲突
想象一下,你有个类实现了两个接口,而这两个接口都要求有同名但逻辑完全不同的方法。比如:
interface IWriter
{
void Print();
}
interface IPrinter
{
void Print();
}
你想让IWriter.Print()把文本写到文件里,而IPrinter.Print()把它发到打印机。直接写一个Print()方法没法区分行为,这时候显式实现就能救场。
场景2:隐藏不常用/技术性接口方法
有时候你的类必须实现接口里的某个方法,但你根本不想让所有用你类的人都能看到它(比如这个接口成员只是给框架内部用的)。
场景3:防止“误调用”
如果接口方法不是给直接调用用的(比如框架内部机制),你可以用显式实现——这样别人就不能直接通过类对象调用它了。
3. 显式接口实现的语法
最大区别就是——显式实现时你要用接口的全名来命名方法和属性。不能加访问修饰符(public/private)或者override关键字!
通用语法:
返回类型 接口名.方法名(参数)
{
// 实现
}
看起来就像你特意写上接口名,让编译器和同事都不会搞混这个实现是给哪个契约用的。
解决名字冲突
来看看刚才那个IWriter和IPrinter的例子。假设我们在做一个学习用的报表类:
interface IWriter
{
void Print();
}
interface IPrinter
{
void Print();
}
public class Report : IWriter, IPrinter
{
// 显式实现 IWriter.Print
void IWriter.Print()
{
Console.WriteLine("把报表保存到文件(Writer)...");
}
// 显式实现 IPrinter.Print
void IPrinter.Print()
{
Console.WriteLine("把报表发到纸质打印机(Printer)...");
}
// 用户自定义输出方法
public void Show()
{
Console.WriteLine("在屏幕上显示报表。");
}
}
现在我们来试试不同的接口调用方式:
var report = new Report();
report.Show(); // 普通public方法
// report.Print(); // 报错!Report类里没有Print方法
IWriter writer = report;
writer.Print(); // 调用IWriter.Print()的实现
IPrinter printer = report;
printer.Print(); // 调用IPrinter.Print()的实现
这里Print方法不能直接通过report对象访问,只能通过对应接口。这就是显式实现的核心:只有通过接口“端口”才能访问到对应方法。
内存里的样子:简单示意
本质上,显式实现会把接口成员“藏”在类里。可以用下面的表格来形象理解:
| 怎么调用 | 实际会执行啥 |
|---|---|
|
编译错误——没有这个方法 |
|
显式实现的 |
|
显式实现的 |
|
类的Show方法 |
4. 示例:接口只给基础设施用
实际项目里经常遇到这种情况:类必须实现某个服务接口,但普通用户根本不需要用到这个实现。
interface IBroadcastable
{
void Broadcast();
}
public class SecretMessage : IBroadcastable
{
void IBroadcastable.Broadcast()
{
Console.WriteLine("秘密消息已广播出去了...");
}
public void Reveal()
{
Console.WriteLine("在屏幕上显示秘密。");
}
}
// 普通代码里:
var message = new SecretMessage();
message.Reveal(); // 用户方法
// message.Broadcast(); // 报错!没有这个方法
// 只有基础设施知道怎么用:
((IBroadcastable)message).Broadcast();
这里Broadcast()方法只给通过接口操作的人用。
5. 显式实现属性和索引器
不仅方法可以显式实现,属性和索引器也可以。
interface IDescribable
{
string Description { get; }
}
public class Product : IDescribable
{
// 显式实现属性
string IDescribable.Description => "只有通过接口才能访问的描述";
// 普通public属性
public string Name { get; set; }
}
// 使用示例:
var p = new Product { Name = "小玩意" };
// p.Description; // 报错!Product里没有这个属性
var descr = ((IDescribable)p).Description;
Console.WriteLine(descr);
6. 有用的细节
显式实现和继承怎么配合
继承时很直观:如果基类显式实现了接口,子类会继承这个“行为”。但如果子类想重写接口方法的实现,是不行的:显式实现不能是虚方法。也就是说,不能重写显式实现的成员。
如果你需要这种行为——只能用普通实现或者考虑“模板方法”模式。
显式和隐式实现
+----------------+
| Invoice |
+----------------+
| Show() | // 可以直接调用
+----------------+
| ITxtExportable.Export() // 只能通过ITxtExportable
| IJsonExportable.Export() // 只能通过IJsonExportable
+----------------+
显式实现的特点/限制
- 显式实现的方法和属性不能有访问修饰符。它们默认对外部是私有的,只能通过接口访问。
- 这些成员不能是static、virtual、abstract或override。
- 不能直接通过类对象访问显式实现的成员(只能通过接口变量)。
- 如果接口继承了别的接口,可以显式实现任意层级的成员。
7. 显式实现的优点
显式实现不仅仅是用来解决冲突的小技巧。它有几个很重要的理由:
解决名字冲突(The Big One): 这是最主要、最直观的理由。如果你实现的两个接口声明了同名同签名的方法(或属性),显式实现让你能为每个接口写不同的实现。否则就会有歧义。
现实例子: 比如你有个打印机,它也能当扫描仪。IPrinter接口有Print()方法,IScanner接口有Scan()方法。但如果两个接口都有ProcessDocument()方法呢?显式实现让你可以写IPrinter.ProcessDocument()用于打印,IScanner.ProcessDocument()用于扫描,而且它们互不影响。
隐藏实现细节,保持API干净: 显式实现的方法不会成为你类的public API。只有把对象转成接口类型才能访问。这很有用,比如你只想让某些功能“按合同”开放,而不是所有人都能用。
例子: 你做了个复杂的金融工具,比如CreditCard。它实现了IPayable(用来支付)和IAdminConfigurable(比如设置额度,只给内部用)。IAdminConfigurable.SetLimit()不应该让所有拿到CreditCard的人都能用,只能让管理系统通过IAdminConfigurable接口来用。显式实现可以把SetLimit()藏起来,让CreditCard的API更干净安全。
保证契约: 有时候你类里碰巧有个和接口同名同签名的方法,但你不想让它自动变成接口实现。比如你有个MyList类,有个Clear()方法清空内部状态。如果你让MyList实现IList<T>接口(也有Clear()),默认你的MyList.Clear()就成了IList<T>.Clear()的实现。如果它们逻辑不一样,显式实现IList<T>.Clear()就能分开处理。
隐式(Implicit) vs. 显式(Explicit)实现
为了更直观地理解区别,来看个对比表:
| 特点 | 隐式(Implicit)实现 | 显式(Explicit)实现 |
|---|---|---|
| 可访问性 | 可以通过类和接口访问。 | 只能通过接口访问。 |
| 访问修饰符 | 通常是public(或protected等)。 | 没有访问修饰符(不能是public)。 |
| 语法 | |
|
| 解决冲突 | 不能解决,方法会给所有同签名接口用。 | 能解决冲突,给每个接口写不同实现。 |
| API干净度 | 方法是类public API的一部分。 | 方法不是类public API的一部分。 |
| 用途 | 适合通用、无歧义的接口实现。 | 适合解决冲突或隐藏特殊逻辑。 |
可以看到,这俩其实是一个硬币的两面,各有用武之地。大多数时候你会用隐式实现,因为简单方便。显式实现是专门为特殊但很重要的场景准备的。
8. 显式接口实现的常见错误
错误1:方法不能通过类对象访问。
显式实现的方法不能直接通过类实例访问。经常有人搞混:编译器说找不到方法,其实它只是“藏起来”了。解决办法:把对象转成接口类型,比如(IMyInterface)obj.Method()。
错误2:错误使用访问修饰符。
显式实现时不能写public或override等修饰符。写了会编译报错——编译器不认这种声明。
错误3:试图在子类重写显式实现的方法。
如果接口方法在基类里显式实现了,子类不能重写它。这是设计接口继承体系时要注意的限制。
GO TO FULL VERSION