C# 继承与多态

继承是面向对象代码复用的核心机制——子类继承父类的字段和方法,在不重复造轮子的前提下扩展功能。C# 是单继承(只能继承一个父类),但通过接口(下一篇讲)实现了"多继承"的灵活性。这一篇我们看继承语法、virtual/override 多态、abstract 抽象类、sealed 密封。

一、继承的基本语法

C# 用 : 表示继承(Java 是 extends):

// 父类(基类)
public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    // 普通方法:子类不能 override(默认隐藏)
    public void Eat()
    {
        Console.WriteLine($"{Name} 在吃东西");
    }

    // virtual:子类可以 override
    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} 发出声音");
    }
}

// 子类:用 : 继承
public class Dog : Animal
{
    public Dog(string name) : base(name) { }   // base 调用父类构造

    // override:重写虚方法
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 汪汪汪!");
    }
}

// 使用
Animal a = new Dog("旺财");
a.MakeSound();   // "旺财 汪汪汪!"(多态)
a.Eat();         // "旺财 在吃东西"

关键点:

二、virtual 与 override(多态)

多态是面向对象的精髓:同一接口,不同表现。C# 的多态要明确声明:

看上面 Animal a = new Dog("旺财") 调用 a.MakeSound() 输出"汪汪汪"——这就是多态。父类声明,子类实现,运行时决定调用哪个版本

为什么不默认多态(像 Java)?因为 C# 设计哲学是显式优于隐式——virtual 有性能开销,默认关闭,需要时显式声明。

三、抽象类 abstract

抽象类是"半成品"类:不能实例化,只能被继承;可以包含抽象方法(只有声明,无实现):

// abstract 抽象类:不能实例化,只能被继承
public abstract class Shape
{
    public string Name { get; set; }

    // 抽象方法:子类必须 override(只有声明,无实现)
    public abstract double Area();

    // 普通方法:可以有实现
    public void Print()
    {
        Console.WriteLine($"{Name} 的面积 = {Area():F2}");
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double r) { Name = "圆形"; Radius = r; }

    public override double Area() => Math.PI * Radius * Radius;
}

public class Rectangle : Shape
{
    public double W { get; set; }
    public double H { get; set; }

    public Rectangle(double w, double h) { Name = "矩形"; W = w; H = h; }

    public override double Area() => W * H;
}

// Shape s = new Shape();  // ❌ 抽象类不能实例化
var shapes = new Shape[] { new Circle(3), new Rectangle(2, 4) };
foreach (var s in shapes)
    s.Print();   // 多态:各自调用不同的 Area()

抽象类的特点:

四、virtual vs abstract 速查

五、sealed 密封

sealed 表示"到此为止":

// sealed:密封类,不能被继承
public sealed class FinalClass
{
    public void DoSomething() { }
}

// ❌ 编译报错
// public class Sub : FinalClass { }

// 密封方法:子类不能再次 override(终止多态链)
public class Base
{
    public virtual void Method() { }
}

public class Mid : Base
{
    public sealed override void Method() { }   // sealed + override
}

// ❌ 编译报错
// public class Leaf : Mid
// {
//     public override void Method() { }   // Mid.Method 已密封
// }

// .NET 内置许多 sealed 类:string、DateTime、int struct 都是
// 性能优化:sealed 让 JIT 内联方法调用更高效

sealed 的用途:

六、方法隐藏(new)—— 别用!

如果父类方法不是 virtual,子类同名方法默认会隐藏父类方法。但这不是多态——调用哪个版本看编译时类型:

public class Base
{
    public void Show() { Console.WriteLine("Base.Show"); }
}

public class Sub : Base
{
    // new:显式隐藏父类方法(不用 new 编译器会警告)
    public new void Show() { Console.WriteLine("Sub.Show"); }
}

Sub s = new Sub();
s.Show();           // "Sub.Show"

Base b = s;
b.Show();           // "Base.Show"(注意:不像 override!)

