接口和抽象类都能定义契约,但用法和语义完全不同。选错会导致设计僵硬、难以扩展,甚至引发重复代码。
核心区别:职责与能力
接口描述“能做什么”(能力契约),比如
IComparable表示“能比较大小”,
IDisposable表示“能释放资源”。它不关心怎么实现,只规定必须提供哪些方法/属性/事件。
抽象类描述“是什么”(类型契约),代表一类事物的共性骨架。比如
Animal抽象类可包含字段(
Age)、已实现方法(
Breathe())、抽象方法(
MakeSound())——子类天然继承“是动物”这个身份。
语法限制决定适用场景
接口不能有字段、构造函数、析构函数,不能有访问修饰符(默认 public),所有成员隐式 abstract;C# 8.0+ 虽支持默认实现,但仍是“可选重写”,不改变其契约本质 抽象类可以有字段、构造函数、虚方法、密封方法、静态成员;子类必须通过: base(...)调用父类构造器,体现“is-a”关系 一个类只能继承一个抽象类,但可实现多个接口——这是选择的关键线索:需要多继承能力?选接口
实际选型判断流程
想让不同类(如Button、
File、
NetworkStream)都支持“释放资源”?→ 用
IDisposable接口,无关类型层级 有一组紧密相关的类(
Dog、
Cat、
Bird),共享状态(
Name)、基础行为(
Eat())和待实现行为(
Move())?→ 用
Animal抽象类 已有类层次(如
Shape→
Circle),现在要统一支持序列化?→ 不改继承链,添加
ISerializable接口更安全 未来可能新增实现方式(比如从数据库查用户,将来还要从 API 查)?优先定义
IUserRepository接口,便于 Mock 和替换
常见误用及修正
错误:为单个类设计抽象基类,仅为了“看起来像面向对象”——没复用、没多态必要,纯属过度设计。
错误:在接口里塞大量默认实现,把接口当抽象类用——破坏接口轻量、组合灵活的初衷。
正确做法:先问“这些类是否本质同类?”再问“它们是否需要被不同维度归类?”前者倾向抽象类,后者倾向接口。混合使用很常见:比如
Stream是抽象类(定义字节流基本结构),同时实现
IDisposable和
IAsyncDisposable(叠加能力契约)。
基本上就这些。不复杂但容易忽略——关键不在语法,而在建模时想清楚:你在定义“角色”,还是在定义“身份”。
