Menu
CoddyTech

接口与抽象类

本课属于 Coddy C# 学习旅程的“面向对象编程”单元。第 26 课,共 70 课。

接口和抽象类都可以定义其他类必须遵守的契约,但它们的用途不同,功能也各有特点。

接口定义了一个类必须执行的操作,但不包含任何实现。它是一份纯粹的契约:

public interface IFlyable
{
    void Fly();
}

public class Bird : IFlyable
{
    public void Fly()
    {
        Console.WriteLine("Flapping wings");
    }
}

抽象类既可以提供契约,也可以提供共享实现。它是一份不完整的蓝图:

public abstract class Vehicle
{
    public string Brand { get; set; }
    
    public void StartEngine()
    {
        Console.WriteLine("Engine started");
    }
    
    public abstract void Drive();  // 必须实现
}

public class Car : Vehicle
{
    public override void Drive()
    {
        Console.WriteLine("Driving on roads");
    }
}

主要区别在于继承规则和用途。

一个类可以实现多个接口,但只能继承一个抽象类。

一个类也可以同时继承抽象类并实现接口:冒号后先写基类,再写接口,彼此用逗号分隔:

public class Airplane : Vehicle, IFlyable
{
    public override void Drive()
    {
        Console.WriteLine("Taxiing to the runway");
    }

    public void Fly()
    {
        Console.WriteLine("Flying above the clouds");
    }
}

当不相关的类需要共享行为时,使用接口(例如,鸟类和飞机都可以使用 IFlyable)。当相关的类共享通用代码和状态时,使用抽象类(例如,车辆共享启动引擎的机制)。

抽象类可以有构造函数、字段和成员访问修饰符。传统上,接口不可以这样做,不过现代 C# 允许提供默认实现。

如果不确定,优先选择接口以获得灵活性。以后如果需要共享实现,你随时可以再添加一个抽象类。

challenge icon

挑战

简单

让我们构建一个媒体播放器系统,展示何时使用接口,何时使用抽象类。你将设计这样一种结构:不相关的设备通过接口共享播放行为,而相关的媒体类型则通过抽象类共享通用实现。

你将把代码分到四个文件中:

  • IPlayable.cs:在 Media 命名空间中定义一个名为 IPlayable 的接口。此接口用于表示任何可以播放媒体的对象所遵循的约定,无论它是手机、电视还是专用音乐播放器。它应声明一个名为 Play() 的方法,该方法返回一个字符串。
  • MediaFile.cs:在 Media 命名空间中定义一个名为 MediaFile 的抽象类。它是所有媒体文件的共同基础。它应包含一个 Title 属性(字符串)和一个 Duration 属性(整数,表示秒数)。添加一个用于设置这两个值的构造函数。添加一个名为 GetInfo() 的具体方法,返回 "{Title} ({Duration}s)"。还要声明一个名为 GetType() 的抽象方法,该方法返回一个字符串,由每种媒体类型定义自己的文件类型。
  • AudioFile.cs:在 Media 命名空间中定义一个 AudioFile 类,让它继承 MediaFile 并实现 IPlayable。添加一个 Artist 属性(字符串)。构造函数应接收标题、时长和艺术家。重写 GetType(),使其返回 "Audio"。实现 Play(),使其返回 "Playing audio: {Title} by {Artist}"。
  • Program.cs:在主文件中,使用输入值创建一个 AudioFile。展示这个类的两个方面:调用 GetInfo() 和 GetType()(从抽象类继承而来),然后调用 Play()(来自接口)。将音频文件存储在一个 IPlayable 变量中,再次调用 Play(),以展示接口多态。

你将收到三个输入:

  • 歌曲名称
  • 时长(秒)
  • 艺术家姓名

按以下格式打印输出:

Info: {GetInfo() result}
Type: {GetType() result}
Direct: {Play() result}
Via Interface: {Play() result from IPlayable variable}

例如,如果输入是 Bohemian Rhapsody、354 和 Queen,输出应为:

Info: Bohemian Rhapsody (354s)
Type: Audio
Direct: Playing audio: Bohemian Rhapsody by Queen
Via Interface: Playing audio: Bohemian Rhapsody by Queen

注意,AudioFile 同时受益于这两种方式:它从抽象类继承了通用实现(GetInfo()),同时也满足了 IPlayable 约定。抽象类为相关媒体类型提供代码复用,而接口则让任何不相关的类无需共享继承层次结构也能成为“可播放”的对象。

亲手试试

using System;
using Media;

class Program
{
    public static void Main(string[] args)
    {
        // 读取输入值
        string title = Console.ReadLine();
        int duration = Convert.ToInt32(Console.ReadLine());
        string artist = Console.ReadLine();
        
        // TODO: 使用输入值创建一个 AudioFile
        
        // TODO: 先输出 Info:,再输出 GetInfo() 的结果
        
        // TODO: 先输出 Type:,再输出 GetType() 的结果
        
        // TODO: 先输出 Direct:,再输出 Play() 的结果
        
        // TODO: 将音频文件存入一个 IPlayable 变量
        
        // TODO: 先输出 Via Interface:,再输出 IPlayable 变量的 Play() 结果
    }
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C# 在线编译器