// ⚠️ 强烈建议:用 virtual/override 而不是 new 隐藏
// new 隐藏破坏多态,容易引发 bug

实际开发几乎不用 new 隐藏——它是给历史遗留代码兜底的。要用多态就用 virtual/override

七、构造函数链

子类构造函数会隐式调用父类无参构造。如果父类没有无参构造,必须显式 base(...):

public class Base
{
    public Base()
    {
        Console.WriteLine("Base 无参构造");
    }

    public Base(int x) : this()   // : this(...) 调本类其他构造
    {
        Console.WriteLine($"Base(int x={x})");
    }
}

public class Sub : Base
{
    public Sub() : base(100)      // : base(...) 调父类构造
    {
        Console.WriteLine("Sub 无参构造");
    }
}

new Sub();
// 输出顺序:
// Base 无参构造
// Base(int x=100)
// Sub 无参构造
// (构造顺序:父类先,子类后;this/base 必须是构造函数第一行)

构造顺序:父类先,子类后(从最顶层基类开始,逐层向下)。析构顺序相反(子类先,父类后),但 C# 用 GC 不需要手动析构。

八、类型转换:is / as / 强转

Animal a = new Dog("旺财");

// is:类型检查
if (a is Dog)
    Console.WriteLine("是 Dog");

// 模式匹配(C# 7+)
if (a is Dog d)
    d.MakeSound();

// as:转换失败返回 null(不抛异常)
Dog dog = a as Dog;
if (dog != null)
    dog.MakeSound();

// 强制转换:失败抛 InvalidCastException
Dog dog2 = (Dog)a;

// GetType vs typeof
Type t1 = a.GetType();   // Dog(运行时类型)
Type t2 = typeof(Animal); // Animal(编译时类型)
Console.WriteLine(t1 == t2);   // False

建议:

九、里氏替换原则(LSP)

继承的核心原则:子类对象必须能替换掉所有父类对象,而程序的行为不变。违反 LSP 的常见例子:

设计建议:用组合而非继承(Has-A 而不是 Is-A)。继承深度别超过 3 层,否则维护噩梦。

十、Object 基类的虚方法

所有类继承自 object,它定义了几个可重写的虚方法:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    // 重写 ToString:返回对象的字符串表示
    public override string ToString() => $"{Name} ({Age})";

    // 重写 Equals:基于值的相等比较
    public override bool Equals(object obj)
    {
        if (obj is Person other)
            return Name == other.Name && Age == other.Age;
        return false;
    }

    // 重写 GetHashCode(Equals 重写必须配套)
    public override int GetHashCode() => HashCode.Combine(Name, Age);
}

// C# 还能 record 自动实现这些,无需手写

实际开发中,优先用 record(自动生成 Equals、GetHashCode、ToString),只有在自定义类需要时才手写。

十一、经典案例:模板方法模式

// 抽象类定义算法骨架,具体步骤留给子类
public abstract class OrderProcessor
{
    // 模板方法:固定流程
    public void Process()
    {
        Validate();
        Charge();
        Ship();
        Notify();
    }

    protected abstract void Validate();
    protected abstract void Charge();

    // 钩子方法:子类可选重写
    protected virtual void Ship() => Console.WriteLine("默认发货");
    protected virtual void Notify() => Console.WriteLine("默认通知");
}

public class DigitalOrderProcessor : OrderProcessor
{
    protected override void Validate() => Console.WriteLine("校验数字商品");
    protected override void Charge() => Console.WriteLine("扣款");
    protected override void Ship() => Console.WriteLine("邮件发货");
}

这是抽象类 + 钩子方法的经典应用——模板方法模式,框架代码不重复。

小结

继承 + 多态是面向对象的灵魂。记住几个原则:单继承(用接口扩展)、用 virtual/override(不要 new 隐藏)、抽象类做模板sealed 终止继承组合优于继承。下一篇我们看接口——比继承更灵活的契约机制。

← 上一篇 C# 类与面向对象

下一篇 C# 接口

✈️💬