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(); // "旺财 在吃东西"关键点:
- 单继承:一个类只能继承一个父类(避免 C++ 多继承的复杂问题)。
- 所有类默认继承 object:你没写父类,编译器自动继承
System.Object。 - 构造函数不被继承:子类用自己的构造,用
base(...)调父类构造。 - private 不被继承:子类访问不到父类的 private 字段(但它们存在)。
二、virtual 与 override(多态)
多态是面向对象的精髓:同一接口,不同表现。C# 的多态要明确声明:
- 父类方法用
virtual标记。 - 子类用
override重写。 - 父类引用调用,实际执行子类版本。
看上面 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()抽象类的特点:
- 不能 new,只能通过具体子类使用。
- 抽象方法必须被子类 override(强制契约)。
- 可以有字段、属性、构造函数、具体方法。
- 适合"模板":定义通用框架,具体实现交给子类。
四、virtual vs abstract 速查
- virtual:普通类,方法有默认实现,子类可以 override(可选)。
- abstract:抽象类,方法无实现,子类必须 override(强制)。
- override:子类重写父类的 virtual/abstract 方法。
- sealed override:子类重写后再禁止孙类重写。
五、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 的用途:
- 设计层面:防止类被继承,保护作者意图(如 string 是 sealed)。
- 性能层面:JIT 可对 sealed 方法做内联优化和去虚化(devirtualization)。
- 安全层面:防止恶意子类绕过基类的不变式。
六、方法隐藏(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建议:
- 判断 + 转换:用模式匹配
is Dog d。 - 转换可能失败:用
as(返回 null,不抛异常)。 - 肯定能转:用强转
(Dog)a(失败抛异常)。 - 避免过多类型检查:善用多态和接口,不要写一大堆
if (x is ...)。
九、里氏替换原则(LSP)
继承的核心原则:子类对象必须能替换掉所有父类对象,而程序的行为不变。违反 LSP 的常见例子:
- 经典反例:正方形继承长方形——重写 Length/Width 后,长方形的不变式被破坏。
- 子类抛出父类不会抛的异常:违反契约。
- 子类拒绝父类接受的输入:收紧前置条件。
- 子类减弱父类的承诺:放宽后置条件。
设计建议:用组合而非继承(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# 接口 →