面向对象三大特性:封装,继承与多态,这里讲一下后两个特性。
继承
继承:指一个对象延续另一个对象的特征(属性和方法),并且有自己的个性特征(属性和方法)。
必要性:代码复用,避免重复;一处更新,处处更新。与封装不同的是,封装主要是封装一个方法、一个类,使用时直接调用不能更改;继承主要讲需要的属性和方法(主要是方法)进行“封装”,且要使用时还可以继续扩展自己的特性(继续增加、修改方法--方法重写)。
使用广泛:C#里,Object类是所有类的基类,winform里面所有控件都继承于Control类。
父类与子类:当A继承于B时,A是子类(或叫派生类),B是父类(或叫基类或超类)。
传递性:A→B,B→C,C具有A的特性。
单根性:一个子类只能有一个父类,一个父类可以有若干子类。
protected:只有父类与子类才能访问。
sealed:密封类,不允许有子类,有利于保护知识产权。
父类与子类关系:父类完全包含于子类,子类完全包含父类:
举例说明一下继承的好处
需求说明:
设计玩具猫、玩具狗相关程序,要求:
属性:姓名,自身颜色,自己类别,喜好食物
动作:自我介绍,跳舞,赛跑
常规实现:
Models--Cat

1 namespace InheritanceTest 2 { 3 class Cat 4 { 5 public string Name { get; set; } 6 public string Color { get; set; } 7 public string Kind { get; set; } 8 public string Favorite { get; set; } 9 10 public void Introduce() 11 { 12 Console.WriteLine("Hi, My name is{0}, I'm a {1}, I'm in {2}, and I like eat {3} very much! ",Name,Kind, Color,Favorite); 13 } 14 15 public void Dancing() 16 { 17 Console.WriteLine("Now I dancing for you!"); 18 } 19 20 } 21 }
Models--Dog

1 namespace InheritanceTest 2 { 3 class Dog 4 { 5 public string Name { get; set; } 6 public string Color { get; set; } 7 public string Kind { get; set; } 8 public string Favorite { get; set; } 9 10 public void Introduce() 11 { 12 Console.WriteLine("Hi, My name is{0}, I'm a {1}, I'm in {2}, and I like eat {3} very much! ", Name, Kind, Color, Favorite); 13 } 14 15 public void Running() 16 { 17 Console.WriteLine("I can run very fast!"); 18 } 19 } 20 }
Program

1 namespace InheritanceTest 2 { 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 Cat cat = new Cat() 8 { 9 Name="kitty", 10 Kind="cat", 11 Color="white", 12 Favorite="fish" 13 }; 14 cat.Introduce(); 15 cat.Dancing(); 16 17 Console.WriteLine(); 18 19 Dog dog = new Dog() 20 { 21 Name = "David", 22 Kind = "dog", 23 Color = "brown", 24 Favorite = "bone" 25 }; 26 dog.Introduce(); 27 dog.Running(); 28 29 Console.ReadLine(); 30 } 31 } 32 }
结果:
问题提出:
出现代码重复:
使用继承来解决这个问题:
增加父类--Animal来存放重复的代码

1 class Animal 2 { 3 public string Name { get; set; } 4 public string Color { get; set; } 5 public string Kind { get; set; } 6 public string Favorite { get; set; } 7 8 public void Introduce() 9 { 10 Console.WriteLine("Hi, My name is{0}, I'm a {1}, I'm in {2}, and I like eat {3} very much! ", Name, Kind, Color, Favorite); 11 } 12 }
改一下Dog、Cat:

1 class Dog:Animal 2 { 3 public void Running() 4 { 5 Console.WriteLine("I can run very fast!"); 6 } 7 } 8 9 //要写在各自的类里面,这里为了省事放在一起了 10 11 class Cat:Animal 12 { 13 public void Dancing() 14 { 15 Console.WriteLine("Now I dancing for you!"); 16 } 17 }
Program里面不需要修改,我们可以一样得到上面的结果,这就是使用继承的好处。
继承使用的步骤与特点:
抽象公共部分,放到特定类,即父类;
其他类继承父类,即可拥有父类的特征(属性和方法);
在子类中再添加自己的特征(属性和方法)。
继承中的构造函数
刚刚是使用对象初始化器来实现属性的赋值的,下面使用构造函数来赋值的例子,介绍this、base关键字的使用。
Models--Animal、Cat、Dog添加构造函数(注意:Animal必须要具有无参构造函数)

