我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

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

C# 继承完全指南:从基类派生到接口多重继承,一篇搞懂 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.namename;}publicvoidEat(){Console.WriteLine(${name}正在进食);}}classDog:Animal{publicvoidBark(){Console.WriteLine(${name}汪汪叫);}}classProgram{staticvoidMain(string[]args){DogdognewDog();dog.SetName(旺财);dog.Eat();// 调用从 Animal 继承的方法dog.Bark();// 调用 Dog 自己的方法}}}运行结果旺财 正在进食 旺财 汪汪叫2.3 继承层次示意图上图展示了一个典型的三级继承结构Animal→Mammal→Dog / Cat。箭头方向从子类指向父类表示继承自。2.4 关键规则C# 中一个类只能直接继承一个基类单继承不支持类的多重继承一个基类可以被多个派生类继承继承具有传递性如果Dog : AnimalAnimal : Object那么Dog也间接继承了Object派生类可以继承基类的所有成员但构造函数和析构函数不能被继承默认情况下所有类都隐式继承自System.Object三、访问修饰符在继承中的表现访问修饰符决定了类成员的可见范围在继承场景下尤其重要。修饰符类内部派生类外部代码public✅✅✅protected✅✅❌private✅❌❌internal✅✅同一程序集✅同一程序集protected internal✅✅✅同一程序集3.1 protected继承专用protected是继承中最常用的修饰符。它允许派生类访问基类成员但对外部代码隐藏。publicclassAnimal{protectedintage;// 派生类可以访问外部不能}publicclassDog:Animal{publicvoidShowAge(){Console.WriteLine($年龄{age});// ✅ 可以访问}}// 外部代码DogdognewDog();// 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.namename;Console.WriteLine(Animal 构造函数执行);}}publicclassDog:Animal{publicDog(stringname):base(name)// 调用基类构造函数{Console.WriteLine(Dog 构造函数执行);}}4.2 构造函数的执行顺序先执行基类构造函数再执行派生类构造函数。这是因为派生类依赖基类的初始化状态。DogdognewDog(旺财);输出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(喵喵喵);}}// 多态调用AnimalanimalnewCat();animal.MakeSound();// 输出喵喵喵执行子类版本5.2 new隐藏不是多态用new关键字隐藏基类方法不会改变多态行为。通过父类引用调用时仍然执行基类版本。publicclassAnimal{publicvoidMakeSound(){Console.WriteLine(动物叫);}}publicclassCat:Animal{publicnewvoidMakeSound()// 隐藏不是重写{Console.WriteLine(喵喵喵);}}AnimalanimalnewCat();animal.MakeSound();// 输出动物叫执行父类版本CatcatnewCat();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.radiusradius;}// 必须实现抽象方法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);}}// 调用方式MyClassobjnewMyClass();((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.Widthbase.Heightvalue;}}publicoverrideintHeight{set{base.Widthbase.Heightvalue;}}}// 测试方法publicvoidTest(Rectanglerect){rect.Width5;rect.Height10;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()浅拷贝对象protectedFinalize()析构函数C# 中用~类名()10.2 重写 ToStringpublicclassPerson{publicstringName{get;set;}publicintAge{get;set;}publicoverridestringToString(){return$姓名{Name}年龄{Age};}}PersonpnewPerson{Name张三,Age25};Console.WriteLine(p);// 自动调用 ToString输出姓名张三年龄25十一、高频面试题精选Q1C# 支持多重继承吗不支持类的多重继承但支持接口的多重实现。一个类可以继承一个基类同时实现任意多个接口。Q2new和override有什么区别override重写基类的virtual方法支持多态父类引用调用子类方法new隐藏基类方法不支持多态父类引用仍然调用父类方法Q3抽象类和接口有什么区别见上文 接口 vs 抽象类对比表。核心区别抽象类是是什么接口是能做什么抽象类只能单继承接口可以多实现。Q4构造函数可以被继承吗可以是虚函数吗构造函数不能被继承也不能是虚函数。派生类通过base()调用基类构造函数。构造函数的执行顺序是从基类到派生类。Q5sealed关键字有什么用修饰类该类不能被继承修饰方法必须配合 override该方法不能再被后续子类重写Q6什么是里氏替换原则子类必须能够替换父类而不改变程序的正确性。违反 LSP 的继承关系应该用组合替代。Q7base和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
返回列表