类的继承与访问控制
base
关键字
base
关键字的用法,有些类似Java中的super
关键字
-
调用基类的构造函数:当创建派生类的实例时,可以使用
base
关键字来调用基类的构造函数。这个用法很重要下面有详细解释,初学到这里,弄懂这个用法,就行了,后面的用法,会涉及其他较深的知识
-
访问基类的方法和属性:
如果派生类重写了基类的方法或属性,可以使用
base
关键字来访问被重写的基类版本。如果在派生类中重写了基类的属性,可以在属性的
get
或set
访问器中使用base
关键字来访问基类的属性。class BaseClass { //虚属性 public virtual string Name { get; set; } /* virtual关键字修饰的方法,叫做虚方法 * 子类可以(不是强制的)重写父类的虚方法 */ public virtual void Greet() { Console.WriteLine("Hello from 父类"); } } class DerivedClass : BaseClass { private string _nickname; //子类重写父类的属性中,使用base关键字访问父类的属性 public override string Name { get => $"{base.Name} ,also known as {_nickname}"; set => base.Name = value; } public override void Greet() { //调用基类的方法 base.Greet(); Console.WriteLine("Hello from 子类"); } }
-
访问基类中和派生类同名的字段:当派生类中有与基类同名的字段时,可以使用
base
关键字来指定访问基类的字段。这应该是一个比较冷门的用法
using System; namespace BaseExample { class BaseClass { protected int number; public BaseClass(int num) { this.number = num; } } class DerivedClass : BaseClass { private int number; public DerivedClass(int num) : base(num) { this .number = num * 2; } public void PrintNumber() { /* 使用base关键字访问基类的number字段 * 只有当子类的字段和父类的字段同名的时候, * 才需要使用base关键字来访问继承自父类的同名字段 * 当不同名的时候当然,因为都继承了父类的字段了(并且该字段,子类可见的情况下), * 当然也就不需要这样子访问了,可以直接使用字段名 */ Console.WriteLine($"Base number: {base.number}" );// 10 // 直接访问派生类的number字段 Console.WriteLine($"Derived number: {number}");// 20 } } internal class Program { static void Main(string[] args) { DerivedClass decl = new DerivedClass(10); decl.PrintNumber(); } } }
总结一下:
base
关键字除了,子类在显式调用父类的构造器要使用到之外,其他情况都是,子类不能直接调用父类的某些东西的时候(也就是子类的某些东西和父类有重合的时候),才能用到。
使用base关键字调用基类的构造器
派生类在创建自己的实例构造器的时候,必须调用(显式或隐式)基类的构造器。
因为派生类需要基类的构造过程来正确初始化其基类部分。
如果派生类没有显式地调用基类的构造函数,编译器将隐式地调用基类的无参构造函数。
有以下情况:
-
当基类没有自己编写的构造器时,派生类只能调用(显式或隐式)基类的默认构造器
class DerivedClass : BaseClass { public DerivedClass() : base()//显式调用基类的无参构造器 { } } //这两段代码是等效的 class DerivedClass : BaseClass { public DerivedClass()//隐式调用基类的无参构造器 { } }
-
当基类有编写构造器时,无论派生类哪个构造器没有指定调用基类的构造器,都会去隐式调用基类的无参构造函数。如果基类没有无参构造器,就会报错。
-
基类有编写构造器时,则派生类可以指定调用基类的哪个构造器,使用
base
关键字class BaseClass { public BaseClass(int num) { Console.WriteLine("基类的有参构造函数"); } public BaseClass() { Console.WriteLine("基类的无参构造函数"); } } class DerivedClass : BaseClass { public DerivedClass():base()//显式地调用基类的无参构造函数 {//也可以选择去调用,有参构造函数,主要看自己的选择 } }
-
因为构造函数可以重载,没有显示写出基类的默认构造函数之前,重载基类的构造函数,那么系统不会自动创建基类的默认的构造函数。
所以我们在创建基类的有参数构造函数的时候,要同时再显示创建一个无参数的和默认形式相同的构造函数。
这样就避免在创建派生类的实例构造器的时候,产生的部分麻烦。
构造器执行顺序
派生类在创建对象的过程当中,会隐式调用基类的某一个构造函数作为创建实例过程的一部分,在继承层次链中的每个类在创建对象过程中,执行自己的构造函数体之前都要执行它的基类构造函数。
也就是说:实例构造函数的执行顺序是自上而下的
静态构造函数在继承中,是自下而上执行的。
注:静态构造器,在类加载的时候就被调用了
using System;
namespace BaseExample {
class BaseClass {
public BaseClass(int num)
{
Console.WriteLine("基类的有参构造函数");
}
public BaseClass()
{
Console.WriteLine("基类的无参构造函数");
}
static BaseClass() {
// 注:静态构造器不允许使用访问修饰符
Console.WriteLine("基类的静态构造器");
}
}
class DerivedClass : BaseClass {
public DerivedClass():base()
{
Console.WriteLine("派生类的无参构造函数");
}
static DerivedClass() {
Console.WriteLine("派生类的静态构造器");
}
}
internal class Program {
static void Main(string[] args) {
DerivedClass derivedClass = new DerivedClass();
/* 输出顺序:
* 派生类的静态构造器
* 基类的静态构造器
* 基类的无参构造函数
* 派生类的无参构造函数
*/
}
}
}
注意:
在构造函数中要避免调用虚函数方法。
当基类的构造函数调用一个虚方法时,如果这个方法在派生类中被重写,那么即使派生类的实例还没有完全构造完成,也会调用派生类中的重写方法。这可能会导致在派生类的构造函数中初始化的字段还没有设置好就被使用,从而引发错误或异常。
因为执行基类构造函数时,在执行派生类的构造函数方法体,基类的虚方法会调用派生类的覆写方法,因此调用会在派生类没有完全初始化之前传递到派生类。
类的继承
- 使用
sealed
修饰的类,是私有类,不能作为基类使用 - C#中一个类,只能有一个父类,但是可以实现多个接口
- 子类的访问级别不能超过父类的访问级别
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HelloOOP {
internal class Program {
static void Main(string[] args) {
//从代码上验证继承关系
Type t = typeof(Car);
Type tb = t.BaseType;
Type ttTop = tb.BaseType;
Console.WriteLine(ttTop.FullName);
/* 所有的类都继承自Object
* 并且Object上面就没有父类了,Object是处于最顶端的
*/
Console.WriteLine(ttTop.BaseType == null);
/* 是一个 is a, 一个子类的实例也是父类的实例
* 一个学生一定是一个人
* 一个父类的实例不一定是子类的实例
* 一个人不一定是一个学生
*/
Car car = new Car();
Console.WriteLine(car is Vehicle);
}
}
class Vehicle { }
//Car类继承自Vehicle类
class Car : Vehicle { }
// 1.私有类,不能当做基类来使用
sealed class Bycicle { }
}
成员的继承与访问
继承
继承的本质:派生类在基类已有的成员的基础之上,对基类进行的横向和纵向上的扩展
-
当继承发生的时候,子类对父类所有的成员是全盘继承的,除了三种东西不会被继承
- 静态构造器:用于初始化类的静态数据。
- 实例构造器:在创建类的新实例时调用。 每个类都必须定义自己的构造函数。
- 析构函数(终结器):由运行时的垃圾回收器调用,用于销毁类实例。
-
在派生的过程当中,我们进行的是扩展
也就是不能对父类的成员进行去除,只能扩展,这是静态类型语言(c++、c#、Java)的特点
而某些动态语言(python、JavaScript)则可以去除父类的成员
-
横向扩展即,类成员的增加;纵向扩展即,类成员的重写
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HellloAccess {
internal class Program {
static void Main(string[] args) {
Car car = new Car();
Console.WriteLine(car.Owner);
}
}
class Vehicle {
public Vehicle()
{
this.Owner = "N/A";
}
public string Owner { get; set; }
}
class Car : Vehicle {
public Car() {
//this.Owner = "Car Owner";
}
public void ShowOwner() {
//只能访问上一级的对象
Console.WriteLine(base.Owner);
/* 创建子类对象的时候,是从基类的构造器开始
* 一层一层往下构造的,最终构造出来子类的对象
*/
Console.WriteLine(this.Owner);
/* 当前这个base.Owner和this.Owner在这个例子中
* 都指向的是同一个字符串即"Car Owner"
* 因为子类中的构造器把原来的Owner覆盖了
*/
}
}
}
实例构造器无法被继承
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HellloAccess {
internal class Program {
static void Main(string[] args) {
Car car = new Car("Ygc");
Console.WriteLine(car.Owner);
/* 实例构造器无法被继承
* 但是会先调用父类的实例构造器,然后再调用子类的实例构造器
*/
}
}
class Vehicle {
public Vehicle(string owner)
{
this.Owner = owner;
}
public string Owner { get; set; }
}
class Car : Vehicle {
/* 当父类的构造器,是带参数的。
* 那么父类的默认的构造器就会被覆盖掉
* 那么此时调用子类的默认构造器是不行的
* 因为调用子类的默认构造器,会先调用父类的默认构造器
* 而此时,父类的默认构造器是不存在的,会发生冲突
*/
//更改的办法:
public Car() : base("N/A") {
//this.Owner = "Car Owner";
}
//或者写一个带参数的构造器
public Car(string owner) : base(owner)
{
}
public void ShowOwner() {
Console.WriteLine(this.Owner);
}
}
}
继承中的访问级别
派生类和派生记录不能具有高于其基类型的可访问性。
虽然基类的其他所有成员都可供派生类继承,但这些成员是否可见取决于它们的可访问性。 成员的可访问性决定了其是否在派生类中可见。
类成员的访问级别是以类的访问级别作为上限的。
internal class Student{
public string name;
//所以这个类成员是不能被其他程序集所访问的
}
父类中的私有字段被限制在了父类的类体中,子类是不能直接访问的,但是是继承了的
以下代码可以证明:
namespace MyLib
{
public class Vehicle
{
private int _rpm;
//命名前面加_表示,这是一个私有字段
public void Accelerate() {
_rpm += 1000;
}
//该属性返回的是私有字段/ 100之后的值
public int Speed { get { return _rpm / 100; } }
}
public class Car : Vehicle{ }
}
using System;
using MyLib;
namespace HellloAccess {
internal class Program {
static void Main(string[] args) {
Car car = new Car();
car.Accelerate();
car.Accelerate();
Console.WriteLine(car.Speed);
/* 使用car对象调用父类的属性,能够成功返回
* 说明,父类的私有字段实际上也是被子类所继承的,
* 只是不能被car类所访问
*/
}
}
}
类成员的访问级别:
-
只有在基类中嵌套的派生类中,私有成员才可见。 否则,此类成员在派生类中不可见。
private
会把访问级别限制在类的类体中,所以只有嵌套的派生类才能访问public class A { private int _value = 10; public class B : A { public int GetValue() { return _value; } } } public class C : A { // public int GetValue() // { // return _value; // } } public class AccessExample { public static void Main(string[] args) { var b = new A.B(); Console.WriteLine(b.GetValue()); } } // The example displays the following output: // 10
-
受保护成员仅在派生类中可见。
会把访问级别限制在继承链上,并且可以夸程序集
protected
更多的用在方法上namespace MyLib { public class Vehicle { protected int _rpm; private int _fuel;//表示油量 //加油的方法 public void Refuel() { _fuel = 100; } /* 烧油方法 * 即不想暴露给外界,引发错误的调用 * 又想继承给子类继续调用 * 于是就用,protected访问修饰符 */ protected void Burn(int fuel) { _fuel -= fuel; } public void Accelerate() { Burn(1);//普通加速耗油1 _rpm += 1000; } public int Speed { get { return _rpm / 100; } } } public class Car : Vehicle{ public void TurboAccelerate() { //烧两次油,加速3000 Burn(2);//涡轮增压加速,耗油2 _rpm += 3000; } } } using System; using MyLib; namespace HellloAccess { internal class Program { static void Main(string[] args) { Car car = new Car(); car.Refuel();//先加油 car.TurboAccelerate();//再加速 Console.WriteLine(car.Speed); Bus bus = new Bus(); bus.Refuel(); bus.SlowAccelerate(); Console.WriteLine(bus.Speed); Console.ReadKey(); } } class Bus: Vehicle { public void SlowAccelerate() { //夸程序集,仍然可以调用父类的protected的成员 Burn(1); _rpm += 500; } } }
-
内部成员仅在与基类同属一个程序集的派生类中可见, 在与基类属于不同程序集的派生类中不可见。
-
公共成员在派生类中可见,并且属于派生类的公共接口。 可以调用继承的公共成员,就像它们是在派生类中定义一样。
public class A { public void Method1() { // Method implementation. } } public class B : A { } public class Example { public static void Main() { B b = new (); b.Method1(); } }
访问修饰符(全)
- public:同一程序集中的任何其他代码或引用该程序集的其他程序集都可以访问该类型或成员。 某一类型的公共成员的可访问性水平由该类型本身的可访问性级别控制。
-
private:只有同一
class
或struct
中的代码可以访问该类型或成员。 -
protected:只有同一
class
或者从该class
派生的class
中的代码可以访问该类型或成员。 -
internal:同一程序集中的任何代码都可以访问该类型或成员,但其他程序集中的代码不可以。 换句话说,
internal
类型或成员可以从属于同一编译的代码中访问。 -
protected internal:该类型或成员可由对其进行声明的程序集或另一程序集中的派生
class
中的任何代码访问。 -
private protected:该类型或成员可以通过从
class
派生的类型访问,这些类型在其包含程序集中进行声明。
各种东西的默认访问级别
默认情况下,类成员和结构成员(包括嵌套的类和结构)的访问级别为 private
。 不能从包含该类型的外部访问私有嵌套类型。
面对对象的实现风格
面向对象语言学习方法的问题
不要因为学了某个语言,就极大的夸赞该语言的优点,而藐视其他语言的缺点。
C#中的派生和继承,仅仅只是众多语言派生和继承的一种风格,还有其他的派生和继承的风格。
**C#使用的是Class-based
风格,基于类的封装、继承和多态。**目前是主流,C++,C#,Java。
JavaScript使用的是Prototype-based
风格,基于原型的封装、继承和多态的方式
Java不是单根的类型系统,它的引用类型是单根的,但是它还有一套系统,叫基本类型系统。
有部分内容是从微软的官方文档上面扒下来的。
base
关键字那里,借阅了以下两篇文章:
C#中base关键字的几种用法:base()_c# :base()-CSDN博客
C# 关于C#中派生类调用基类构造函数的理解_c# 调用基类构造函数-CSDN博客