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. 最佳实践与常见陷阱

  1. 接口命名以 I 开头:IService, IRepository,这是 .NET 生态的强约定。
  2. 保持接口精简:遵循接口隔离原则(ISP)。一个大接口拆成多个小接口优于一个臃肿的接口。
  3. 不要滥用默认方法:默认方法是为了向后兼容和横切关注点,不是替代抽象类的工具。如果大部分成员都有默认实现,你可能应该用抽象类。
  4. 单元测试的利器:依赖接口而非具体类注入,使得 Mock/Fake 变得简单:
    // 测试时可以轻松替换
    public class OrderService(IRepository<Order> repo) { ... }
    
  5. 泛型接口注意变体:
    • out T(协变):IEnumerable<out T> — 只能作为返回值
    • in T(逆变):IComparer<in T> — 只能作为参数
    • 不加修饰:IList<T> — 既作参数又作返回值
  6. 记录类型也能实现接口:
    public record Point(double X, double Y) : IShape
    {
        public double GetArea() => 0; // 点是零维的
    }
    

📌 一句话总结

接口 = 纯行为契约 + 可选的默认胶水代码。它是解耦、测试和多态的基石。在现代 C# 中,优先用接口定义系统边界,用抽象类复用内部实现,用默认方法安全地演进接口。

。51

更多推荐