C#基础(七):接口(Interface)
接口是 C# 面向对象体系中实现纯契约抽象的核心机制,它只定义 “能做什么” 的行为规范,不包含任何实现细节,是实现 “面向接口编程”、解耦、多继承的核心载体。如果说抽象类是 “不完整的类”,那接口就是 “纯粹的行为契约”。
一、接口的核心本质与设计思想
核心定义
接口是一种引用类型,使用 interface 关键字声明,它是一组行为约定的集合:
- 只定义方法、属性、事件、索引器的签名,默认不包含实现(C# 8.0+ 支持默认实现)
- 自身不能实例化,必须由类 / 结构体实现后才能使用
- 支持多继承:一个类 / 结构体可以同时实现多个接口,这是接口和类最关键的区别之一
- 描述的是 can-do(能做什么) 的关系,而非类继承的 is-a(是什么) 关系
核心价值
- 解耦:上层业务依赖接口而非具体实现,替换实现类时无需修改调用代码,符合开闭原则
- 突破单继承限制:C# 类只能单继承类,但可以实现任意多个接口,实现能力的组合
- 标准化契约:不同团队、不同组件通过接口约定交互规范,互不关心内部实现
- 支撑依赖注入与单元测试:面向接口编程是依赖注入、Mock 单元测试的基础
- 能力分离:一个类可以实现多个接口,对外暴露不同的能力集,符合接口隔离原则
命名规范
接口名默认以大写 I 开头,采用 PascalCase 命名法,如 IDisposable、IEnumerable<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("测试消息");
适用场景
- 解决命名冲突:一个类实现多个接口,多个接口有同名方法且语义不同时,必须用显式实现区分
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
- 隐藏接口细节:接口方法是给框架调用的,不希望业务代码直接调用时,用显式实现隐藏
- 保持类的公共接口干净:避免大量接口方法污染类的公共 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" });
核心作用
- 接口版本兼容升级:给已发布的接口新增方法时,提供默认实现,不会破坏已有的实现类
- 减少重复代码:接口的通用逻辑可以写在默认实现中,避免每个实现类重复编写
- 重载方法复用:多个重载方法中,只留一个核心抽象方法,其余重载用默认实现调用核心方法
注意点:钻石继承问题
如果一个类实现的多个接口有同名的默认方法,会产生歧义,此时实现类必须显式重写该方法,消除歧义。
六、泛型接口的协变与逆变
泛型接口支持协变(covariance)和逆变(contravariance),通过 out 和 in 关键字标记类型参数,实现泛型类型之间的安全转换,是 C# 类型系统的高级特性。
1. 协变(out):输出方向的兼容性
- 使用
out标记类型参数,表示该类型参数仅用于返回值(输出) - 支持 “子类转父类”:如果
TChild是TParent的子类,则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标记类型参数,表示该类型参数仅用于方法参数(输入) - 支持 “父类转子类”:如果
TParent是TChild的父类,则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(本质归属) |
| 版本升级 | 新增成员默认破坏实现类(默认实现除外) | 新增普通成员不影响子类 |
| 适用场景 | 定义能力规范、解耦、多能力组合 | 复用公共代码、定义模板方法 |
选型原则
- 只定义行为契约,没有公共代码复用 → 用接口
- 有大量公共逻辑需要复用,需要存储共享状态 → 用抽象类
- 需要多继承 / 多能力组合 → 必须用接口
- 作为组件之间的交互标准 → 优先用接口
- 模板方法模式(固定算法骨架,子类实现步骤)→ 用抽象类
实际开发中二者经常配合使用:抽象基类实现核心接口,提供通用逻辑,子类继承基类并按需扩展。
八、.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) |
九、最佳实践与常见坑点
最佳实践
- 面向接口编程,而非面向实现编程:上层依赖接口,下层依赖具体实现,符合依赖倒置原则
- 接口粒度要合理:遵循接口隔离原则,不要定义臃肿的大接口;一个接口只定义一组相关的能力
- 能力命名而非对象命名:接口名体现能力,如
IComparable、IDisposable,而非IPerson - 优先用接口解耦:跨层交互、第三方库替换、单元测试 Mock 都依赖接口解耦
- 显式实现按需使用:没有命名冲突或隐藏需求时,优先用隐式实现,降低调用复杂度
常见坑点
- 显式实现成员无法通过类实例调用:必须先转换为接口类型,这是新手最常踩的坑
- 默认接口方法只能通过接口调用:实现类不能直接调用,重写后才可以通过类访问
- 接口成员默认是 public:不要手动加
public,显式实现不能加任何访问修饰符 - 协变逆变只能用于接口和委托:类、结构体的泛型不支持协变逆变
- 不要滥用接口:简单的内部逻辑不需要强行抽接口,过度抽象会增加代码复杂度
- 接口不能定义实例字段:接口是行为契约,不负责存储状态,状态由实现类维护

浙公网安备 33010602011771号