首页 / C# 入门教程 / 接口定义与实现

C# 入门教程

接口定义与实现

本教程共 100 篇 · 第 44 篇 · 更新于 2026-07-31 · 约 12 分钟阅读

C#C# 入门教程面向对象接口

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 开头,成员表达“能力”而非“状态”。
  • 需要跨不相关类型共享能力,或类型已有基类时,优先用接口。
  • 方法参数、返回值尽量“面向接口”,提升可替换性。
  • 接口数量克制:只为真正需要多态/解耦的边界定义,别过度抽象。
  • 需要状态与共享逻辑时,回到抽象类或普通基类。

动手练一练

  1. 定义 IMovable 接口带 Move(),让 CarRobot 各自实现,用 List<IMovable> 循环调用。
  2. 让一个 Duck 同时实现 IFlyableISwimmable,体会多接口叠加。
  3. 写一个方法 Test(ILogger logger),用 ConsoleLogger 调用,思考如果换成文件版 logger 主流程是否需要改动。

要点速记

接口把“行为”从“类型”里解耦出来,用 I 前缀约定、靠 : IName 实现、可多份叠加。它是“面向契约编程”的基石,也是解耦与可测试性的关键。C# 8 之后接口还能带默认实现,下一章细说。