C# 核心进阶:深度解析继承(Inheritance)与多态机制
在面向对象编程(OOP)中,继承是代码复用和构建类层次结构的核心。通过继承,子类可以扩展或定制基类的功能,而无须从零开始构建。
1. 多态(Polymorphism)与引用转换
多态是继承的灵魂。在 C# 中,引用是多态的,这意味着父类型的变量可以指向其子类的对象。
引用转换
- 向上转换(Upcasting):从子类引用创建基类引用。这是隐式的,且仅影响引用类型,不影响被引用的对象本身。
- 向下转换(Downcasting):从基类引用创建子类引用。这必须是显式的,因为它在运行时可能失败并抛出异常。
类型转换工具
为了安全地进行向下转换,C# 提供了以下运算符:
-
as运算符:转换失败时返回null而非抛出异常。 -
is运算符:检查对象是否满足特定模式(如是否属于某个特定类),常用于转换前的类型检查。 -
模式变量:C# 支持在
is检查的同时引入变量(如if (obj is Subclass s)),引入的变量可立即使用。
2. 虚函数成员与重写
子类可以通过重写基类的成员来改变其行为。
-
virtual关键字:允许在子类中被重写。方法、属性、索引器和事件均可声明为虚成员。 -
override关键字:用于提供虚成员的特定实现。重写时,方法的签名、返回值和访问权限必须保持一致。 - 协变返回类型(C# 9):允许重写方法时返回比基类定义更具体的派生类型。
⚠️ 安全警告:从构造器调用虚方法具有潜在危险。因为子类重写的方法可能会在子类字段尚未完全初始化之前被调用。
3. 抽象类与密封类
-
抽象类 (
abstract):不能被实例化,仅作为基类使用。它可以包含不提供默认实现的抽象成员,强制子类实现这些成员。 -
密封类 (
sealed):防止类被继承。同样,sealed也可以作用于重写的函数成员,防止其在更深层的子类中被再次重写。
4. 隐藏继承成员(Shadowing)
当子类定义了与基类同名的成员时,会发生成员隐藏。
- 编译器默认会发出警告。
- 使用
new修饰符 可以明确告诉编译器这种隐藏是有意为之,从而消除警告。 - 注意:
new修饰符隐藏成员与override重写成员在多态下的表现完全不同。
5. base 关键字的妙用
base 关键字在子类中有两个核心用途:
- 访问基类成员:调用被子类重写或隐藏的基类函数实现。
- 调用基类构造器:在子类构造器中显式指定调用父类的哪个构造方法。
6. 构造器的执行与初始化顺序
继承体系下的对象实例化有着严格的执行顺序,理解这一点对排查 Bug 至关重要:
总体原则
- 基类构造器优先:基类的初始化总是先于子类的特定初始化执行。
-
隐式调用:如果子类构造器未显式使用
base关键字,编译器会自动尝试调用基类的无参数构造器。
详细步骤
| 阶段 | 执行动作 | 顺序 |
|---|---|---|
| 初始化阶段 | 字段初始化 & 计算基类构造器参数 | 从子类到基类 |
| 执行阶段 | 构造器方法体执行 | 从基类到子类 |
7. 方法重载与解析
当重载方法在继承体系中被调用时,编译器会根据以下规则决定执行哪个版本:
- 优先匹配最明确的类型:选择参数类型最贴近传入实参的重载版本。
- 静态决定:具体调用哪个重载是在编译时静态决定的,而不是在运行时动态决定的。
技术总结:
继承不仅是“获取父类的代码”,更是一种“是一个(is-a)”的逻辑关系。
通过合理使用 virtual、abstract 和 sealed,可以构建出既灵活又安全的类层次结构。在处理构造器时,务必注意字段初始化与方法体执行的先后逻辑。
定义Book 类
定义一个表示“书”的类,涵盖自动属性、只读字段、构造器和表达式体方法。
using System;
namespace LibrarySystem
{
public class Book
{
// 字段 (Field)
private readonly string _id;
// 属性 (Property)
// 使用自动属性和 init-only setter(C# 9),初始化后不可修改
public string Title { get; init; }
public string Author { get; set; }
public decimal Price { get; set; }
// 构造器 (Constructor)
// 执行类初始化代码,名称与类型相同
public Book(string id, string title, string author)
{
_id = id;
Title = title;
Author = author;
}
// 方法 (Method)
// 使用表达式体方法简写形式
public virtual void DisplayInfo() =>
Console.WriteLine($"ID: {_id}, 书名: {Title}, 作者: {Author}, 价格: {Price:C}");
// 解构器 (Deconstructor)
// 将对象属性反向赋值给变量
public void Deconstruct(out string title, out string author)
{
title = Title;
author = Author;
}
}
}
通过在之前定义的 Book 类基础上创建一个名为 EBook 的派生类(子类),来直观地解释继承的核心概念
在 C# 中,使用 : 符号表示继承。子类会自动获得父类的所有非私有成员。
using System;
namespace LibrarySystem
{
public class EBook : Book
{
// 子类特有的属性
public string DownloadUrl { get; set; }
public double FileSizeMB { get; set; }
// 子类构造器
// 必须使用 base 关键字调用父类构造器,因为父类没有无参构造器
public EBook(string id, string title, string author, string downloadUrl)
: base(id, title, author)
{
DownloadUrl = downloadUrl;
}
// 重写父类方法 (Polymorphism)
// 注意:父类的 DisplayInfo 需标记为 virtual 才能被 override
public override void DisplayInfo()
{
// 使用 base 调用父类的逻辑,避免重复代码
base.DisplayInfo();
Console.WriteLine($"[电子书特有] 下载地址: {DownloadUrl}, 文件大小: {FileSizeMB}MB");
}
}
}
测试父类与子类直接转换
class Program
{
static void Main()
{
// 实例化子类
EBook myEBook = new EBook("EB001", "C# 核心技术", "李四", "https://example.com/dl");
myEBook.Price = 29.9m;
myEBook.FileSizeMB = 15.5;
// 多态:父类型变量指向子类对象
//C# 中被称为“向上转换”(Upcasting),是隐式且安全的
Book normalBook = myEBook;
// 调用的是 EBook 重写后的 DisplayInfo
normalBook.DisplayInfo();
//访问限制
// normalBook.DownloadUrl = "..."; // 编译错误!
// 解释:虽然对象确实有这个属性,但因为变量类型是 Book
// 编译器在编译阶段只能看到 Book 定义的成员
}
}
理解继承的关键点
继承的四个核心技术:
-
代码重用 (Reuse):
EBook不需要重新定义Title、Author和Price,它直接从Book类“继承”了这些功能。 -
构造器链 (Constructor Chaining):创建
EBook时,系统会先调用Book的构造器完成基础初始化,再执行EBook自己的逻辑。这是通过base关键字实现的。 -
方法重写 (Overriding):
EBook通过override改变了DisplayInfo的行为,使其能打印出下载链接。这体现了多态:同样的方法名,在不同对象上有不同的表现。 -
向上转换 (Upcasting):我们可以把
EBook对象赋值给Book类型的变量(如Book normalBook = myEBook)。这是隐式且安全的,因为“电子书也是一种书”。