C#基础(七):接口(Interface)

接口是 C# 面向对象体系中实现纯契约抽象的核心机制,它只定义 “能做什么” 的行为规范,不包含任何实现细节,是实现 “面向接口编程”、解耦、多继承的核心载体。如果说抽象类是 “不完整的类”,那接口就是 “纯粹的行为契约”。

一、接口的核心本质与设计思想

核心定义

接口是一种引用类型,使用 interface 关键字声明,它是一组行为约定的集合

  • 只定义方法、属性、事件、索引器的签名,默认不包含实现(C# 8.0+ 支持默认实现)
  • 自身不能实例化,必须由类 / 结构体实现后才能使用
  • 支持多继承:一个类 / 结构体可以同时实现多个接口,这是接口和类最关键的区别之一
  • 描述的是 can-do(能做什么) 的关系,而非类继承的 is-a(是什么) 关系

核心价值

  1. 解耦:上层业务依赖接口而非具体实现,替换实现类时无需修改调用代码,符合开闭原则
  2. 突破单继承限制:C# 类只能单继承类,但可以实现任意多个接口,实现能力的组合
  3. 标准化契约:不同团队、不同组件通过接口约定交互规范,互不关心内部实现
  4. 支撑依赖注入与单元测试:面向接口编程是依赖注入、Mock 单元测试的基础
  5. 能力分离:一个类可以实现多个接口,对外暴露不同的能力集,符合接口隔离原则

命名规范

接口名默认以大写 I 开头,采用 PascalCase 命名法,如 IDisposableIEnumerable<T>IComparable

二、 接口的声明与成员

// 声明一个基础接口
public interface IShape
{
    // 方法:只有签名,没有方法体
    double GetArea();
    double GetPerimeter();

    // 属性:只有访问器声明,没有实现
    string Name { get; }

    // 索引器
    int this[int index] { get; set; }

    // 事件
    event EventHandler? ShapeChanged;
}

成员规则

✅ 接口可以包含的成员

  • 方法(普通方法、泛型方法)
  • 属性(读写 / 只读 / 只写)
  • 索引器
  • 事件
  • C# 8.0+:默认方法实现、静态成员、静态构造函数
  • C# 11+:静态抽象成员、静态虚成员(用于泛型数学等场景)

❌ 接口不能包含的成员

  • 实例字段(接口不存储状态)
  • 实例构造函数、析构函数
  • 运算符重载
  • 常量(C# 8.0+ 可定义静态常量)

访问级别

  • 接口成员默认隐式为 public,不能显式添加 public/private 等访问修饰符(默认实现除外)
  • 显式接口实现的成员没有访问修饰符,且默认是私有的,只能通过接口访问

三、接口的两种实现方式

类实现接口分为隐式实现显式实现两种,二者的访问方式、适用场景完全不同。

1. 隐式实现(最常用)

实现类将接口成员声明为 public 的普通成员,既可以通过类实例调用,也可以通过接口引用调用。

// 接口
public interface ILogger
{
    void Log(string message);
}

// 实现类:隐式实现
public class ConsoleLogger : ILogger
{
    // 直接声明 public 方法,与接口签名一致
    public void Log(string message)
    {
        Console.WriteLine($"[日志] {message}");
    }
}

// 调用方式
var logger = new ConsoleLogger();

// 1. 通过类实例直接调用
logger.Log("测试消息");

// 2. 通过接口引用调用
ILogger iLogger = logger;
iLogger.Log("测试消息");

特点:简单直观,是绝大多数场景的默认选择。

2. 显式接口实现

实现时使用 接口名.成员名 的格式,不写访问修饰符,成员只能通过接口类型调用,类实例无法直接访问。

public class FileLogger : ILogger
{
    // 显式实现:接口名.方法名,无访问修饰符
    void ILogger.Log(string message)
    {
        File.AppendAllText("log.txt", message + Environment.NewLine);
    }
}

// 调用
var fileLogger = new FileLogger();

// ❌ 编译报错:类实例无法直接访问显式实现的成员
// fileLogger.Log("测试");

// ✅ 必须转换为接口类型才能调用
ILogger logger = fileLogger;
logger.Log("测试消息");

适用场景

  1. 解决命名冲突:一个类实现多个接口,多个接口有同名方法且语义不同时,必须用显式实现区分
    public interface IA { void Show(); }
    public interface IB { void Show(); }
    
    public class MyClass : IA, IB
    {
        void IA.Show() => Console.WriteLine("IA 的 Show");
        void IB.Show() => Console.WriteLine("IB 的 Show");
    }
    
    // 调用
    var obj = new MyClass();
    ((IA)obj).Show(); // 输出 IA 的 Show
    ((IB)obj).Show(); // 输出 IB 的 Show
  2. 隐藏接口细节:接口方法是给框架调用的,不希望业务代码直接调用时,用显式实现隐藏
  3. 保持类的公共接口干净:避免大量接口方法污染类的公共 API

四、接口的继承与多继承

1. 接口继承接口

接口可以继承一个或多个其他接口,形成接口层次。子接口会继承父接口的所有成员,实现类必须实现整个继承链的所有成员。

// 父接口1
public interface IReadable
{
    string Read();
}

// 父接口2
public interface IWritable
{
    void Write(string content);
}

// 子接口:同时继承两个父接口
public interface IReadWritable : IReadable, IWritable
{
    // 可以扩展自己的成员
    void Clear();
}

// 实现类:必须实现所有父接口+子接口的成员
public class TextFile : IReadWritable
{
    private string _content;

    public string Read() => _content;
    public void Write(string content) => _content = content;
    public void Clear() => _content = string.Empty;
}

2. 类实现多个接口

C# 类只能继承一个基类,但可以实现任意多个接口,这是接口最重要的能力之一,通过接口组合实现能力的复用。

// 多个独立的能力接口
public interface ICanFly { void Fly(); }
public interface ICanSwim { void Swim(); }
public interface ICanRun { void Run(); }

// 组合能力:超人会飞、会跑、会游泳
public class SuperMan : ICanFly, ICanRun, ICanSwim
{
    public void Fly() => Console.WriteLine("飞行中");
    public void Run() => Console.WriteLine("奔跑中");
    public void Swim() => Console.WriteLine("游泳中");
}

设计思想:用接口定义单一能力,通过多实现进行组合,替代笨重的多类继承,符合 “组合优于继承” 的原则。

五、高级特性:默认接口方法(C# 8.0+)

C# 8.0 引入了默认接口方法(Default Interface Methods),允许在接口中为方法编写默认实现,这是接口机制的重大升级。

public interface INotificationService
{
    // 抽象方法:必须由实现类实现
    void Send(string message);

    // 默认实现方法:提供默认逻辑,实现类可以不重写
    void SendBatch(IEnumerable<string> messages)
    {
        foreach (var msg in messages)
        {
            Send(msg); // 调用抽象方法
        }
    }
}

// 实现类:只需要实现 Send 方法,SendBatch 直接复用默认实现
public class SmsService : INotificationService
{
    public void Send(string message)
    {
        Console.WriteLine($"发送短信:{message}");
    }
}

核心调用规则

默认实现的方法只能通过接口类型调用,实现类的实例无法直接访问。

var sms = new SmsService();

// ❌ 编译报错:类实例不能直接调用默认实现
// sms.SendBatch(new[] {"a","b"});

// ✅ 必须转成接口类型才能调用
INotificationService service = sms;
service.SendBatch(new[] { "a", "b" });

核心作用

  1. 接口版本兼容升级:给已发布的接口新增方法时,提供默认实现,不会破坏已有的实现类
  2. 减少重复代码:接口的通用逻辑可以写在默认实现中,避免每个实现类重复编写
  3. 重载方法复用:多个重载方法中,只留一个核心抽象方法,其余重载用默认实现调用核心方法

注意点:钻石继承问题

如果一个类实现的多个接口有同名的默认方法,会产生歧义,此时实现类必须显式重写该方法,消除歧义。

六、泛型接口的协变与逆变

泛型接口支持协变(covariance)逆变(contravariance),通过 outin 关键字标记类型参数,实现泛型类型之间的安全转换,是 C# 类型系统的高级特性。

1. 协变(out):输出方向的兼容性

  • 使用 out 标记类型参数,表示该类型参数仅用于返回值(输出)
  • 支持 “子类转父类”:如果 TChildTParent 的子类,则 IInterface<TChild> 可以隐式转换为 IInterface<TParent>
  • 典型例子:IEnumerable<out T>
// 协变接口:out 标记 T 为协变
public interface IReadOnlyCollection<out T>
{
    T GetItem(int index);
}

// 实现类
public class MyList<T> : IReadOnlyCollection<T>
{
    private T[] _items;
    public T GetItem(int index) => _items[index];
}

// 协变转换:string 是 object 的子类
IReadOnlyCollection<string> strList = new MyList<string>();
// 可以隐式转换为 object 版本
IReadOnlyCollection<object> objList = strList;

原理:既然只能从接口中取出 T(返回值),那么取出 string 当 object 用是完全安全的。

2. 逆变(in):输入方向的兼容性

  • 使用 in 标记类型参数,表示该类型参数仅用于方法参数(输入)
  • 支持 “父类转子类”:如果 TParentTChild 的父类,则 IInterface<TParent> 可以隐式转换为 IInterface<TChild>
  • 典型例子:IComparable<in T>IComparer<in T>
// 逆变接口:in 标记 T 为逆变
public interface IPrinter<in T>
{
    void Print(T item);
}

public class ObjectPrinter : IPrinter<object>
{
    public void Print(object item)
    {
        Console.WriteLine(item.ToString());
    }
}

// 逆变转换:能打印 object,必然能打印 string
IPrinter<object> objPrinter = new ObjectPrinter();
IPrinter<string> strPrinter = objPrinter; // 隐式转换
strPrinter.Print("hello");

原理:既然接口只接收 T 作为参数,那么能处理 object 的方法,处理 string 肯定是安全的。

3. 不变(Invariant)

类型参数既不标记 in 也不标记 out,表示既做输入又做输出,不支持协变逆变转换。

  • 典型例子:List<T> 既可以添加又可以读取,因此是不变的,List<string> 不能转换为 List<object>

七、接口 vs 抽象类:核心对比与选型

二者都能实现抽象,但设计定位完全不同。

对比维度接口(interface)抽象类(abstract class)
核心定位 纯行为契约,定义能力 不完整的类,定义基类模板
继承限制 支持多实现(类 / 接口都可多继承) 仅支持单继承
状态存储 不能包含实例字段,不存储状态 可以包含字段、属性,存储状态
成员实现 默认无实现,C# 8.0+ 支持默认实现 可以包含抽象成员和完整实现成员
构造函数 无实例构造函数 有实例构造函数
继承关系 can-do(能力契约) is-a(本质归属)
版本升级 新增成员默认破坏实现类(默认实现除外) 新增普通成员不影响子类
适用场景 定义能力规范、解耦、多能力组合 复用公共代码、定义模板方法

选型原则

  1. 只定义行为契约,没有公共代码复用 → 用接口
  2. 有大量公共逻辑需要复用,需要存储共享状态 → 用抽象类
  3. 需要多继承 / 多能力组合 → 必须用接口
  4. 作为组件之间的交互标准 → 优先用接口
  5. 模板方法模式(固定算法骨架,子类实现步骤)→ 用抽象类

实际开发中二者经常配合使用:抽象基类实现核心接口,提供通用逻辑,子类继承基类并按需扩展。

八、.NET 内置常用接口

.NET 基类库提供了大量标准接口,掌握常用接口是写出规范 C# 代码的基础

接口核心作用关键方法
IDisposable 释放非托管资源,配合 using 使用 Dispose()
IEnumerable<T> 支持 foreach 遍历,提供迭代器 GetEnumerator()
IEnumerator<T> 迭代器,实现遍历逻辑 MoveNext()Current
IComparable<T> 定义类型的默认比较规则,支持排序 CompareTo(T other)
IComparer<T> 自定义比较器,用于排序时指定规则 Compare(T x, T y)
IEquatable<T> 强类型相等比较,避免装箱 Equals(T other)
ICloneable 克隆对象(不推荐使用,语义模糊) Clone()
IServiceProvider 依赖注入服务获取 GetService(Type type)

九、最佳实践与常见坑点

最佳实践

  1. 面向接口编程,而非面向实现编程:上层依赖接口,下层依赖具体实现,符合依赖倒置原则
  2. 接口粒度要合理:遵循接口隔离原则,不要定义臃肿的大接口;一个接口只定义一组相关的能力
  3. 能力命名而非对象命名:接口名体现能力,如 IComparableIDisposable,而非 IPerson
  4. 优先用接口解耦:跨层交互、第三方库替换、单元测试 Mock 都依赖接口解耦
  5. 显式实现按需使用:没有命名冲突或隐藏需求时,优先用隐式实现,降低调用复杂度

常见坑点

  1. 显式实现成员无法通过类实例调用:必须先转换为接口类型,这是新手最常踩的坑
  2. 默认接口方法只能通过接口调用:实现类不能直接调用,重写后才可以通过类访问
  3. 接口成员默认是 public:不要手动加 public,显式实现不能加任何访问修饰符
  4. 协变逆变只能用于接口和委托:类、结构体的泛型不支持协变逆变
  5. 不要滥用接口:简单的内部逻辑不需要强行抽接口,过度抽象会增加代码复杂度
  6. 接口不能定义实例字段:接口是行为契约,不负责存储状态,状态由实现类维护
posted @ 2026-07-23 09:32  挺秃然的i  阅读(5)  评论(0)    收藏  举报