面向对象编程就是,找到对象,调用方法,完成需要的事情. 那么现在有一个场景:有 一个父类Person和两个子类Student与Teacher,在Main方法中有3个对象,分别是Person 对象、Student对象和Teacher对象. 那么现在想要用一种数据类型将其储存起来,就像数组 一样可以统一管理,该如何是好呢? 这里就需要考虑里氏转换了. 换句话说:里氏转换就是为了处理类型一致而存在的. 里氏转换的两条 对于里氏转换,只用记住两点即可(严格上讲应该是一个模型): 1、子类可以直接赋值给父类 2、父类若指向子类,那么可以强制转化为该子类 里氏转换的最重要的使用是在多态中,为了实现多态. 对于这两条准则,可以分别进行 一个演示,看看怎么回事儿. 子类可以直接赋值给父类 1、写一个父类和一个子类 class Person { public string Name; public Person(string name) { Name = name; } public void Say() { Console.WriteLine(“大家好,我是父类,我叫{0}”, Name); } } class Son : Person { 2 / 11 public Son(string name) : base(name) { } public void SayHello() { Console.WriteLine(“大家好,我是子类,我叫{0}”, Name); } } 注: -> 这里使用public修饰字段,目的是方便在子类和外界进行访问,而不使用属性 -> 不使用属性可以使代码变得简单点,便于理解 -> 虽然字段是public,但是依旧只允许在构造函数中进行初始化 2、在Main方法中添加代码 static void Main(string[] args) { Son son1 = new Son(“张三”); son1.SayHello(); son1.Say(); } 注: -> 编译、运行程序,可以分别得到子类SayHello方法与父类Say方法的运行结果 -> 这里由于子类Son继承自Person,因此具备了两种方法 3、修改一下Main方法 static void Main(string[] args) { Son son1 = new Son(“张三”); Person person1 = son1; person1.Say(); } 注: -> 这里new的是一个子类对象,但是赋值给了一个父类变量,编译没有报错 -> 由于person1是Person类型,类型决定了其能访问的成员 -> 如果person1能访问Sub的成员,那类就没意义了 -> person1 调用Say方法,执行父类方法 这里暂们看到的是首先new一个子类对象,然后将子类对象赋值给一个父类对象. 在实 际的使用过程中,常常可以这么用 Peron person1 = new Son(“张三”); 下面来看看一开始提出的问题. 1、要解决这个问题,我们先写一个父类(正常的父类) class Person 3 / 11 { private string name; private int age; private char gender; public Person(string name, int age, char gender) { this.name = name; this.age = age; this.gender = gender; } public void Say() { Console.WriteLine(“大家好,我是{0},今年{1}岁了,我是{2}生”, name, age, gender); } public string Name { get { return this.name; } } public int Age { get { return this.age; } } public char Gender { get { return this.gender; } } } 注: -> 定义一个父类Person,提供三个字段:姓名、年龄和性别 -> 字段为私有的,提供三个只读属性,允许外界对字段进行访问 -> 定义一个构造方法,来为三个字段进行初始化 2、写一个Student类 class Student : Person { private string hobby; public Person(string name, int age, char gender, string hobby) : base(name, age, gender) { this.hobby = hobby; } 4 / 11 public void SayHello() { Console.WriteLine(“大家好,我是{0},今年{1}岁,我是{2}孩,我的爱好是{3}”, Name, Age, Gender, hobby ) } public string Hobby { get { return this.hobby; } } } 注: -> 子类Student提供一个私有字段hobby,表示学生爱好 -> 子类继承字符类Person,因此具备了姓名、年龄与性别 -> 在子类中访问父类中的成员,只能通过属性来实现 -> 不要将方法名设为与父类的某方法相同 3、添加一个Teacher类 class Teacher : Person { private int yearsOfService; public Teacher(string name, int age, char gender, int yearsOfService) : base(name, age, gender) { this.yearsOfService = yearsOfService; } public void SayHello() { Console.WriteLine(“大家好,我是{0},我已经工作{1}年了”, Name, yearsOfService ); } public int YearsOfService { get { return this.yearsOfService; } } } 注: -> 子类Teacher与Student都是来自Person,都具备了姓名、性别与年龄 -> 子类Teacher提供了一个自己的字段,表示工作经验 -> 在子类中,访问父类的成员依旧使用属性 -> 子类Teacher中提供的方法名不要与父类的方法名相同 4、写好了三个类,那么就可以为Main方法添加内容了 static void Main(string[] args) 5 / 11 { Person person1 = new Person(“张三”,18,’男’); Teacher teacher1 = new Teacher(“李四”,45,’男’,20); Student student1 = new Student(“王五”,19,’男’,”打篮球”); Person[] persons = { person1, teacher1, student1 }; } 注: -> 这里根据三个类定义了三个对象 -> Person对象,姓名是张三,性别男,年龄18 -> Teacher对象,姓名李四,年龄45岁,性别男,工龄20年 -> Student对象,姓名王五,年龄19岁,性别男,爱好是打篮球 -> 然后声明一个Person数组,将三个对象一起放入 -> 由于Teacher与Student都派生于Person,因此可以使用Person数组存放这些对象 5、添加一个循环让三个人进行自我介绍 static void Main(string[] args) { Person person1 = new Person(“张三”,18,’男’); Teacher teacher1 = new Teacher(“李四”,45,’男’,20); Student student1 = new Student(“王五”,19,’男’,”打篮球”); Person[] persons = { person1, teacher1, student1 }; for(int i = 0; i < persons.Length; i++) { persons[i].Say(); } } 注: -> 这里由于都是Person对象,因此只能访问到Person对象的方法 -> 循环一个个的做自我介绍 指定父类强转为子类 对于一开始的问题是解决了,但是问题也来了. 将子类赋值给父类以后,那么子类对象 就变成了父类的对象,而原来那些子类的特征与方法难道就丢失了吗? 很显然,没有丢失. 要是随便就将数据丢掉了,那还写什么啊. 这里问题便是,转成父 类的子类对象,如何再调用原来的方法呢? 我们知道类型决定了能访问什么成员,因此想要调用子类的方法,就必须再将子类转换 回来. 这里便是里氏转换的第二句话: 如果父类对象指向的是一个子类对象,那么该父类对象可以强制转化为这个子类对象 这句话怎么听就觉得怎么别扭,还是直接来代码看看,先放下这个数组的例子,回到刚 刚说里氏转换第一句话用的例子上来. 6 / 11 1、续写一下Main方法 static void Main(string[] args) { Son son1 = new Son(“张三”); Person person1 = son1; person1.Say(); Son son2 = (Son)person1; son2.SayHello(); son2.Say(); } 注: -> 这里一开始new了一个Son对象,而后将其赋值给person1,转化为父类对象 -> 这里person1是一个父类对象,但是里面存放的是一个Son类型的引用类型 -> 因此可以直接将其强转为Son对象,再赋值给一个Son类型的变量 -> 转化回Son类型以后,即可使用子类和父类的方法了 总结一下: 其实就是将赋值给父类的子类对象,再次的赋值给子类对象. 将里氏转换原则的两句话做一个整合,实际上就是如下代码模型 父类 父类对象 = new 子类(); … … 子类 子类对象 = (子类)父类对象; 对于上面模型有几点要说明: 1、这里只是一个模型框架,代码的实现方式不仅仅就此一种; 2、在中间省略号部分,不允许再次为父类对象赋予其他类型的数据 3、对于里氏转换的第一句话,其实就是上面模型的第一句话 4、里氏转换的第二句话,就是上面模型的全部:强传的前提是,事前就存放的该类型 下面在回到前文数组处理Person、Student和Teacher的例子上来 1、修改一下Main方法 static void Main(string[] args) { Person person1 = new Person(“张三”,18,’男’); Teacher teacher1 = new Teacher(“李四”,45,’男’,20); Student student1 = new Student(“王五”,19,’男’,”打篮球”); Person[] persons = { person1, teacher1, student1 }; Person person2 = (Person)persons[0]; Teacher teacher2 = (Teacher)persons[1]; Student student2 = (Student)persons[2]; person2.Say(); person2.SayHello(); teacher2.Say(); 7 / 11 teacher2.SayHello(); student2.Say(); student2.SayHello(); } 注: -> 这里根据数组当中的数据,强制转化为对应的类型,并调用相应的方法 -> 但是这里的转换必须知道被转换的对象是什么类型,如若不知道则无法转换 -> 问题就来了,如果这个存放对象的数组很长,没有办法一个个列出来,能用循环吗? is 的用法 问题已经在上面提出来了,对于存储的对象,有时不能确定到底是什么类型,因此无法 进行强转,那么有没有办法呢? 如果有一种办法能判断就好了,好像“persons[1] 是不是 Teacher”的语言,返回一个 是与不是的结果. 很显然,程序员是伟大的,这就是is 的用法: 对象或实例 is 类型 它将返回一个bool值,如过左边的数据能够转化为右边的类型,那么就返回true;若果不 能转化,就返回false. 可以先不考虑上面的代码,来做个检验看看 1、写一个父类,不要求成员,只为了检验转换关系 class MyBase { } 2、写一个子类继承自MyBase class MySub : MyBase { } 3、在Main方法中添加代码 static void Main(string[] args) { MyBase myBase1 = new MyBase(); MySub mySub1 = new MySub(); bool isBelong = mySub1 is MyBase; Console.WriteLine(isBelong); } 注: -> 运行结果是true,表示mySub1是可以转化为MyBase类型 -> 由于mySub1是MySub类型,而MySub类型继承自MyBase类型 -> 由里氏转换原则便可知mySub1可以直接赋值给MyBase类型的变量,因此可以转换 8 / 11 4、但是不是所有的都可以转换呢?修改一下Main方法 static void Main(string[] args) { MyBase myBase1 = new MyBase(); MySub mySub1 = new MySub(); bool isBelong1 = mySub1 is MySub; bool isBelong2 = myBase1 is MySub; Console.WriteLine(isBelong1); Console.WriteLine(isBelong2); } 注: -> 运行结果是true和false -> 对于isBelong1,由于mySub1本身就是MySub类型,肯定是可以转的 -> 而isBelong2,由于myBase1本身是父类,父类不能直接转换为子类,所以为false -> 对于父类的转换,实际并不是将父类对象进行转换,而是将子类对象的衣服脱去 通过上面的例子可以发现,实现里氏转换是可以进行判断的. 因此在转换的时候只用加 一个if 判断就可以辨别了。回到刚刚的Person数组的例子,修改Main方法: static void Main(string[] args) { Person person1 = new Person(“张三”,18,’男’); Teacher teacher1 = new Teacher(“李四”,45,’男’,20); Student student1 = new Student(“王五”,19,’男’,”打篮球”); Person[] persons = { person1, teacher1, student1 }; for(int i = 0; i < persons.Length; i++) { if(persons[i] is Teacher) { ((Teacher)persons[i]).SayHello(); } else if(persons[i] is Student) { ((Student)persons[i]).SayHello(); } else { persons[i].Say(); } } } 注: -> 循环遍历persons数组,每次persons[i]中都会是一个对象,但是不知道具体类型 -> 使用if 依次判断是不是Teacher、Student,如果都不是就原样的调用父类的Say方法 -> 如果is 判断为true,就进行转换,并调用子类的SayHello方法 9 / 11 as的用法 对于判断是否可以转换,is 就已经可以使用了,不过有些时候对于某些对象,有种需求 是进行转换. 也就是说如果判断了能转,就进行转换,如果不行就什么也不做. 用is 描述就 是这样. … Teacher teacher2; if(persons[1] is Teacher) { teacher2 = (Teacher)persons[1]; } else { teacher2 = null; } … 在此类代码中主要是为了使用转换后的对象,若果没有转换则报一个错,或者使用一个方式 进行跳转等. 那么C#中还提供了一种语法,用来快速的完成上面的操作,就是as. 上面的代码完成的事情是,判断persons[1]能否转换为Teacher类型,如果可以转换, 则前转,并将转换的结果赋值给teacher2;如果转换不了,则为teacher2 赋值为null,那么 用as语法则为: Teacher teacher2 = persons[1] as Teacher; 注: -> 这段代码完成的工作与上面用is 中的一样 -> 准换判断is 只是完成判断的工作,其后的事情需要由程序员实现 -> as 完成的是判断并转换,如果能转,那么teacher2 就指向转换后的结果,否则为null 常见问题 这里常见的问题有两个方面: 1、父类与兄弟子类间的转换 2、使用if-is 进行判断的时候,判断条件应该由子类到父类的顺序排放 第一类问题 对于里氏转换父类不能直接转换为,前文中已经验证过,但是有的朋友会翻下面的错误 class MyBase { } class MySub1 : MyBase { } class MySub2 : MyBase { 10 / 11 } class Program { static void Main(string[] args) { MySub1 mySub1 = new MySub1(); MyBase myBase = mySub1; MySub2 mySub2 = (MySub2)myBase; } } 注: -> 运行起来,在“MySub2 mySub2 = (MySub2)myBase;”会报异常 -> 由于此时myBase中指向的对象是MySub1,因此无法将其强转为MySub2 第二类问题 还是前面Person数组的问题,类就不再这里重写了,一个Person、一个Teacher和一个 Student类,下面对Main方法做一点点修改 static void Main(string[] args) { Person person1 = new Person(“张三”,18,’男’); Teacher teacher1 = new Teacher(“李四”,45,’男’,20); Student student1 = new Student(“王五”,19,’男’,”打篮球”); Person[] persons = { person1, teacher1, student1 }; for(int i = 0; i < persons.Length; i++) { if(persons[i] is Person) { persons[i].Say(); } else if(persons[i] is Student) { ((Student)persons[i]).SayHello(); } else if(persons[i] is Teacher) { ((Teacher)persons[i]).SayHello(); } } } 注: -> 编译运行正常,执行也没有什么异常,但是结果不太对劲儿 -> 由于is 是判断对象是否可以转换,这里Person是父类,所以任何时候都为true -> 由于执行了第一个if,那么后面的所有的判断都不再执行 -> 因此使用if-is 结构判断有个原则,就是父类往后放,越是祖宗越在后面 11 / 11 总结一下 对于里氏转换,是建立在继承的基础之上,在有了继承以后,子类对象可以直接赋值给 父类对象,即使用父类对象引用子类的对象,就好像将子类转换成了父类一样. 而父类对象不能直接转换为子类对象,转换的前提是父类对象本身指向的就是要转的子 类类型,因此在转换前需要使用is 或as进行判断.