C#31、接口和抽象类的区别是什么
在 C#(以及面向对象编程)中,接口(Interface) 和 抽象类(Abstract Class) 都用于定义“契约”或“模板”,支持多态和代码复用,但它们在设计意图、使用场景和能力上有本质区别。
下面从多个维度为你清晰对比:
✅ 一句话总结区别
- 接口:定义“能做什么”(行为契约),完全抽象,强调 “是什么能力”。
- 抽象类:定义“是什么”(类型基类),可包含部分实现,强调 “属于什么类别”。
🔍 核心区别对比表
| 特性 | 接口(interface) | 抽象类(abstract class) |
|---|---|---|
| 关键字 | interface | abstract class |
| 成员实现 | ❌ C# 8 之前不能有实现 ✅ C# 8+ 可提供默认实现(但仍是契约) | ✅ 可包含完整方法实现、字段、构造函数等 |
| 字段/状态 | ❌ 不能有实例字段(只能有 static readonly 常量) | ✅ 可以有字段、属性、状态 |
| 构造函数 | ❌ 不能有 | ✅ 可以有(用于初始化基类状态) |
| 访问修饰符 | 所有成员默认 public(C# 8+ 可设 private/protected) | 成员可有 public、protected、private 等 |
| 继承数量 | ✅ 一个类可实现多个接口 | ❌ 一个类只能继承一个抽象类(C# 不支持多继承) |
| 设计目的 | 定义能力契约(如 IDisposable, IEnumerable) | 定义共通基类(如 Animal → Dog, Cat) |
| 适用关系 | “Can-Do” 关系(会飞、可序列化) | “Is-A” 关系(狗是一种动物) |
✅ 代码示例对比
场景:定义“会飞”的生物
方式1️⃣:用 接口(强调“能力”)
public interface IFlyable
{void Fly(); // C# 8+ 可写默认实现
}public class Bird : IFlyable
{public void Fly() => Console.WriteLine("Bird is flying");
}public class Airplane : IFlyable
{public void Fly() => Console.WriteLine("Airplane is flying");
}
✅ 优势:Bird 和 Airplane 毫无关系,但都能“飞”——接口描述的是能力,不是类型。
方式2️⃣:用 抽象类(强调“类别”)
public abstract class Animal
{public string Name { get; set; }protected int age; // 字段(状态)public abstract void MakeSound(); // 必须子类实现public void Sleep() // 已实现的方法{Console.WriteLine($"{Name} is sleeping");}
}public class Dog : Animal
{public override void MakeSound() => Console.WriteLine("Woof!");
}
✅ 优势:Dog 是一种 Animal,共享 Name、Sleep() 等共通特征。
✅ 如何选择?关键原则
| 问题 | 选接口 | 选抽象类 |
|---|---|---|
| 是否需要多继承? | ✅ 是 | ❌ 否 |
是否要定义跨领域的能力?(如 IComparable, IDisposable) | ✅ 是 | ❌ 否 |
| 是否有共通的状态或代码要复用? | ❌ 否 | ✅ 是 |
| 类之间是否有 “Is-A” 关系? | ❌ 否 | ✅ 是 |
| 是否想强制子类实现某些方法? | ✅ 可以(通过接口方法) | ✅ 可以(通过 abstract 方法) |
📌 经典组合用法:
public abstract class Animal : IDisposable // 继承一个抽象类 + 实现多个接口 {// ... }
⚠️ C# 8+ 的新变化(重要!)
从 C# 8 开始,接口可以包含默认实现:
public interface ILogger
{void Log(string message);// 默认实现(C# 8+)void LogError(string error) => Log($"[ERROR] {error}");
}
但这不改变接口的本质:
- 仍然不能有实例字段(无法保存状态)
- 默认实现是为了向后兼容(避免破坏已有实现类)
- 不应滥用——接口仍应聚焦“契约”,而非实现细节
💡 如果你需要带状态的默认行为,还是应该用抽象类。
✅ .NET 中的经典例子
| 类型 | 示例 | 说明 |
|---|---|---|
| 接口 | IEnumerable<T>, IDisposable, IComparable<T> | 定义通用能力,被无数无关类型实现 |
| 抽象类 | Stream, TextWriter, DbContext | 提供基础实现,子类继承并扩展(如 FileStream, MemoryStream) |
💡 最佳实践建议
- 优先使用接口:当只需要定义行为契约时(尤其是跨模块交互)。
- 用抽象类封装共通逻辑:当多个子类有大量重复代码或共享状态时。
- 接口命名以
I开头(如IRepository),这是 .NET 约定。 - 不要为了“看起来像 Java”而滥用抽象类——C# 更推崇组合 + 接口。
✅ 小结
| 接口(Interface) | 抽象类(Abstract Class) | |
|---|---|---|
| 核心 | 能力契约(Can-Do) | 类型基类(Is-A) |
| 实现 | C# 8+ 可默认实现 | 可完整实现 + 字段 + 构造函数 |
| 继承 | 多实现 ✅ | 单继承 ❌ |
| 状态 | 无实例状态 ❌ | 有状态 ✅ |
🧠 终极口诀:
“接口定能力,抽象定归属;多继承用接口,共代码用抽象。”
问题
为什么不能为在接口中定义的方法指定访问修饰符
因为创建接口的目的是指定实现它的类将公开的公共契约。除了public以外的修饰符将毫无意义。
为什么不能创建抽象类的实例
因为抽象类可能有不包含主体的抽象方法。
如果在该对象上调用此类方法会发生什么?
该方法没有主体,因此行为将是没有定义的。