1 //Animal 2 public Animal() { } 3 public Animal(string name,string color,string kind) 4 { 5 this.Name = name; 6 this.Color = color; 7 this.Kind = kind; 8 } 9 10 //Cat 11 public Cat(string name, string color, string kind, string favorite) 12 { 13 this.Name = name; 14 this.Color = color; 15 this.Kind = kind; 16 this.Favorite = favorite; 17 } 18 19 //Dog 20 public Dog(string name, string color, string kind, string favorite) 21 { 22 this.Name = name; 23 this.Color = color; 24 this.Kind = kind; 25 this.Favorite = favorite; 26 }
Program改写:

1 static void Main(string[] args) 2 { 3 Cat cat = new Cat("kitty", "cat", "white", "fish"); 4 cat.Introduce(); 5 cat.Dancing(); 6 7 Console.WriteLine(); 8 9 Dog dog = new Dog("David", "dog", "brown", "bone"); 10 dog.Introduce(); 11 dog.Running(); 12 13 Console.ReadLine(); 14 }
再次出现代码重复:
使用base关键字来优化:
优化Models--Dog、Cat构造函数

//Dog public Dog(string name, string kind, string color, string favorite):base(name, color, kind) { this.Favorite = favorite; } //Cat public Cat(string name, string kind, string color, string favorite):base(name, color, kind) { this.Favorite = favorite; }
这里使用了base关键字,该关键字除了可以调用父类的构造方法,还可以调用父类的属性和方法,使用关键字base能将逻辑变得清晰,this关键字则是表示自己类里面的属性和方法,与base做区分。
protected关键字限制了父类的某个成员只能被其子类访问,但是如果在父类里面使用public去调用protected,依旧是可以访问带protected的成员,但是给带protected的属性赋值只能通过构造函数的方式。
继续深入:
我们回头看看会发现,刚刚我们解决的是属性代码重复的问题,如果现在方法代码也重复怎么办?先举例说明!
我们给Cat、Dog添加方法:

//Dog public void Have() { Console.WriteLine("I'm David, I want to eat bone please…"); } //Cat public void Have() { Console.WriteLine("I'm kitty, I want to eat fish please…"); }
各自单独调用自然没问题,但是如果统一放到list里面的时候,就麻烦了:
Program:

1 static void Main(string[] args) 2 { 3 Cat cat = new Cat("kitty", "cat", "white", "fish"); 4 cat.Introduce(); 5 cat.Dancing(); 6 7 Console.WriteLine(); 8 9 Dog dog = new Dog("David", "dog", "brown", "bone"); 10 dog.Introduce(); 11 dog.Running(); 12 13 Console.WriteLine(); 14 15 List<Animal> list = new List<Animal>(); 16 list.Add(cat); 17 list.Add(dog); 18 foreach (var item in list) 19 { 20 if (item is Cat) 21 { 22 Cat c = item as Cat; 23 c.Have(); 24 } 25 else if (item is Dog) 26 { 27 Dog d = item as Dog; 28 d.Have(); 29 } 30 } 31 Console.ReadLine(); 32 } 33 }
先看一下结果:
先介绍几个关键字:
is : 检查对象是否与指定类型兼容,不兼容则返回false;as:用于在兼容的引用类型之间执行转换,失败则返回null 。
父类是完全包含于子类的,因此用父类创建的list,可以放子类类型成员进去,但放进去后会被转换为父类类型成员,因此在取出时需要再进行转换。
上面Program代码里面有几个问题:
第一,每次在遍历List的时候都需要判断成员的类型;
第二,都需要进行一次转换才能去调用相应的Have方法。
而且这不符合“开-闭原则”---开发扩展,封闭修改。就是应该要尽可能少的去修改源代码,这里很明显,当子类再增加的时候,需要再去加判断才行。
使用抽象方法进行优化
这里使用抽象方法对以上问题进行优化,关键字:abstract, override。
抽象类:使用abstract修饰的类,抽象类可以没有抽象方法,但抽象方法不能没有抽象类。抽象类不能创建实例对象。Animal a=new Animal();是错的,Animal c=new Cat();是对的。抽象类不能是静态的或密封的。
抽象方法:即在父类定义一个方法,但不去实现,在子类实现或重写(若在子类重写,需要在子类的子类实现,以此类推…)。定义抽象方法使用关键字abstract,当一个类中有一个抽象方法时,需要在类的前面也加上abstract关键字;子类在方法重写时加上override关键字。
下面用代码演示:
Models--Animal

