接口定义与实现
本教程共 100 篇 · 第 44 篇 · 更新于 2026-07-31 · 约 12 分钟阅读
44. 接口定义与实现
本节目标:用
interface定义行为契约,让一个类同时实现多个接口。
接口就是一份契约
接口(interface)声明“一个类型能做什么”,但不关心“它是什么”。比如“会飞”这个能力,鸟有,飞机也有,可鸟和飞机毫无继承关系。用接口描述这种跨类型的能力最自然。
接口名通常以大写 I 开头,这是 C# 的约定,看到 I 前缀就知道它是接口。这套约定让读代码的人一眼区分“能力”和“类型”。
Note接口回答的是“能做什么(can-do)”,抽象类回答的是“是什么(is-a)”。一个鸭子是“动物”(is-a),同时“会飞、会游”(can-do)。这就解释了为什么接口和抽象类经常搭配使用。
接口里能放什么成员
接口只能声明“成员签名”,具体包括:方法、属性、事件、索引器。它不能有字段、构造器,也不能给成员写具体实现——不过 C# 8 起接口可以带默认实现(这点留到第 45 章细说)。
namespace CSharpDemo;
interface IShape
{
double Area(); // 方法
string Name { get; } // 属性
}
注意属性在接口里只写“有没有、能不能写”,不写背后用什么字段支撑,字段只能落在实现类。
定义并实现接口
接口里只能声明方法、属性、事件、索引器,不能有字段或构造器。实现类必须给出所有成员的具体代码。
namespace CSharpDemo;
interface IAnimal
{
string Sound();
}
class Dog : IAnimal
{
public string Sound() => "汪汪";
}
IAnimal a = new Dog();
Console.WriteLine(a.Sound());
Dog 承诺实现 IAnimal 的全部成员,否则编译不过。接口像一份强制履约的合同。
一个类实现多个接口
C# 不支持类多继承,但一个类可以实现多个接口。这正是接口最大的用武之地。
namespace CSharpDemo;
interface IFlyable
{
void Fly();
}
interface ISwimmable
{
void Swim();
}
class Duck : IFlyable, ISwimmable
{
public void Fly() => Console.WriteLine("鸭子飞");
public void Swim() => Console.WriteLine("鸭子游");
}
Duck duck = new Duck();
duck.Fly();
duck.Swim();
Duck 同时具备“会飞”和“会游”两种能力。一份具体类型,多份契约,互不冲突。
Note类只能继承自一个基类,却能实现任意多个接口。这是 C# 避开多继承歧义、又保留灵活性的设计取舍。
接口作为方法参数
接口不只是“类型声明”,还能当方法参数。这样方法就能接受任何实现该接口的对象。
namespace CSharpDemo;
void LetItFly(IFlyable f) => f.Fly();
LetItFly(new Duck());
LetItFly 不关心传进来的是鸭、是鸟还是纸飞机,只要会飞就行。这正是“面向契约编程”的威力。
接口也能继承接口
接口之间可以继承,形成一个更大的契约。实现“子接口”的类,要把继承链上所有成员都实现。
namespace CSharpDemo;
interface IDrawable
{
void Draw();
}
interface IShape : IDrawable
{
double Area();
}
class Circle : IShape
{
public void Draw() => Console.WriteLine("画圆");
public double Area() => 3.14;
}
Circle 既要实现 Area,也要实现从 IDrawable 继承来的 Draw。
Tip接口继承适合“大能力包含小能力”的场景,比如
IShape自然是一种IDrawable。继承能让契约按粒度分层,调用方按需引用更细的接口。
接口与抽象类怎么选
两者都定义“派生类型必须做什么”。区别看这里:
- 抽象类表达“is-a”(是什么)。相关类型共享状态、字段、构造逻辑时用它。
- 接口表达“can-do”(能做什么)。能力需要跨不相关的类型复用,或类型已有基类时用它。
Tip记一句口诀:要“身份”用抽象类,要“能力”用接口。需要多份能力就叠加多个接口。
接口的显式实现(提前铺垫)
接口成员也可以“显式实现”,写法像 void IFlyable.Fly()。这种成员不带 public,只能透过接口类型调用。它常用来“消歧义”或“隐藏内部 API”。因为多接口同名冲突时会强制用到它,第 45 章会展开讲。
接口助力可测试性
接口让代码“依赖契约而非具体类”。测试时可以用一个“假实现”替换真实类,而不用动主流程。这是单元测试里依赖注入(dependency injection)的基础思想。
比如有个 ILogger,生产用写文件,测试用“记在内存里”的假实现,主流程一行都不用改。这种解耦让代码既好测又好换。
namespace CSharpDemo;
interface ILogger
{
void Log(string message);
}
class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
}
void Process(ILogger logger) => logger.Log("处理完成");
Process(new ConsoleLogger());
换个 ILogger 的假实现,Process 一行都不用改。这就是“面向接口编程”带来的可替换性。
与其他语言客观对比
Java 的接口和 C# 很像,且 Java 8 起也支持默认方法,思路一致。C++ 没有“接口”关键字,通常用“纯虚类(只含纯虚函数的抽象类)”模拟接口,但纯虚类仍算基类、受单继承限制。Go 语言的接口更“隐式”:类型只要实现了接口的方法集,就自动算实现了该接口,不需要 : Ixxx 显式声明,称为“鸭子类型(duck typing)”。C# 要求显式声明实现,编译期就能发现没履约。
新手常见坑
第一,接口成员默认是 public,实现时也必须 public,不能改小可见性。
第二,误以为接口能存数据。接口不能有实例字段,想带状态请用抽象类或把数据放实现类。
第三,为了“看起来规范”而滥建接口。只在真的需要多态或跨类型契约时才定义接口,否则只是增加负担。
第四,把接口当成“多继承”的替代品。接口没有状态与构造器,它只描述行为,不是类的完整替代。
第五,忘写某个接口成员的实现,编译直接失败。这其实是好事——编译器在逼你把契约履约。
最佳实践小结
- 接口命名以
I开头,成员表达“能力”而非“状态”。 - 需要跨不相关类型共享能力,或类型已有基类时,优先用接口。
- 方法参数、返回值尽量“面向接口”,提升可替换性。
- 接口数量克制:只为真正需要多态/解耦的边界定义,别过度抽象。
- 需要状态与共享逻辑时,回到抽象类或普通基类。
动手练一练
- 定义
IMovable接口带Move(),让Car、Robot各自实现,用List<IMovable>循环调用。 - 让一个
Duck同时实现IFlyable与ISwimmable,体会多接口叠加。 - 写一个方法
Test(ILogger logger),用ConsoleLogger调用,思考如果换成文件版 logger 主流程是否需要改动。
要点速记
接口把“行为”从“类型”里解耦出来,用 I 前缀约定、靠 : IName 实现、可多份叠加。它是“面向契约编程”的基石,也是解耦与可测试性的关键。C# 8 之后接口还能带默认实现,下一章细说。