面向对象三大特
继承 (Inheritance) 是面向对象语言中的重要特征之一。
在 C# 语言中仅支持单重继承,主要用于解决代码重用问题。
为了将继承关系灵活运地用到程序设计中,在 C# 语言中提供了接口来解决多重继承的关系。
在编程中灵活地使用类之间的继承关系能很好地利于重用代码和节省开发时间
也就是说,当A继承于B之后,A在拥有B含有的全部类成员的同时,还可以特立独行的拥有它自己的成员。
【举例】
我现在有一个Student类有如下属性:
姓名、性别、身份证号、联系方式、专业、年级
同时有个Teacher类有如下属性:
姓名、性别、身份证号、联系方式、职称、工资号
可以很明显的发现,姓名、性别、身份证号、联系方式这四个熟悉是在重复造轮子。
那么此时其实可以考虑声明一个Person类,在这个类中放置这四个属性,然后让Student类和Teacher类继承于Person类,那么Student类中只是需要额外声明专业和年级两个属性,Teacher类中只需要额外声明职称和工资号两个属性。一定程度上的减轻我们重复编码的动作。
继承使用的语法:
<访问修饰符> class <基类> { ... } class <派生类> : <基类> { ... }
上述案例中的Person类在官方定义中是被称为基类(其实可能更习惯称为父类),Student类和Teacher类被称为Person类的派生类(也是通常称呼它俩是子类)
Objecte类
Object 类是 C# 语言中最原始、最重要的类。
不管是 C# 系统所提供的标准类,还是用户自行編写的类,都是从Object类直接或间接继承而来,它是类层次结构中的顶级类,即 C# 树型类层次结构的根。
当编码者没有明确指出当前类的父类的时候,默认是认为该类从Object类继承而来。
身为最原始父类的Object类在功能上比较有限的,其提供了四个常用方法:Equals方法、GetHashCode方法、GetType方法、ToString方法。
在实际开发中,咱们大多数需要去重写这个四个方法。
Objecte中的四大元老方法
① Equal方法
Equals 方法主要用于比较两个对象是否相等,如果相等则返回 True,否则返回 False。
如果是引用类型的对象,则用于判断两个对象是否引用了同一个对象。
至于判断是否为同一对象,我的理解是判断是不是同一个地址
知识点一:Equals 方法提供了两个,一个是静态的,一个是非静态的
静态的可以直接使用了,至于非静态的,则需要创建对象,通过对象调用此方法了。
Equals (object ol, object o2); //静态方法 Equals (object o); //非静态方法
【课堂演示】
首先创建 Person 类,在Person类中可以不必写任何代码,然后创建一个Test类,在这个类中,编写一个 Test_Equals的方法,用于测试如何比较两个对象是否相等。
最后在Main函数中创建Test类的对象,并调用Test_Equals方法。
测试效果:
测试总结:
Equals用于引用类型的对象的比较时,比较的是其地址
测试代码:
/// <summary> /// Main函数 /// </summary> class Program { public static void Main(string[] args) { Test t = new Test(); t.Test_Equals(); Console.ReadKey(); } } /// <summary> /// Person 类 /// </summary> public class Person { public int Id { get; set; } public string Name { get; set; } public string Sex { get; set; } } /// <summary> /// 获取引用类型地址,比较引用类型地址的方法 /// </summary> public void Test_Equals() { try { Person pp = new Person(); Person ee = new Person(); Person tt = pp; //对象tt 是由 对象pp 直接赋值 pp.Id = 99; pp.Name = "test"; pp.Sex = "nan"; // 获取对象的地址 var addr2 = getMemory(pp); var addr3 = getMemory(ee); var addr4 = getMemory(tt); Console.WriteLine("对象pp的地址是" + addr2); Console.WriteLine("对象ee的地址是" + addr3); Console.WriteLine("对象tt的地址是" + addr4); Console.WriteLine(); Console.ForegroundColor = ConsoleColor.DarkRed; Console.WriteLine("Equals 方法主要用于比较两个对象是否相等,如果相等则返回 True,否则返回 False。\n" + "下面切实对Equals(静态方法) 进行使用:"); Console.ResetColor(); Console.WriteLine("对象pp和对象ee是否相等:{0}",Equals(pp,ee)); Console.WriteLine(); Console.WriteLine("对象tt直接由对象pp赋值:"); Console.WriteLine("对象pp和对象tt是否相等:{0}", Equals(pp, tt)); Console.WriteLine(); Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("Equals 方法主要用于比较两个对象是否相等,如果相等则返回 True,否则返回 False。\n" + "下面切实对Equals(非静态方法) 进行使用:"); Console.ResetColor(); Console.WriteLine("对象pp和对象ee是否相等:{0}", ee.Equals(pp)); } catch (Exception ex) { } } // 获取引用类型的内存地址方法 public string getMemory(object o) { GCHandle h = GCHandle.Alloc(o, GCHandleType.WeakTrackResurrection); IntPtr addr = GCHandle.ToIntPtr(h); return "0x" + addr.ToString("X"); } }
② GetHashCode方法
GetHashCode 方法返回当前 System.Object 的哈希代码,每个对象的哈希值都是固定的。
【课堂演示】
创建两个 Person 类的对象,并分别计算其哈希值。
测试效果:
测试代码
class Program { public static void Main(string[] args) { //Test t = new Test(); //t.Test_Equals(); Person person1 = new Person(); Person person2 = new Person(); Console.WriteLine("person1对象的哈希值是:"+person1.GetHashCode()); Console.WriteLine("person2对象的哈希值是:"+person2.GetHashCode()); Console.ReadKey(); } }
③ GetType方法
作用:
GetType 方法用于获取当前实例的类型,返回值为 System.Type 类型。
注意点:
GetType 方法不含任何参数,是非静态方法。
【课堂演示】
【实例】创建字整数类型的变量、浮点类型变量、符串类型的变量、以及 Person 类的对象,并分别使用 GetType 方法获取其类型并输出。
测试效果:
测试代码
class Program { public static void Main(string[] args) { int a = 520; double b = 13.14; string c = "1314520"; Person d = new Person(); Console.WriteLine(a.GetType()); Console.WriteLine(b.GetType()); Console.WriteLine(c.GetType()); Console.WriteLine(d.GetType()); Console.ReadKey(); } }
④ ToString方法
作用:
ToString 方法返回一个对象实例的字符串,在默认情况下将返回类类型的限定名。
这个方法我觉得挺常用的,更多的是根据自己的需求,重写ToString方法,返回自己需求的字符串信息。
【课堂演示】
创建整数类型的变量、浮点类型变量以及 Object 类的对象和Person类型的对象,并分别使用 ToString 方法获取其字符串的表现形式并输出。
测试效果:
测试总结:
可以浅总结出来,在没有重写的ToString 中,值类型输出的结果是当前的数值,对于应用类型,输出的是当前对象的类名称。
测试代码:
class Program { public static void Main(string[] args) { Int32 a = 520; Object b = new Object(); double c = 13.14; Person d = new Person(); Console.WriteLine("值类型(Int32类型)的字符串的表现形式:{0}", a.ToString()); Console.WriteLine("值类型(double类型)的字符串的表现形式:{0}", c.ToString()); Console.WriteLine("引用类型(Object类型)字符串的表现形式:{0}", b.ToString()); Console.WriteLine("引用类型(Person类型)字符串的表现形式:{0}", d.ToString()); Console.ReadKey(); } }
继承中的四个关键字
① 调用父类成员方法 —— base
1)继承关系中属性、方法之间的关系
当子类和父类定义了同名的方法,在子类中的对象是调用不到父类中的同名方法的,调用的是子类中的方法。
子类如果需要调用父类中的成员可以借助 base 关键字来完成,具体的用法如下:
base. 父类成员
用户在程序中会遇到 this 和 base 关键字,this 关键字代表的是当前类的对象,而 base 关键字代表的是父类中的对象。
在Student类和Teacher类都继承于Person类的情况,它们都有自己的Print方法用于输出信息。
倘若不加base关键字,直接调用两个派生类中的Print方法,会得到如下结果:
在这里插入图片描述
在需要利用父类中方法时,此时需要base关键字的帮助了:
测试结果:
2)继承关系中构造器之间的关系
默认情况下,在子类的构造器中都会自动调用父类的无参构造器,如果需要调用父类中带参数的构造器才使用:base(参数)的形式。
【默认情况演示】
测试结果:
【调用子类有参数构造器演示】
测试结果:
那么,倘若想调用父类的有参数构造器,就需要:base(参数)来实现了
测试结果:
测试总结:
如果需要调用父类中带参数的构造器才使用:base(参数)的形式。
② 增加程序延展性 —— virtual 以及 abstract
1)virtual(虚方法)
virtual 关键字能修饰方法、属性、索引器以及事件等,用到父类的成员中
其语法形式如下:
//修饰属性 public virtual 数据类型 属性名{get; set; } //修饰方法 访问修饰符 virtual 返回值类型方法名 { 语句块; }
注意点:virtual 关键字不能修饰使用 static 修饰的成员。
virtual 关键字用于在基类中修饰方法。virtual的使用会有两种情况:
情况1:在基类中定义了virtual方法,但在派生类中没有重写该虚方法。那么在对派生类实例的调用中,该虚方法使用的是基类定义的方法。
拓展:
此时在子类Student类中的方法是没有修饰符的,倘若加上new 关键字来使该子类中的方法隐藏,得到的结果是一样的
new 修饰符(C# 参考)
咱们来浅看一下输出的结果:
测试代码:
public class Person { public int Id { get; set; }=1001; public string Name { get; set; } = "张三"; public string Sex { get; set; } = "男"; public string Cardid { get; set; } = "0x1111"; public string Tel { get; set; } = "13390782367"; public Person() { //Console.WriteLine("这是父类Person中的无参数构造器"); } public Person(string str) { Console.WriteLine("这是父类Person中的有参数构造器,输入的参数是"+str); } public virtual void Print() { Console.WriteLine("编号:" + Id); Console.WriteLine("姓名:" + Name); Console.WriteLine("性别:" + Sex); Console.WriteLine("身份证号:" + Cardid); Console.WriteLine("联系方式:" + Tel); } } class Student:Person { public string Major { get; set; } = "计科技"; public string Grade { get; set; } = "2001"; public void Print() { Console.WriteLine("专业:" + Major); Console.WriteLine("年级:" + Grade); } } class Program { public static void Main(string[] args) { Person p = new Person(); Console.ForegroundColor = ConsoleColor.DarkRed; Console.WriteLine("这是父类Person自己的Print方法(虚方法)输出的内容:"); p.Print(); Console.ResetColor(); Console.WriteLine(); Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("在子类Student不重写该虚方法的情况下:"); Person stu1 = new Student(); stu1.Print(); Console.ResetColor(); Console.ReadKey(); } }
情况2:在基类中定义了virtual方法,然后在派生类中使用override重写该方法。那么在对派生类实例的调用中,该虚方法使用的是派生重写的方法。
测试结果:
那么总结来说,在对父类使用virtual关键字的时候,在其继承的子类声明另一个同名方法且没有使用override重写的时候,默认调用父类中的方法。
但是假如重写了,可以理解为子类方法更优秀了,那么系统就调用这个重写的方法。
2)abstract(抽象方法)
可以看成是没有实现体的虚方法。
抽象方法声明使用,是必须被派生类覆写的方法,抽象类就是用来被继承的;如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法;抽象类不能有实体的。
抽象方法的定义语法:
访问修饰符 abstract 方法返回值类型 方法名(参数列表);
【随堂演练】
创建抽象类 ExamResult,并在类中定义学号(Id)、数学 (Math)、英语 (English) 成绩的属性,定义抽象方法计算总成绩。
分别定义数学专业和英语专业的学生类继承抽象类 ExamResult,重写计算总成绩的方法并根据科目分数的不同权重计算总成绩。
其中,数学专业的数学分数占60%、英语分数占40%;英语专业的数学分数占40%、英语分数占60%。
测试结果:
这个小练习蛮简单的,其作用也就仅限于熟悉上手定义抽象类和重写抽象方法
测试代码
abstract class ExamResult { //学号 public int Id { get; set; } //数学成绩 public double Math { get; set; } //英语 public double English { get; set; } //计算总成绩 public abstract void Total(); } class MathMajor : ExamResult { public override void Total() { double total = Math * 0.6 + English * 0.4; Console.WriteLine("学号为"+Id+"的数学专业的同学的最终成绩为:"+total); } } class EnglishMajor : ExamResult { public override void Total() { double tatol = Math * 0.4 + English * 0.6; Console.WriteLine("学号为"+Id+"的英语专业的同学的最终成绩为"+tatol); } }
总结:
在实际应用中,子类仅能重写父类中的虚方法或者抽象方法,当不需要使用父类中方法的内容时,将其定义成抽象方法,否则将方法定义成虚方法。
③ 声明密封类或密封方法 —— sealed
sealed 关键字的含义是密封的,使用该关键字能修饰类或者类中的方法,修饰的类被称为密封类、修饰的方法被称为密封方法。
密封方法必须出现在子类中,并且是子类重写的父类方法,即 sealed 关键字必须与 override 关键字一起使用。
【随堂演练】
创建一个计算面积的抽象类 AreaAbstract ,并定义抽象方法计算面积。
定义矩形类继承该抽象类,并重写抽象方法,将其定义为密封方法;
定义圆类继承该抽象类,并重写抽象方法,将该类定义为密封类。
此处还可以积累C#如何输出指定位数的浮点数。
参考文章
参考代码
abstract class AreaAbstract { public abstract void Total(); } class Rectangle : AreaAbstract { //设置自己的长和宽的熟悉 public double Len { get; set; } public double Wid { get; set; } //重写抽象方法,并定义为密封方法 public sealed override void Total() { Console.WriteLine("该矩形的面积是"+Len*Wid); } } sealed class Round : AreaAbstract { //计算圆面积的半径熟悉 public double R { get; set; } public override void Total() { //积累C# 浮点类型输出指定位数 Console.WriteLine("该圆的面积是{0:f3}",R*Math.PI); } }
Round 类不能被继承:Rectangle 类中的 Area 方法不能被重写。
总结
① C#仅支持单继承,用于解决代码重用问题。C#使用接口解决多重继承。
② Object 类是 C# 语言中最原始、最重要的类。旗下有Equals()、GetHashCode()、GetTyep()、ToString()四个方法,我们在实际使用的时候,大多数是需要重写这四个方法的。
③ 在子类中想要使用父类中的某个方法,需要使用base关键字
④ 子类仅能重写父类中的虚方法或者抽象方法,当不需要使用父类中方法的内容时,将其定义成抽象方法,否则将方法定义成虚方法。