☰
C# 继承完全指南:从基类派生到接口多重继承,一篇搞懂
2026/10/11 4:55:37 网站建设 项目流程

C# 继承完全指南:从基类派生到接口多重继承,一篇搞懂

本文系统梳理 C# 中继承的核心概念,涵盖基类与派生类、访问修饰符、base关键字、方法重写与隐藏、sealed密封类、抽象类、接口多重继承、里氏替换原则以及常见面试题,适合有一定 C# 基础的开发者深入学习。


目录

  • 一、继承是什么:为什么需要它
  • 二、基本语法:基类与派生类
  • 三、访问修饰符在继承中的表现
  • 四、base 关键字:与父类对话
  • 五、方法的隐藏与重写:new vs override
  • 六、sealed:密封类与密封方法
  • 七、抽象类与抽象方法
  • 八、接口与多重继承
  • 九、里氏替换原则(LSP)
  • 十、万物之根:object 类
  • 十一、高频面试题精选
  • 十二、最佳实践与避坑指南
  • 总结

一、继承是什么:为什么需要它

继承是面向对象编程(OOP)的三大基本特征之一,另外两个是封装和多态。它允许我们基于一个已有的类来创建新类,新类自动获得已有类的字段、属性和方法,同时可以扩展自己独有的成员。

用一句话概括:继承实现了"是一个"(IS-A)的关系。

比如:

  • 狗是一种哺乳动物
  • 哺乳动物是一种动物
  • 所以狗也是一种动物

这种层次关系带来了两个直接好处:

  1. 代码复用:子类不需要重复编写父类已经实现的逻辑
  2. 统一接口:可以用父类类型引用所有子类对象,实现多态

注意:继承不是为了"少写几行代码"而存在的。只有当类之间确实存在"是一种"的层次关系时,才应该使用继承。如果只是想复用功能,组合往往优于继承。


二、基本语法:基类与派生类

在 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关键字用于在派生类中访问基类的成员,主要有两个用途:

  1. 调用基类的构造函数
  2. 调用基类中被重写或隐藏的方法

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 的 Execute

8.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# 继承的核心知识:

  1. 继承的本质是 IS-A 关系,实现代码复用和多态
  2. 基本语法用冒号:,C# 只支持单继承
  3. 访问修饰符中protected是继承专用
  4. base关键字用于调用基类构造函数和方法
  5. override实现多态,new只是隐藏,二者有本质区别
  6. sealed可以密封类或方法,阻止进一步继承或重写
  7. 抽象类定义模板,接口定义能力契约,接口可多实现
  8. 里氏替换原则是判断继承是否合理的试金石
  9. 所有类都继承自object
  10. 组合优于继承,不要为了复用而滥用继承

掌握这些知识点,不仅能写出更健壮的面向对象代码,也能从容应对相关的技术面试。建议结合实际项目多加练习,尤其是多态和接口的运用。


如果本文对你有帮助,欢迎点赞、收藏、关注!有任何疑问欢迎在评论区交流。

标签:C#面向对象继承多态接口抽象类.NET

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询