c# 第七章 抽像,接口
48.重写
using System.Diagnostics;
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine(“我要叫了”);
}
}
class Dog : Animal
{
public override void MakeSound()//重写案例
{
Console.WriteLine($“在旺旺叫”);
}
}
抽象类和抽象方法
在 C# 中,抽象类(Abstract Class)和抽象方法(Abstract Method)是构建面向对象框架、定义“契约”的核心工具。它们强制规定了子类“必须做什么”,但不关心“具体怎么做”。
核心概念速览
特性 抽象类 (abstract class) 抽象方法 (abstract method)
关键字 abstract 修饰类 abstract 修饰方法/属性
能否实例化 ❌ 绝对不能 new N/A
是否有实现体 可以包含已实现的方法 + 抽象方法 ❌ 不能有方法体,连 {} 都不行
谁来实现 非抽象的派生类 非抽象的派生类必须用 override 实现
设计意图 提供部分通用代码 + 定义子类契约 定义子类必须履行的行为约定
⚠️ 铁律:抽象方法只能存在于抽象类中。但抽象类不一定非要包含抽象方法(它可以全是已实现的方法,只是不想被直接实例化)。
代码示例:从理论到实践
基础语法
// ✅ 抽象类:不能 new Shape()
public abstract class Shape
{
// 普通字段/属性:所有子类共享
public string Color { get; set; } = “Black”;
// ✅ 已实现的方法:子类可以直接用或 override
public void DisplayColor() => Console.WriteLine("Color: {Color}");
// ✅ 抽象方法:没有方法体,以分号结尾
// 强制要求每个具体形状都必须自己计算面积
public abstract double GetArea();
// ✅ 抽象属性也是允许的
public abstract string Name { get; }
}
// ✅ 具体类:必须实现所有抽象成员
public class Circle : Shape
{
public double Radius { get; set; }
// 必须用 override 实现
public override double GetArea() => Math.PI * Radius * Radius;
public override string Name => "Circle";
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double GetArea() => Width * Height;
public override string Name => "Rectangle";
}
使用方式
// ❌ 编译错误!抽象类不能实例化
// Shape s = new Shape();
// ✅ 通过具体子类实例化,但可以用基类引用(多态)
Shape circle = new Circle { Radius = 5, Color = “Red” };
Shape rect = new Rectangle { Width = 4, Height = 6 };
Console.WriteLine(circle.GetArea()); // 78.539…
circle.DisplayColor(); // Color: Red (继承的已实现方法)
// ✅ 多态遍历
List shapes = new List { circle, rect };
foreach (var shape in shapes)
{
Console.WriteLine(“{shape.Name}: Area = {shape.GetArea():F2}”);
}
抽象类 vs 接口:到底该用哪个?
这是最常见的困惑。C# 8+ 之后接口也能有默认实现了,区别在哪?
对比维度 抽象类 接口 (interface)
继承数量 单继承(一个类只能继承一个抽象类) 多实现(一个类可实现多个接口)
状态(字段) ✅ 可以有实例字段、构造函数 ❌ 不能有实例字段(仅有静态字段)
访问修饰符 ✅ 支持 protected、private 等 成员默认 public(C# 8+ 可显式指定)
已实现代码 ✅ 完整的实现,可访问私有状态 ⚠️ 默认方法不能访问实例字段
版本演进 ✅ 新增已实现方法不会破坏子类 ⚠️ 新增默认方法可能有菱形继承问题
语义 “是什么” (is-a),强调血缘关系 “能做什么” (can-do),强调能力契约
💡 选择原则:
多个类有共同的代码逻辑和状态需要复用 → 抽象类
定义跨不同类型体系的通用能力(如 IDisposable, IComparable)→ 接口
两者结合:抽象类实现接口,提供部分默认实现,留给子类完成剩余部分
经典设计模式应用
抽象类最闪耀的场景是在模板方法模式(Template Method Pattern) 中:
public abstract class DataProcessor
{
// 🔑 模板方法:定义了算法骨架,且不允许子类篡改流程
public void Process()
{
LoadData(); // 步骤1:子类自定义
Validate(); // 步骤2:通用逻辑,子类不用管
Transform(); // 步骤3:子类自定义
Save(); // 步骤4:通用逻辑
}
protected abstract void LoadData(); // 子类必须实现
protected abstract void Transform(); // 子类必须实现
// 通用步骤,子类可选择性 override
protected virtual void Validate()
=> Console.WriteLine("Default validation passed");
private void Save() // 私有方法,子类完全无法修改
=> Console.WriteLine("Saving to database...");
}
核心价值:父类控制流程不变,子类只负责填充细节。这比纯接口强大得多,因为接口无法强制执行调用顺序。
常见陷阱与注意事项
忘记 override:子类实现抽象方法时漏写 override,编译器会报错(不像隐藏那样只是警告)。这其实是好事,避免了意外隐藏。
抽象类中的构造函数:虽然不能 new 抽象类,但可以有构造函数!它用于初始化抽象类的字段,由子类的 base() 调用。
public abstract class Shape
{
protected Shape(string color) { Color = color; } // ✅ 合法
}
public class Circle : Shape
{
public Circle() : base(“Red”) { } // 子类调用
}
sealed + override:如果某个抽象方法的实现在某一层已经“终极”了,不想让更下层再重写:
public override sealed double GetArea() => …; // 到此为止,不能再 override
不要为了抽象而抽象:如果你只有一个子类,或者不确定未来是否需要多态,先用具体类。等到真正出现第二个相似类、发现重复代码时,再提取抽象类。过早抽象是万恶之源。
📌 一句话总结
抽象类 = 半成品模板。它把“确定的代码”写好,把“不确定的部分”挖成坑(抽象方法),逼着子类去填坑。它是代码复用和行为约束的完美平衡点。
接口 interface 定义和实现
在 C# 中,接口(Interface) 是定义行为契约的核心机制。它只规定“能做什么”,不关心“怎么做”。从 C# 8.0 开始,接口的能力有了重大增强,不再仅仅是纯粹的抽象契约。
1. 核心概念速览
| 特性 | 传统接口 (C# 7 及以前) | 现代接口 (C# 8+) |
|---|---|---|
| 实例成员 | 仅方法、属性、事件、索引器的签名 | ✅ 可包含默认实现(Default Interface Methods) |
| 静态成员 | ❌ 不允许 | ✅ 允许静态方法、字段、属性 |
| 访问修饰符 | 隐式 public | ✅ 支持 private, protected, internal 等 |
| 多重继承 | ✅ 一个类可实现多个接口 | ✅ 不变,但需注意菱形继承问题 |
| 状态(字段) | ❌ 不能有实例字段 | ❌ 仍然不能有实例字段 |
| 设计语义 | “Can-Do” 能力契约 | 能力契约 + 可选的默认行为 |
⚠️ 铁律:接口永远不能被实例化(
new IMyInterface()永远非法),也永远不能包含实例字段。
2. 基础定义与实现
标准用法
// 定义接口
public interface IRepository<T>
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
bool Delete(int id);
}
// 实现接口
public class SqlUserRepository : IRepository<User>
{
// 隐式实现:公共方法名必须完全匹配
public User GetById(int id) => /* SQL查询 */;
public IEnumerable<User> GetAll() => /* SQL查询 */;
public void Add(User entity) => /* SQL插入 */;
public bool Delete(int id) => /* SQL删除 */;
}
显式接口实现(EII)
当两个接口有同名方法,或你想把接口方法从类的公共 API 中隐藏时使用:
public interface ILogger { void Log(string msg); }
public interface IAuditLogger { void Log(string msg); }
public class DualLogger : ILogger, IAuditLogger
{
// 显式实现:无访问修饰符,不能通过类实例直接调用
void ILogger.Log(string msg) => Console.WriteLine($"[LOG] {msg}");
void IAuditLogger.Log(string msg) => File.AppendAllText("audit.log", msg);
// 使用时必须转型
// ((ILogger)logger).Log("hello");
// ((IAuditLogger)logger).Log("hello");
}
💡 何时用显式实现?
- 解决命名冲突
- 接口方法是实现细节,不应暴露给类的直接使用者(如
IEnumerator的实现通常隐藏Reset())- 强制调用方通过接口引用访问,确保多态行为
3. C# 8+ 新特性:默认接口方法 (DIM)
这是现代 C# 接口最重要的变化,解决了接口演进的向后兼容问题。
public interface IShape
{
// 抽象成员:实现类必须提供
double GetArea();
// ✅ 默认实现:实现类可以直接用,也可以 override
public string Describe() => $"Shape with area {GetArea():F2}";
// ✅ 私有辅助方法:仅在接口内部可用
private static string FormatNumber(double n) => n.ToString("F2");
// ✅ 静态成员
public static IShape CreateCircle(double radius) => new Circle(radius);
}
public class Circle : IShape
{
private readonly double _radius;
public Circle(double r) => _radius = r;
public double GetArea() => Math.PI * _radius * _radius;
// Describe() 不需要实现,自动获得默认版本
// 如果想自定义:public string Describe() => "...";
}
⚠️ 菱形继承问题
当一个类实现了两个接口,且这两个接口为同一方法提供了不同的默认实现时:
interface IA { void M() => Console.WriteLine("A"); }
interface IB { void M() => Console.WriteLine("B"); }
class C : IA, IB { } // ❌ 编译错误!C.M() 歧义
// ✅ 解决方案:类必须显式提供自己的实现
class C : IA, IB
{
public void M() => Console.WriteLine("C");
}
关键区别:默认接口方法中的
base(IXxx).Method()语法可以在实现类中调用特定接口的默认实现来消歧。
4. 接口 vs 抽象类:决策指南
需要定义跨类型的通用能力? ──Yes──→ 接口
│No
▼
多个子类有共享的状态/代码? ──Yes──→ 抽象类
│No
▼
是否需要多重继承? ──Yes──→ 接口
│No
▼
先用具体类,等有重复再重构
| 场景 | 推荐 |
|---|---|
IDisposable, IComparable, IEnumerable | 接口 |
| 数据访问层的统一契约 | 接口(方便 Mock 测试) |
| 模板方法模式(固定算法骨架) | 抽象类 |
| 共享字段、构造函数逻辑 | 抽象类 |
| 向已有接口添加方法且不破坏现有实现 | 接口 + 默认方法 |
| 表示 “is-a” 血缘关系 | 抽象类 |
5. 最佳实践与常见陷阱
- 接口命名以
I开头:IService,IRepository,这是 .NET 生态的强约定。 - 保持接口精简:遵循接口隔离原则(ISP)。一个大接口拆成多个小接口优于一个臃肿的接口。
- 不要滥用默认方法:默认方法是为了向后兼容和横切关注点,不是替代抽象类的工具。如果大部分成员都有默认实现,你可能应该用抽象类。
- 单元测试的利器:依赖接口而非具体类注入,使得 Mock/Fake 变得简单:
// 测试时可以轻松替换 public class OrderService(IRepository<Order> repo) { ... } - 泛型接口注意变体:
out T(协变):IEnumerable<out T>— 只能作为返回值in T(逆变):IComparer<in T>— 只能作为参数- 不加修饰:
IList<T>— 既作参数又作返回值
- 记录类型也能实现接口:
public record Point(double X, double Y) : IShape { public double GetArea() => 0; // 点是零维的 }
📌 一句话总结
接口 = 纯行为契约 + 可选的默认胶水代码。它是解耦、测试和多态的基石。在现代 C# 中,优先用接口定义系统边界,用抽象类复用内部实现,用默认方法安全地演进接口。
。51
更多推荐

所有评论(0)