继承
整理要点
前言
面向对象(Object-Oriented, OO):
C#不是一种纯粹的面向对象编程语言。
C#提供了多种编程范例。
然而,面向的对象是C#的一种重要概念,也是.NET提供的所有库的核心原则。
面向对象的三大重要概念:封装、继承和多态性。
继承的类型
- 单重继承:表示一个类可以派生自一个基类。C#就采用这种继承。
- 多重继承:多重继承允许一个类派生自多个类。C#不支持类的多重继承,但允许接口的多重继承。
- 多层继承:允许继承有更大的层次结构。例如:类B继承于A,类C又继承于B。其中,类B也称为中间基类。C#支持多层继承,且很常用。
- 接口继承:定义了接口的继承。这里允许多重继承。
多重继承
一些语言(如C++)支持所谓的“多重继承”,即一个类可以继承多个类。对于实现继承,多重继承会给生成的代码增加复杂性,还会带来一些开销。因此,C#不支持多重继承。
而C#允许类型派生自多个接口。一个类可以实现多个接口。即:一个类可以派生自另一个类和任意多个接口。
准确地说,System.Object是一个公共的基类,所以每个C#类(除了Object类之外)都有一个基类,还可以有任意多个基类接口。
结构和类:
结构不支持继承,但每个结构都自动派生自System.ValueType。
不能编码实现结构的类型层次,但结构可以实现接口。即:结构并不支持实现继承,但支持接口继承。
实现继承
一个类可以派生自另一个类和任意多个接口:
public class Animal { public string Name { get; set; } } public interface IAction { void Eat(); } public class Dog: Animal, IAnimal { public void Eat() { // } }
虚方法:
把一个基类方法声明为virtual,就可以在任何派生类中重写该方法
在派生类中重写虚方法时使用orverride关键字显示声明:
/// <summary> /// 动物类 /// </summary> public class Animal { public virtual void Eat() { Console.WriteLine("吃东西!"); } } /// <summary> /// 狗类 /// </summary> public class Dog : Animal { public override void Eat() { Console.WriteLine("狗吃骨头!"); } } /// <summary> /// 猫类 /// </summary> public class Cat : Animal { public override void Eat() { Console.WriteLine("猫吃鱼!"); } }
多态性:
例如:某个方法的参数使用基类类型定义,任何该基类的派生类都可以作为参数使用该方法。
代码如下:
1 /// <summary> 2 /// 表示动物吃什么的方法 3 /// </summary> 4 /// <param name="animal">使用基类(动物类)定义</param> 5 public void EatWhat(Animal animal) 6 { 7 animal.Eat(); 8 } 9 10 // 调用时,就可以使用具体对象 11 var dog = new Dog(); // 创建一个狗对象 12 var cat = new Cat(); // 创建一个猫对象 13 EatWhat(dog); // 狗吃骨头! 14 EatWhat(cat); // 猫吃鱼!
隐藏方法:
如果基类和派生类中拥有签名相同的方法,但该方法没有分别声明为virtual和override,那么派生类方法就会隐藏基类方法。
例如:
1 /// <summary> 2 /// 动物类 3 /// </summary> 4 public class Animal 5 { 6 /// <summary> 7 /// 基类中没有声明为虚方法 8 /// </summary> 9 public void Eat() 10 { 11 Console.WriteLine("吃东西!"); 12 } 13 } 14 /// <summary> 15 /// 狗类 16 /// </summary> 17 public class Dog : Animal 18 { 19 /// <summary> 20 /// 派生类中也没有使用override关键字重写 21 /// 使用new关键字表示覆盖基类的方法 22 /// </summary> 23 public new void Eat() 24 { 25 Console.WriteLine("狗吃骨头!"); 26 } 27 } 28 /// <summary> 29 /// 猫类 30 /// </summary> 31 public class Cat : Animal 32 { 33 /// <summary> 34 /// 派生类中也没有使用override关键字重写 35 /// 使用new关键字表示覆盖基类的方法 36 /// </summary> 37 public new void Eat() 38 { 39 Console.WriteLine("猫吃鱼!"); 40 } 41 }
new 方法修饰符不应该故意用于隐藏基类的方法,主要是来解决版本冲突,在修改派生类后,响应基类的变化。
最好使用在基类中指定方法为虚方法,在需要的时候,在派生类中使用override关键字重写基类方法。
调用方法的基类版本:
1 /// <summary> 2 /// 狗类 3 /// </summary> 4 public class Dog : Animal 5 { 6 /// <summary> 7 /// 派生类中也没有使用override关键字重写 8 /// 使用new关键字表示覆盖基类的方法 9 /// </summary> 10 public new void Eat() 11 { 12 base.Eat(); 13 Console.WriteLine("狗吃骨头!"); 14 } 15 }
抽象类和抽象方法:
C#允许把类和方法声明为abstract。
- 抽象类不能被实例化
- 抽象方法不能直接显示,必须在非抽象的派生类中重写。
- 所以抽象方法本身就是虚拟的,不需要也不能再显示的声明为virtual
- 如果类中包含抽象方法,则该类也是抽象的,也必须显示的声明为抽象类!
例如:
1 /// <summary> 2 /// 动物类 3 /// </summary> 4 public abstract class Animal 5 { 6 /// <summary> 7 /// 抽象方法不能直接实现! 8 /// </summary> 9 public abstract void Eat(); 10 } 11 /// <summary> 12 /// 狗类 13 /// </summary> 14 public class Dog : Animal 15 { 16 /// <summary> 17 /// 抽象方法必须在非抽象的派生类中使用override关键字重写 18 /// </summary> 19 public override void Eat() 20 { 21 Console.WriteLine("狗吃骨头!"); 22 } 23 } 24 /// <summary> 25 /// 猫类 26 /// </summary> 27 public class Cat : Animal 28 { 29 /// <summary> 30 /// 抽象方法必须在非抽象的派生类中使用override关键字重写 31 /// </summary> 32 public override void Eat() 33 { 34 Console.WriteLine("猫吃鱼!"); 35 } 36 } 37 38 // 抽象方法不能直接实例化,可以使用基类声明和创建派生类的实例 39 Animal dog = new Dog(); 40 Animal cat = new Cat();
密封类和密封方法:
表示指定的类不能被继承或指定的方法不能被重写。
使用关键字sealed:
1 public sealed class Animal 2 { 3 /// <summary> 4 /// 抽象方法不能直接实现! 5 /// </summary> 6 public void Eat() 7 { 8 Console.WriteLine("吃东西!"); 9 } 10 } 11 public class Dog : Animal // 报错,Animal是密封类 无法被继承 12 { 13 14 }
密封方法不能被重写:
1 public abstract class Animal 2 { 3 public abstract void Eat(); 4 } 5 public class Dog : Animal 6 { 7 /// <summary> 8 /// 表示密封方法 9 /// </summary> 10 public sealed override void Eat() 11 { 12 Console.WriteLine("吃骨头!"); 13 } 14 } 15 16 /// <summary> 17 /// 表示柯基狗 18 /// </summary> 19 public class KeJiDog : Dog 20 { 21 /// <summary> 22 /// 派生类中无法重写! 23 /// </summary> 24 public override void Eat() // 报错 25 { 26 Console.WriteLine("柯基狗吃骨头!"); 27 } 28 }
派生类的构造函数:
如果一个基类中没有声明无参数的构造函数,那么当派生类继承它是也必须要实现基类中的构造函数。
如果基类中有多个有参的构造函数,可以在派生类中自行指定要实现的基类的构造函数(根据参数的不同)
例如:
1 public class Animal 2 { 3 public Animal(string food) 4 { 5 Console.WriteLine($"吃:{food}"); 6 } 7 8 public Animal(string food, string food2) 9 { 10 Console.WriteLine($"吃:{food}和 {food2}"); 11 } 12 } 13 public class Dog : Animal 14 { 15 public Dog() : base("骨头") 16 { 17 } 18 }
访问修饰符
修饰符 | 应用于 | 说明 |
public | 所有类型或成员 | 任何代码均可以访问该项 |
protected | 类型和内嵌类型的所有成员 | 只有派生类的类型能访问该项 |
internal | 所有类型或成员 | 只能在包含它的程序集中访问该项 |
private | 类型和内嵌类型的所有成员 | 只能在它所属的类型中访问该项(类自身的内部) |
protected internal | 类型和内嵌类型的所有成员 | 只能在包含它的程序集和派生类型的任何代码中访问该项 |
private protected | 类型和内嵌类型的所有成员 | C# 7.2新增的修饰符,只允许访问同一程序集中的派生类型,而不允许访问其他程序集中的派生类型 |
public、protected、private是逻辑访问修饰符。internal是一个物理访问修饰符,其边界是一个程序集。 |
接口
接口是约束指定派生类的一组行为。
- 接口成员不能有任何实现
- 接口只能包含方法、属性、索引器和事件的声明
- 接口不能实例化,只能包含成员的签名
- 接口总是抽象的,所以接口不需要abstract关键字
- 接口既不能有构造函数,也不能有字段
- 接口成员不能带有修饰符
- 接口成员总是隐式为public,不能声明为virtual
定义和实现接口:
接口的命名通常以字母I开头,以便知道这是一个接口
1 public interface IAction 2 { 3 void Run(); 4 } 5 6 public class Action : IAction 7 { 8 public void Run() 9 { 10 11 } 12 }
声明接口类型的变量
1 IAction action = new Action();
派生的接口:
1 public interface IBehavior 2 { 3 void Cry(); 4 } 5 6 public interface IAction : IBehavior 7 { 8 void Run(); 9 } 10 11 public class Action : IAction 12 { 13 public void Run() 14 { 15 throw new NotImplementedException(); 16 } 17 18 public void Cry() 19 { 20 throw new NotImplementedException(); 21 } 22 }
is和as运算符
as:
1 public void HandleAction(Action action) 2 { 3 4 } 5 6 IAction action = new Action(); 7 HandleAction(action as Action);
is:
1 public void HandleAction(object o) 2 { 3 if (o is IAction action) 4 { 5 6 } 7 } 8 9 IAction action = new Action(); 10 HandleAction(action);