文章目录
-
- 6. 面向对象——继承
-
- 6.1 继承概念
- 6.2 子类当中的构造函数
- 6.3 万物之父Object
-
- 什么是万物之父Object?
- object有什么用处?
- 如何使用?
- 什么是装箱拆箱?
- 装箱拆箱有什么弊端?
- Object中的静态方法`bool Equals(Object? objA, Object? objB)`
- Object中的静态方法`bool ReferenceEquals(Object? objA, Object? objB)`
- Object中的成员方法`public Type GetType()`
- Object中的成员方法`protected Object MemberwiseClone()`
- Object中的虚方法`public virtual bool Equals(Object? obj)`
- Object中的虚方法`public virtual int GetHashCode()`
- Object中的虚方法`public virtual string? ToString()`
- 6.4 密封类sealed class
6. 面向对象——继承
6.1 继承概念
什么是继承?
继承是面向对象中三大特征之一,它允许类像父子一样存在继承关系,继承类可以继承被继承类中的所有成员及方法,其中被继承的类称为父类
、基类
、超类
,继承类称为子类
或派生类
,在C#
中只允许单类继承。
继承有什么作用?
如同后代继承现代资产和手艺等,使用继承可以提高代码复用,同时在子类中还可以对父类方法进行进一步的拓展或修改。
继承的基本语法是什么?
C#
中的继承与C++
、Java
不同,它不需要使用任何关键字,只需要使用:
并在后面写上所继承的父类,形式如下:
访问修饰符 class 子类 : 父类 {
}
父类中不同访问修饰符的作用或影响是什么?
public
可以让内外部均可以访问该属性或方法。private
只有父类内部可以进行访问,子类和外部都无法访问该属性和方法。protected
只有父类及其子类可以访问,外部无法访问。internal
只有在同一个程序集中的文件才可以进行访问。
子类和父类允许有同名成员吗?
子类和父类中可以有同一个名称的成员,子类中的成员将会覆盖掉父类中原有的成员。通常来讲不建议在子类中声明父类中同名的成员,如果不得不声明,需要在变量名或返回值前加上new
关键字。
使用示例
class Food
{
public string name = "";
public int price;
public void GetNameAndPrice()
{
Console.WriteLine(name + ":" + price);
}
}
class Apple : Food
{
public string color = "red";
//覆盖父类成员方法
public new void GetNameAndPrice()
{
//调用父类中公有成员
Console.WriteLine(color + name + ":" + price);
}
}
6.2 子类当中的构造函数
父类中含有构造函数子类中是否还可以书写构造函数?
可以,子类当中可以书写构造函数。
调用子类构造函数执行顺序是什么?
在执行时,先会寻找到父类中的对应同参构造函数,再执行子类构造函数。
为什么父类当中没有无参构造函数会导致继承报错?
因为在实例化子类对象时,会默认调用父类的无参构造函数初始化被继承的部分,没有无参构造函数将导致无法被正常初始化,所以会无法继承,但是可以通过base
调用父类中其他构造函数来解决。
如何使用没有无参构造的父类继承?
使用base
关键字调用父类中的其他构造函数,可以解决父类没有无参构造时的继承问题。
例如父类如下:
public class Animal
{
public Animal(string name)
{
Console.WriteLine("The Animal`s name is " + name);
}
}
子类直接继承会有报错提示:
将子类构造定向调用父类构造,可以解决这个问题:
public class Cat : Animal
{
public Cat(string name) : base(name)
{
Console.WriteLine("The Cat`s name is " + name);
}
}
在实例化子类时,他的调用如下:
The Animal`s name is neko
The Cat`s name is neko
6.3 万物之父Object
什么是万物之父Object?
Object
是所有类型的基类,C#中任何类型都可以向上转换为object
类,它本身是一个引用类型。
object有什么用处?
因为所有类型都是基于Object,所以在不确定参数类型时可以使用Object来进行参数接收,但是需要注意的是使用时需要先转换为对应类型才可以进行使用。
如何使用?
在创建Object变量或参数时,使用的方法与其他类型相同,但是使用时需要进行类型转换,转换的方式有两种,第一种为强制转换即在前面加上括号并写入目标类型,第二种为对引用类型使用as
关键字进行转换,如下代码:
object p1 = new Person();//用object接收类
if (p1 is Person)
(p1 as Person).Speak();//转换为对应类实例
object i = 10;//用object接收值类型
Console.WriteLine((int)i);//转换为对应值类型
object str = "12345";//用object接收接收特殊数据类型
Console.WriteLine(str.ToString());//进行类型转换
Console.WriteLine(str as string);
object arr = new int[5] {
1, 2, 3, 4, 5 };//用object接收接收数组
Console.WriteLine((int[])arr);
Console.WriteLine(arr as int[]);
什么是装箱拆箱?
装箱拆箱本质即在使用Object接收值类型数据时产生的内存迁移。
值类型本身存储于栈区,再通过object接收时会将栈区内存迁移至堆区,即为装箱
;在使用时需要转换为值类型使用,需要将堆区内存移回栈区,即为拆箱
。
装箱拆箱有什么弊端?
装箱拆箱存在内存迁移,每一次迁移CPU都会对迁移地址进行运算,大量的装拆箱将会浪费大量CPU资源。
Object中的静态方法bool Equals(Object? objA, Object? objB)
该方法用于比较两个数据的值是否相同,对于引用类型如果调用这个函数,他比较的将是这个变量所指向的内存地址是否相同而非内存值是否相同,例如如下代码:
int i1 = 1;
int i2 = 1;
Console.WriteLine("object.Equals(1,1):" + Equals(i1, i2));
object o1 = new();
object o2 = new();
Console.WriteLine("object.Equals(o1,o2):" + Equals(o1, o2));
object o3 = o1;
Console.WriteLine("object.Equals(o1,o3):" + Equals(o1, o3));
执行结果为:
object.Equals(1,1):True
object.Equals(o1,o2):False
object.Equals(o1,o3):True
Object中的静态方法bool ReferenceEquals(Object? objA, Object? objB)
ReferenceEquals
用于比较两个对象实例是否相等(是否指向同一块内存),如果两个内存相同则返回true
,不同则返回false
。他仅可以比较两个引用类型的数据,不可以用来比较值类型数据,比较值类型数据时会始终返回false
。
Console.WriteLine("ReferenceEquals(o1, o2):" + ReferenceEquals(o1, o2));//比较两个不同实例
Console.WriteLine("ReferenceEquals(o1, o3):" + ReferenceEquals(o1, o3));//比较相同实例
Console.WriteLine("ReferenceEquals(1, 1):" + ReferenceEquals(1, 1));//比较值类型
执行结果为:
ReferenceEquals(o1, o2):False
ReferenceEquals(o1, o3):True
ReferenceEquals(1, 1):False
Object中的成员方法public Type GetType()
这个方法用于获取实例的类型,它会返回一个Type类型的对象,直接进行打印会获得该实例的类所属的命名空间及类名称:
object obj = new Person(new People());
Console.WriteLine("obj.GetType().ToString():" + obj.GetType().ToString());
obj = new People();
Console.WriteLine("obj.GetType().ToString():" + obj.GetType().ToString());
运行结果:
obj.GetType().ToString():CSharp.Person
obj.GetType().ToString():CSharp.People
Object中的成员方法protected Object MemberwiseClone()
这个方法实现了对象的浅拷贝,使用它拷贝出来的对象会是一个新的对象,但是示例中的引用变量与原来的对象相同。但是这个方法可访问性为protected
,因此只能在被拷贝的类中使用,无法在外部使用,如果需要在外部使用需要用函数进行包裹。
例如有一个Person
类对象p1
,其中包含一个People
对象成员people
,对p1
使用浅拷贝得到p2
,会发现p2
中的非引用变量与p1
不同,但引用变量指向的是同一个实例(内存地址):
class Person
{
public bool sex;
public People people;
public Person(People people, bool sex)
{
this.people = people;
this.sex = sex;
}
public Person GetCopy()//用函数进行包裹,让外部可以使用浅拷贝函数
{
return (Person)MemberwiseClone();
}
}
public class People
{
public int age = 0;
}
People p = new();
Person p1 = new(p, true);
p.age = 10;
Person p2 = p1.GetCopy();
Console.WriteLine("p1.people.age:" + p1.people.age);
Console.WriteLine("p1.sex:" + p1.sex);
Console.WriteLine("p2.people.age:" + p2.people.age);
Console.WriteLine("p2.sex:" + p2.sex);
p1.people.age = 99;//改变p1的成员对象的成员变量值
Console.WriteLine("p2.people.age:" + p2.people.age);
Console.WriteLine(ReferenceEquals(p1.people, p2.people));
输出结果:
p1.people.age:10
p1.sex:True
p2.people.age:10
p2.sex:True
p2.people.age:99
True
Object中的虚方法public virtual bool Equals(Object? obj)
这个方法适用于比较与传入实例值是否相等,并且对于所有值类型变量,官方已经对这个方法进行了重写。
对于我们自己的类,默认执行会比较是否指向同一实例,而我们可以对其进行重写让其实现我们自己的判断逻辑。
默认比较是否指向同一实例:
object obj1 = new int[1, 2, 3];
object obj2 = new int[1, 2, 3];
Console.WriteLine(obj1.Equals(obj2));
False
自定义重写方法比较:
class Animal
{
public string name;
public Animal(string name)
{
this.name = name;
}
public override bool Equals(object? obj)
{
if(obj!=null&&((Animal)obj).name == this.name)
return true;
return false;
}
}
Animal a1 = new("Cat");
Animal a2 = new("Cat");
Console.WriteLine(a1.Equals(a2));
True
Object中的虚方法public virtual int GetHashCode()
该函数用于返回对象的哈希编码,即一种通过哈希算法计算而得的对象编码,如果两个对象是同样的,那么他们的哈希码一定相同,但哈希码一定相同的不一定会是同一个对象。
一般而言该函数使用频率非常低,通常不会使用该方法。
Object.GetHashCode 方法 (System) | Microsoft Learn
Object中的虚方法public virtual string? ToString()
这个方法即将对象转换为字符串形式的方法,默认情况下会打印出来对象所在命名空间即对象的类型,但是可以通过在类里面进行重写实现我们自己的功能,例如下面的直接打印数组和重写Animal
后进行打印:
class Animal
{
public string name;
public Animal(string name)
{
this.name = name;
}
public override bool Equals(object? obj)
{
if (obj != null && ((Animal)obj).name == this.name)
return true;
return false;
}
public override string ToString()
{
return "The Animal`s Name is " + name;
}
}
object obj = new int[1, 2, 3];
Console.WriteLine(obj.ToString());
Animal animal = new("Cat");
Console.WriteLine(animal);
输出结果:
System.Int32[,,]
The Animal`s Name is Cat
6.4 密封类sealed class
什么是密封类?
密封类即在class
关键字前加上sealed
关键字的类,它可以让这个类不能被继承(断子绝孙)。
密封类有什么用处?
密封类可以让底层子类不能被继承,保证代码规范性、结构性和安全性,主要用于复杂程序或程序框架之中。
如何声明密封类?
声明密封类只需要在class
关键字前加上sealed
关键字即可,例如下面的代码:
sealed class Cat : Animal{
...}