C# 继承完全指南:从基类派生到接口多重继承,一篇搞懂
本文系统梳理 C# 中继承的核心概念,涵盖基类与派生类、访问修饰符、
base关键字、方法重写与隐藏、sealed密封类、抽象类、接口多重继承、里氏替换原则以及常见面试题,适合有一定 C# 基础的开发者深入学习。
目录
- 一、继承是什么:为什么需要它
- 二、基本语法:基类与派生类
- 三、访问修饰符在继承中的表现
- 四、base 关键字:与父类对话
- 五、方法的隐藏与重写:new vs override
- 六、sealed:密封类与密封方法
- 七、抽象类与抽象方法
- 八、接口与多重继承
- 九、里氏替换原则(LSP)
- 十、万物之根:object 类
- 十一、高频面试题精选
- 十二、最佳实践与避坑指南
- 总结
一、继承是什么:为什么需要它
继承是面向对象编程(OOP)的三大基本特征之一,另外两个是封装和多态。它允许我们基于一个已有的类来创建新类,新类自动获得已有类的字段、属性和方法,同时可以扩展自己独有的成员。
用一句话概括:继承实现了"是一个"(IS-A)的关系。
比如:
- 狗是一种哺乳动物
- 哺乳动物是一种动物
- 所以狗也是一种动物
这种层次关系带来了两个直接好处:
- 代码复用:子类不需要重复编写父类已经实现的逻辑
- 统一接口:可以用父类类型引用所有子类对象,实现多态
注意:继承不是为了"少写几行代码"而存在的。只有当类之间确实存在"是一种"的层次关系时,才应该使用继承。如果只是想复用功能,组合往往优于继承。
二、基本语法:基类与派生类
在 C# 中,被继承的类叫做基类(Base Class,也叫父类),继承出来的类叫做派生类(Derived Class,也叫子类)。
2.1 语法格式
// 基类publicclassAnimal{publicstringName{get;set;}publicvoidEat(){Console.WriteLine($"{Name}在吃东西");}}// 派生类,用冒号 : 表示继承publicclassDog:Animal{publicvoidBark(){Console.WriteLine($"{Name}在汪汪叫");}}Dog继承了Animal,所以Dog的实例可以直接调用Eat()方法和Name属性,同时还拥有自己的Bark()方法。
2.2 完整示例
usingSystem;namespaceInheritanceDemo{classAnimal{protectedstringname;publicvoidSetName(stringname){this.name=name;}publicvoidEat(){Console.WriteLine($"{name}正在进食");}}classDog:Animal{publicvoidBark(){Console.WriteLine($"{name}汪汪叫");}}classProgram{staticvoidMain(string[]args){Dogdog=newDog();dog.SetName("旺财");dog.Eat();// 调用从 Animal 继承的方法dog.Bark();// 调用 Dog 自己的方法}}}运行结果:
旺财 正在进食 旺财 汪汪叫2.3 继承层次示意图
上图展示了一个典型的三级继承结构:Animal→Mammal→Dog / Cat。箭头方向从子类指向父类,表示"继承自"。
2.4 关键规则
- C# 中一个类只能直接继承一个基类(单继承),不支持类的多重继承
- 一个基类可以被多个派生类继承
- 继承具有传递性:如果
Dog : Animal,Animal : Object,那么Dog也间接继承了Object - 派生类可以继承基类的所有成员,但构造函数和析构函数不能被继承
- 默认情况下,所有类都隐式继承自
System.Object
三、访问修饰符在继承中的表现
访问修饰符决定了类成员的可见范围,在继承场景下尤其重要。
| 修饰符 | 类内部 | 派生类 | 外部代码 |
|---|---|---|---|
public | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌ |
private | ✅ | ❌ | ❌ |
internal | ✅ | ✅(同一程序集) | ✅(同一程序集) |
protected internal | ✅ | ✅ | ✅(同一程序集) |
3.1 protected:继承专用
protected是继承中最常用的修饰符。它允许派生类访问基类成员,但对外部代码隐藏。
publicclassAnimal{protectedintage;// 派生类可以访问,外部不能}publicclassDog:Animal{publicvoidShowAge(){Console.WriteLine($"年龄:{age}");// ✅ 可以访问}}// 外部代码Dogdog=newDog();// dog.age = 3; // ❌ 编译错误,不可访问3.2 private:即使是子类也不能直接访问
publicclassAnimal{privatestringid;// 只有 Animal 自己能访问}publicclassDog:Animal{publicvoidShowId(){// Console.WriteLine(id); // ❌ 编译错误}}经验:字段一般声明为
private,通过protected的属性或方法暴露给子类。这既保持了封装性,又给了子类必要的访问能力。
四、base 关键字:与父类对话
base关键字用于在派生类中访问基类的成员,主要有两个用途:
- 调用基类的构造函数
- 调用基类中被重写或隐藏的方法
4.1 调用基类构造函数
派生类不会继承基类的构造函数,但可以通过base()显式调用基类构造函数。
publicclassAnimal{protectedstringname;publicAnimal(stringname){this.name=name;Console.WriteLine("Animal 构造函数执行");}}publicclassDog:Animal{publicDog(stringname):base(name)// 调用基类构造函数{Console.WriteLine("Dog 构造函数执行");}}4.2 构造函数的执行顺序
先执行基类构造函数,再执行派生类构造函数。这是因为派生类依赖基类的初始化状态。
Dogdog=newDog("旺财");输出:
Animal 构造函数执行 Dog 构造函数执行如果基类没有无参构造函数,派生类必须用
base()显式调用基类的有参构造函数,否则编译报错。
4.3 调用基类方法
当派生类重写了基类方法后,如果还想调用基类版本,可以用base.方法名()。
publicclassAnimal{publicvirtualvoidMakeSound(){Console.WriteLine("动物发出声音");}}publicclassDog:Animal{publicoverridevoidMakeSound(){base.MakeSound();// 先调用基类版本Console.WriteLine("汪汪汪");}}五、方法的隐藏与重写:new vs override
这是 C# 继承中最容易混淆的知识点,也是面试高频考点。
5.1 override:重写(多态的基础)
基类方法用virtual标记,派生类用override重写。重写后,通过父类引用调用时,执行的是子类版本(多态)。
publicclassAnimal{publicvirtualvoidMakeSound(){Console.WriteLine("动物叫");}}publicclassCat:Animal{publicoverridevoidMakeSound(){Console.WriteLine("喵喵喵");}}// 多态调用Animalanimal=newCat();animal.MakeSound();// 输出:喵喵喵(执行子类版本)5.2 new:隐藏(不是多态)
用new关键字隐藏基类方法,不会改变多态行为。通过父类引用调用时,仍然执行基类版本。
publicclassAnimal{publicvoidMakeSound(){Console.WriteLine("动物叫");}}publicclassCat:Animal{publicnewvoidMakeSound()// 隐藏,不是重写{Console.WriteLine("喵喵喵");}}Animalanimal=newCat();animal.MakeSound();// 输出:动物叫(执行父类版本!)Catcat=newCat();cat.MakeSound();// 输出:喵喵喵(执行子类版本)5.3 对比总结
| 特性 | override重写 | new隐藏 |
|---|---|---|
| 基类要求 | 方法必须标记virtual/abstract/override | 无要求 |
| 多态行为 | ✅ 支持,父类引用调用子类方法 | ❌ 不支持 |
| 本质 | 替换方法实现 | 定义一个同名新方法 |
| 使用场景 | 真正需要多态时 | 迫不得已需要同名方法时 |
建议:绝大多数情况下用
override。new隐藏方法容易造成调用行为不一致,应谨慎使用。
六、sealed:密封类与密封方法
6.1 密封类
用sealed修饰的类不能被继承。
publicsealedclassUtility{publicstaticvoidDoSomething(){}}// public class MyUtil : Utility { } // ❌ 编译错误,无法继承密封类常见的密封类:System.String、System.DateTime等。
什么时候用密封类?
- 类包含安全敏感代码,不希望被扩展
- 类是工具类,没有继承的必要
- 性能优化(密封类不需要虚方法表查找)
6.2 密封方法
密封方法必须配合override使用,表示重写后不再允许子类继续重写。
publicclassAnimal{publicvirtualvoidEat(){}}publicclassMammal:Animal{publicoverridevoidEat(){}}publicclassDog:Mammal{publicsealedoverridevoidEat(){}// 密封,后续子类不能再重写}publicclassHusky:Dog{// public override void Eat() { } // ❌ 编译错误}七、抽象类与抽象方法
7.1 基本概念
抽象类用abstract修饰,不能被实例化,只能被继承。抽象类可以包含抽象方法(没有实现的方法),也可以包含普通方法。
publicabstractclassShape{// 抽象方法:没有方法体,派生类必须实现publicabstractdoubleGetArea();// 普通方法:有实现,可以直接复用publicvoidPrintInfo(){Console.WriteLine($"面积:{GetArea()}");}}publicclassCircle:Shape{privatedoubleradius;publicCircle(doubleradius){this.radius=radius;}// 必须实现抽象方法publicoverridedoubleGetArea(){returnMath.PI*radius*radius;}}7.2 抽象类的规则
- 抽象类不能用
sealed修饰(矛盾) - 抽象方法只能存在于抽象类中
- 派生类必须实现所有抽象方法,除非派生类也是抽象类
- 抽象方法隐式为
virtual,不能用static、virtual、override修饰 - 抽象类可以有构造函数(供派生类调用)
八、接口与多重继承
8.1 C# 不支持类的多重继承
C# 明确规定一个类只能有一个直接基类。这是为了避免"菱形继承"带来的歧义(比如两个父类有同名方法时,子类不知道该用哪个)。
但是,C# 允许一个类实现多个接口,从而达到类似多重继承的效果。
8.2 接口实现多重继承
上图中心的类同时实现了周围多个接口,虚线箭头表示"实现"关系。
// 接口1:可绘制publicinterfaceIDrawable{voidDraw();}// 接口2:可保存publicinterfaceISavable{voidSave();}// 接口3:可打印publicinterfaceIPrintable{voidPrint();}// 一个类同时实现三个接口publicclassDocument:IDrawable,ISavable,IPrintable{publicvoidDraw(){Console.WriteLine("绘制文档");}publicvoidSave(){Console.WriteLine("保存文档");}publicvoidPrint(){Console.WriteLine("打印文档");}}8.3 接口也可以继承接口
publicinterfaceIEditable{voidEdit();}// 接口继承接口,可以继承多个publicinterfaceIAdvancedEditable:IEditable,IDrawable{voidAdvancedEdit();}8.4 显式接口实现
当多个接口有同名方法时,用显式接口实现来区分:
publicinterfaceIA{voidExecute();}publicinterfaceIB{voidExecute();}publicclassMyClass:IA,IB{// 显式实现 IA 的 ExecutevoidIA.Execute(){Console.WriteLine("IA 的 Execute");}// 显式实现 IB 的 ExecutevoidIB.Execute(){Console.WriteLine("IB 的 Execute");}}// 调用方式MyClassobj=newMyClass();((IA)obj).Execute();// 输出:IA 的 Execute((IB)obj).Execute();// 输出:IB 的 Execute8.5 接口 vs 抽象类
| 对比项 | 接口(interface) | 抽象类(abstract class) |
|---|---|---|
| 实例化 | 不能 | 不能 |
| 成员实现 | C# 8.0 之前只能有抽象成员;之后可以有默认实现 | 可以有抽象成员和普通成员 |
| 字段 | 不能有字段 | 可以有字段 |
| 构造函数 | 没有 | 可以有 |
| 多重继承 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类 |
| 访问修饰符 | 默认 public,不能用其他 | 可以用各种修饰符 |
| 用途 | 定义能力契约(能做什么) | 定义基类模板(是什么) |
选择原则:描述"是什么"用抽象类,描述"能做什么"用接口。
九、里氏替换原则(LSP)
里氏替换原则(Liskov Substitution Principle)是面向对象设计的五大原则(SOLID)之一,由 Barbara Liskov 提出。
核心定义:子类必须能够替换掉它的父类,并且程序的行为不发生异常或改变。
换句话说,任何使用基类的地方,都应该能透明地使用派生类,而不需要知道派生类的具体类型。
9.1 违反 LSP 的例子
publicclassRectangle{publicvirtualintWidth{get;set;}publicvirtualintHeight{get;set;}publicintGetArea(){returnWidth*Height;}}publicclassSquare:Rectangle{publicoverrideintWidth{set{base.Width=base.Height=value;}}publicoverrideintHeight{set{base.Width=base.Height=value;}}}// 测试方法publicvoidTest(Rectanglerect){rect.Width=5;rect.Height=10;Console.WriteLine(rect.GetArea());// 期望 50}Test(newRectangle());// 输出 50 ✅Test(newSquare());// 输出 100 ❌ 行为改变了!正方形"是一种"矩形在数学上成立,但在继承关系中违反了 LSP,因为设置宽高的行为不一致。
9.2 如何遵守 LSP
- 子类不能改变父类方法的预期行为
- 子类不能加强前置条件(比如要求更严格的参数)
- 子类不能减弱后置条件(比如返回值不符合父类约定)
- 子类不能抛出父类没有声明的异常
- 当继承关系不满足 LSP 时,考虑用组合代替继承
十、万物之根:object 类
在 C# 中,所有类型都直接或间接继承自System.Object。即使你没有写: object,编译器也会自动加上。
// 这两行完全等价publicclassMyClass{}publicclassMyClass:object{}10.1 object 的常用方法
| 方法 | 作用 |
|---|---|
ToString() | 返回对象的字符串表示,默认返回类型全名 |
Equals(object) | 判断两个对象是否相等,默认比较引用 |
GetHashCode() | 获取对象的哈希码 |
GetType() | 获取对象的运行时类型 |
MemberwiseClone() | 浅拷贝对象(protected) |
Finalize() | 析构函数(C# 中用~类名()) |
10.2 重写 ToString
publicclassPerson{publicstringName{get;set;}publicintAge{get;set;}publicoverridestringToString(){return$"姓名:{Name},年龄:{Age}";}}Personp=newPerson{Name="张三",Age=25};Console.WriteLine(p);// 自动调用 ToString,输出:姓名:张三,年龄:25十一、高频面试题精选
Q1:C# 支持多重继承吗?
不支持类的多重继承,但支持接口的多重实现。一个类可以继承一个基类,同时实现任意多个接口。
Q2:new和override有什么区别?
override重写基类的virtual方法,支持多态,父类引用调用子类方法new隐藏基类方法,不支持多态,父类引用仍然调用父类方法
Q3:抽象类和接口有什么区别?
见上文 接口 vs 抽象类对比表。核心区别:抽象类是"是什么",接口是"能做什么";抽象类只能单继承,接口可以多实现。
Q4:构造函数可以被继承吗?可以是虚函数吗?
构造函数不能被继承,也不能是虚函数。派生类通过base()调用基类构造函数。构造函数的执行顺序是从基类到派生类。
Q5:sealed关键字有什么用?
- 修饰类:该类不能被继承
- 修饰方法(必须配合 override):该方法不能再被后续子类重写
Q6:什么是里氏替换原则?
子类必须能够替换父类而不改变程序的正确性。违反 LSP 的继承关系应该用组合替代。
Q7:base和this有什么区别?
base:访问基类成员(构造函数、方法、属性)this:访问当前类的成员(构造函数、方法、属性、字段)
Q8:所有类都继承自哪个类?
System.Object。这也是为什么所有对象都有ToString()、Equals()等方法。
十二、最佳实践与避坑指南
12.1 什么时候该用继承
- 类之间确实存在"是一种"(IS-A)的关系
- 需要多态行为
- 多个子类有共同的基类逻辑需要复用
12.2 什么时候不该用继承
- 只是为了复用代码,而没有层次关系 → 用组合
- 子类不能完全替代父类(违反 LSP)→ 用组合
- 继承层次超过 3-4 层 → 考虑重构
12.3 常见坑
坑1:在构造函数中调用虚方法
publicclassBase{publicBase(){Init();// ❌ 危险!此时子类字段还未初始化}publicvirtualvoidInit(){}}因为构造函数执行顺序是先基类后子类,基类构造函数中调用虚方法时,子类的字段还没初始化,可能导致NullReferenceException。
坑2:忘记调用 base() 导致编译错误
如果基类没有无参构造函数,派生类必须显式调用base(参数)。
坑3:用 new 隐藏方法导致行为不一致
团队协作中,new隐藏的方法容易让调用者产生误解。尽量用override,或者直接换个方法名。
坑4:过度继承
继承层次过深会导致代码难以理解和维护。记住:组合优于继承(Favor Composition over Inheritance)。
总结
本文从基础到进阶,全面梳理了 C# 继承的核心知识:
- 继承的本质是 IS-A 关系,实现代码复用和多态
- 基本语法用冒号
:,C# 只支持单继承 - 访问修饰符中
protected是继承专用 base关键字用于调用基类构造函数和方法override实现多态,new只是隐藏,二者有本质区别sealed可以密封类或方法,阻止进一步继承或重写- 抽象类定义模板,接口定义能力契约,接口可多实现
- 里氏替换原则是判断继承是否合理的试金石
- 所有类都继承自
object - 组合优于继承,不要为了复用而滥用继承
掌握这些知识点,不仅能写出更健壮的面向对象代码,也能从容应对相关的技术面试。建议结合实际项目多加练习,尤其是多态和接口的运用。
如果本文对你有帮助,欢迎点赞、收藏、关注!有任何疑问欢迎在评论区交流。
标签:C#面向对象继承多态接口抽象类.NET