1 abstract class Animal 2 { 3 public Animal() { } 4 public Animal(string name,string color,string kind) 5 { 6 this.Name = name; 7 this.Color = color; 8 this.Kind = kind; 9 } 10 public string Name { get; set; } 11 public string Color { get; set; } 12 public string Kind { get; set; } 13 public string Favorite { get; set; } 14 15 public void Introduce() 16 { 17 Console.WriteLine("Hi, My name is {0}, I'm a {1}, I'm in {2}, and I like eat {3} very much! ", Name, Kind, Color, Favorite); 18 } 19 20 public abstract void Have(); 21 }
Models--Dog、Cat

//Dog public override void Have() { Console.WriteLine("I'm David, I want to eat bone please…"); } //Cat public override void Have() { Console.WriteLine("I'm kitty, I want to eat fish please…"); }
Program只需要修改foreach循环:

foreach (var item in list) { item.Have(); }
不再需要判断、转换,虚拟机会在运行时自动判断当前成员类型,再去调用相应子类的重写方法。
这里面我们在父类定义Have方法的时候使用的是抽象方法---abstract,在父类是不能对抽象方法进行编写的;还有一种可能,就是我们可以在父类对该方法进行实现,子类可以自行选择是重写该方法,还是直接使用父类的默认方法,这加强了灵活性,这就是虚方法---virtual,单纯使用虚方法的时候该类不需要再写上abstract(测试环境:VS2015,.Net 4.5),但是如果该类里面有其他抽象方法还是要写abstract关键字的。
虚方法使用很广泛,系统自带的虚方法比如:Equals,ToString等。Equals()默认是对两个值的引用地址进行比较,如果不一致则返回false;如果我们想让他对引用类型的变量的值进行比较,就应该重写该方法。比如:
重写Equals()

public override bool Equals(object obj) { Cat objCat = obj as Cat; if (objCat.Name == this.Name && objCat.Kind == this.Kind && objCat.Color == this.Color && objCat.Favorite == this.Favorite) return true; else return false; }
这样去创建两个Cat对象,就可以对他们的值进行对比了。
同理可以去改写其他的系统方法。
补充一下:虚方法是通过override去对父类方法重写实现,实际上我们还是可以通过base.虚方法()这样的方式去调用父类的方法的;这里介绍一个方法覆盖,使用new关键字,比如上面父类Animal里面有一个Introduce方法:
public void Introduce() { Console.WriteLine("Hi, My name is {0}, I'm a {1}, I'm in {2}, and I like eat {3} very much! ", Name, Kind, Color, Favorite); }
我们在子类去进行方法覆盖,在子类Cat里进行覆盖:
public new void Introduce() { Console.WriteLine("I think I should be different from that dog…"); }
当Cat对象再次去调用Introduce这个方法时,只能使用新方法,不能再去实现父类方法了。
多态
其实我们在上面已经用到了多态这个特性了。
在上面我们通过抽象类抽象方法(或虚方法)实现了不同的对象去调用同样一个方法时,产生不同的行为,这就是“继承多态”,就是多态的一种,还有一个就是“接口多态”,与此类似,不过是通过接口实现的而不是继承。
多态概念:不同对象,接受相同的信息,产生不同的行为,称为多态。多态有虚拟机自行决定,它会根据子类的原始类型,自动调用该对象使用override重写后的方法,利于程序扩展。
使用继承实现多态的要求:
- 父类必须有抽象方法或虚方法;
- 子类必须重写父类的抽象方法或虚方法;
- 子类必须转换为父类类型去使用。
可以将父类类型作为方法参数,但是传递的是子类类型,这就是里氏替换原则(LSP):
- 子类对象能够替换其父类;
- 父类对象不能够替换其子类;
- 父类的方法都要在子类中实现或重写。
面向对象三大特性总结:
- 封装:隐藏内部实行,稳定外部接口 →系统安全性
- 继承:子类继承父类成员,实现代码复用 →开发和维护效率
- 多态:不同子类,对同一消息,作出不同反映 →系统扩展性