通过


接口 - 定义多种类型的行为

小窍门

开发软件的新手? 首先开始 学习入门 教程。 当你需要在不相关的类型之间定义共享行为时,你将会遇到接口。

是否在其他语言中有经验? C# 接口类似于 Java 中的接口或 Swift 中的协议。 跳过 C# 特定模式的显式实现部分。

接口定义了一种契约:一组相关的方法、属性、事件和索引器,这些必须由classstruct实现。 接口允许单个类型实现多个协定,这一点很重要,因为 C# 不支持多个类的继承。 结构不能继承自其他结构或类,因此接口是跨结构类型添加共享行为的唯一方法。

以下示例声明一个接口和一个实现它的类:

interface IEquatable<T>
{
    bool Equals(T obj);
}
public class Car : IEquatable<Car>
{
    public string? Make { get; set; }
    public string? Model { get; set; }
    public string? Year { get; set; }

    public bool Equals(Car? car) =>
        car is not null &&
        (Make, Model, Year) == (car.Make, car.Model, car.Year);
}

实现 IEquatable<T> 的任何类或结构都必须提供与 Equals 接口签名匹配的方法。 无论具体类型如何,所有 IEquatable<T> 实现都可以确保支持相等性比较。 这种可预测性是接口的核心价值。

声明接口

使用 interface 关键字定义接口。 按照约定,接口名称以大写 I开头:

interface ILogger
{
    void Log(string message);
    string Name { get; }
}

接口可以包含方法、属性、事件和索引器。 接口不能包含实例字段、实例构造函数或终结器。 成员默认为 public 可以根据需要指定其他访问修饰符。 例如,使用 internal 来标记那些不应在程序集外部可见的成员。

实现接口

类或结构列出它在声明中冒号后实现的接口。 该类必须为接口中声明的每个成员提供实现:

public class ConsoleLogger : ILogger
{
    public string Name => "Console";

    public void Log(string message) =>
        Console.WriteLine($"[{Name}] {message}");
}

public class FileLogger : ILogger
{
    public string Name => "File";

    public void Log(string message)
    {
        // In a real app, write to a file
        Console.WriteLine($"[{Name}] Writing to file: {message}");
    }
}

类可以实现多个接口,用逗号分隔。 它必须为它列出的每个接口中的所有成员提供实现。

显式实现

有时,需要实现接口成员,而不使其成为类的公共 API 的一部分。 显式实现 使用接口名称标识成员。 该成员只能通过接口类型的变量进行访问:

interface IMetric
{
    double GetDistance(); // Returns meters
}

interface IImperial
{
    double GetDistance(); // Returns feet
}

public class Runway(double meters) : IMetric, IImperial
{
    // Explicit implementation for IMetric
    double IMetric.GetDistance() => meters;

    // Explicit implementation for IImperial
    double IImperial.GetDistance() => meters * 3.28084;
}

当两个接口声明具有相同名称的成员或想要使类的公共图面保持干净时,显式实现非常有用。 有关更多详细信息,请参阅 显式接口实现

接口继承

接口可从一个或多个接口继承。 实现派生接口的类必须实现派生接口及其所有基接口中的所有成员:

interface IDrawable
{
    void Draw();
}

interface IShape : IDrawable
{
    double Area { get; }
}

public class Circle(double radius) : IShape
{
    public double Area => Math.PI * radius * radius;

    public void Draw() =>
        Console.WriteLine($"Drawing circle with area {Area:F2}");
}

实现的 IShape 类可以隐式转换为 IDrawable,因为 IShape 继承自该类。

接口与抽象类

接口和抽象类都定义了派生类型必须满足的协定。

  • 当相关类型共享状态(字段)、构造函数或非公共成员时,请使用抽象类。 抽象类允许你通过添加具有默认行为的新成员来改进层次结构,而无需中断现有的派生类型。
  • 当类型需要满足跨不相关的层次结构的协定,或者需要实现多个协定时,请使用接口。 接口无法声明实例字段或构造函数,因此它们最适合将功能添加到已有基类的类型。 对于高级方案,接口还支持 默认成员实现

类只能从一个基类继承,但可以实现多个接口。 这种区别通常使接口成为定义跨类型层次结构的功能的更好选择。

使用内部接口

通常,只要可以公开访问接口签名中的所有类型,就可以实现具有公共成员的内部接口。 当接口在其成员签名中使用内部类型时,必须使用显式实现,因为实现成员在公开内部类型时不能公开:

internal class InternalConfiguration
{
    public string Setting { get; set; } = "";
}

internal interface ILoggable
{
    void Log(string message);
}

internal interface IConfigurable
{
    void Configure(InternalConfiguration config);
}

public class ServiceImplementation : ILoggable, IConfigurable
{
    // Implicit implementation: ILoggable uses only public types in its signature
    public void Log(string message) =>
        Console.WriteLine($"Log: {message}");

    // Explicit implementation: IConfigurable uses internal types
    void IConfigurable.Configure(InternalConfiguration config) =>
        Console.WriteLine($"Configured with: {config.Setting}");
}

在前面的示例中, IConfigurable 在其方法签名中使用内部类型 InternalConfigurationServiceImplementation 对该成员使用显式实现。 相比之下, ILoggable 仅在其签名中使用公共类型(string),并且可以隐式实现。

默认接口成员和静态抽象成员

接口支持两个超越基本协定的高级功能:

  • 默认接口成员 允许接口提供方法主体。 实现类型继承默认实现,并且可以选择性地替代它。 有关详细信息,请参阅默认接口方法
  • 静态抽象成员 需要实现类型以提供静态成员,这对于定义运算符协定或工厂模式很有用。 有关详细信息,请参阅 接口中的静态抽象成员

这两个功能都在语言参考中关于接口的文章中介绍。 大多数日常接口用法都涉及本文前面所述的声明和实现模式。

接口摘要

  • 接口定义方法、属性、事件和索引器的协定。
  • 实现接口的类或结构必须为所有声明的成员提供实现(除非接口提供默认实现)。
  • 无法直接实例化接口。
  • 一个类或结构可以实现多个接口。 一个类可以继承一个基类,还可实现一个或多个接口。
  • 接口名称通常以 . 开头 I