C#面向对象 之 继承

R | | 访问(49)

  案例代码:

/// <summary>
/// 动物
/// </summary>
public abstract class Animal
{
	public Animal()
	{
		Console.WriteLine("Animal:Init");
	}

	public abstract void ShowType();

	public void Eat()
	{
		Console.WriteLine("Animal always eat.");
	}
}

/// <summary>
/// 鸟
/// </summary>
public class Bird : Animal
{
	public Bird()
	{
		Console.WriteLine("Bird:Init");
	}

	public string type = "Bird";

	public override void ShowType()
	{
		Console.WriteLine("Type is {0}", type);
	}

	public void A()
	{
		Console.WriteLine("Bird:A");
	}

	private string color;
	public string Color
	{
		get { return color; }
		set { color = value; }
	}
}

/// <summary>
/// 鸡
/// </summary>
public class Chicken : Bird
{
	public Chicken()
	{
		Console.WriteLine("Chicken:Init");
	}

	public new string type = "Chicken";

	public override void ShowType()
	{
		Console.WriteLine("Type is {0}", type);
	}

	public new void A()
	{
		Console.WriteLine("Chicken:A");
	}

	public void ShowColor()
	{
		Console.WriteLine("Color is {0}", Color);
	}
}

  继承

  通过继承,我们轻而易举地实现了代码的复用和扩展,同时通过重载(overload)、重写(override)、接口实现等方式实现了封装变化,隐藏私有信息等面向对象的基本规则。

  通过继承,轻易地实现了子类对父类共性的继承,例如,Animal类中实现了方法Eat(),那么它的所有子类就都具有了Eat()特性。

  同时,子类也可以实现对基类的扩展和改写,主要有两种方式:

  (1)通过在子类中添加新方法,例如Bird类中就添加了新方法ShowColor用于现实鸟类的毛色;

  (2)通过对父类方法的重写(override),例如Eagle类中的ShowColor()方法。

  继承就是代码的重用,父类有的,子类也可以使用:

  继承,就是面向对象中类与类之间的一种关系。继承的类称为子类、派生类,而被继承类称为父类、基类或超类。通过继承,使得子类具有父类的属性和方法,同时子类也可以通过加入新的属性和方法或者修改父类的属性和方法建立新的类层次。继承机制体现了面向对象技术中的复用性、扩展性和安全性。为面向对象软件开发与模块化软件架构提供了最基本的技术基础。

  继承按照其实现方式的不同,一般分类如下:

  实现继承:派生类继承了基类的所有属性和方法,并且只能有一个基类,在.NET中System.Object是所有类型的最终基类,这种继承方式称为实现继承。

  接口继承:派生类继承了接口的方法签名。不同于实现继承的是,接口继承允许多继承,同时派生类只继承了方法签名而没有方法实现,具体的实现必须在派生类中完成。因此,确切地说,这种继承方式应该称为接口实现。

  代码解读:

  1、我们简要地分析一下对象的创建过程:

  Bird bird = new Bird();

  Bird bird创建的是一个Bird类型的引用,而new Bird()完成的是创建Bird对象,分配内存空间和初始化操作,然后将这个对象引用赋给bird变量,也就是建立bird变量与Bird对象的关联。

  2、继承的本质正体现于对象的创建过程中:

  在此我们以Chicken对象的创建为例,首先是字段,对象一经创建,会首先找到其父类Bird,并为其字段分配存储空间,而Bird也会继续找到其父类Animal,为其分配存储空间,依次类推直到递归结束,也就是完成System.Object内存分配为止。我们可以在编译器中用单步执行的方法来大致了解其分配的过程和顺序,因此,对象的创建过程是按照顺序完成了对整个父类及其本身字段的内存创建,并且字段的存储顺序是由上到下排列,最高层类的字段排在最前面。其原因是如果父类和子类出现了同名字段,则在子类对象创建时,编译器会自动认为这是两个不同的字段而加以区别。

  然后,是方法表的创建,必须明确的一点是方法表的创建是类第一次加载到AppDomain时完成的,在对象创建时只是将其附加成员TypeHandle指向方法列表在Loader Heap上的地址,将对象与其动态方法列表相关联起来,因此方法表是先于对象而存在的。类似于字段的创建过程,方法表的创建也是父类在先子类在后,原因是显而易见的,类Chicken生成方法列表时,首先将Bird的所有虚方法复制一份,然后和Chicken本身的方法列表做对比,如果有重写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而创建完成Chicken的方法列表。这种创建过程也是逐层递归到Object类,并且方法列表中也是按照顺序排列的,父类在前子类在后,其原因和字段大同小异。

  不言而喻,任何类型方法表中,开始的4个方法总是继承自System.Object类型的虚方法,它们是:ToString、Equals、GetHashCode和Finalize。

  继承思考:

  (1)、继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时可以添加新方法。

  (2)、子类可以调用父类方法和字段,而父类不能调用子类方法和字段。

  (3)、虚方法如何实现重写操作,使得父类指针可以指向子类对象成员。

  (4)、子类不光继承父类的公有成员,同时继承了父类的私有成员,只是在子类中不被访问。

  (5)、new关键字在虚方法继承

  3、分析下面代码执行的结果:

Bird bird2 = new Chicken();
Console.WriteLine(bird2.type);// 调试时,字段会 先执行子类的,在执行父类的,执行了两处,最终返回父类的值
bird2.ShowType();
bird2.A();// 调试时,直接执行父类的

  Bird bird2 = new Chicken();

  这种情况下,bird2.ShowType应该返回什么值呢?而bird2.type又该是什么值呢?

  关注对象原则:

  调用子类还是父类的方法,取决于创建的对象是子类对象还是父类对象,而不是它的引用类型。例如Bird bird2 = new Chicken()时,我们关注的是其创建对象为Chicken类型,因此子类将继承父类的字段和方法,或者重写父类的虚方法,而不用关注bird2的引用类型是否为Bird。引用类型的区别决定了不同的对象在方法表中不同的访问权限。

  执行就近原则:

  对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它创建最近的字段或者方法,例如上例中的bird2,是Bird类型,因此会首先访问Bird类的type,如果type类型设为public,则在此将返回“Bird”值。这也就是为什么在对象创建时必须将字段按顺序排列,而父类要先于子类编译的原因了。

  思考:

  (1)上面我们分析到bird2.type的值是“Bird”,那么bird2.ShowType()会显示什么值呢? 答案是“Type isChicken”,根据上面的分析,想想到底为什么?

  (2)关于new关键字在虚方法动态调用中的阻断作用,也有了更明确的理论基础。在子类方法中,如果标记new关键字,则意味着隐藏基类实现,其实就是创建了与父类同名的另一个方法,在编译中这两个方法处于动态方法表的不同地址位置,父类方法排在前面,子类方法排在后面。

Chicken chicken = new Chicken();
Console.WriteLine(chicken.type);
chicken.ShowType();
chicken.A();

  Bird bird2 = new Chicken();

  Chicken chicken = new Chicken();

  根据上文的分析,bird2对象和chicken对象在内存布局上是一样的,差别就在于其引用指针的类型不同:bird2为Bird类型指针,而chicken为Chicken类型指针。以方法调用为例,不同的类型指针在虚拟方法表中有不同的附加信息作为标志来区别其访问的地址区域,称为offset。不同类型的指针只能在其特定地址区域内执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不同的类型访问具有不同的访问权限问题。

  4、实现继承与接口继承实现继承

  通常情况下表现为对抽象类的继承,而其与接口继承在规则上有以下几点归纳:

  (1)抽象类适合于有族层概念的类间关系,而接口最适合为不同的类提供通用功能。

  (2)接口着重于CAN-DO关系类型,而抽象类则偏重于IS-A式的关系。

  (3)接口多定义对象的行为;抽象类多定义对象的属性。

  (4)如果预计会出现版本问题,可以创建“抽象类”。例如,创建了狗(Dog)、鸡(Chicken)和鸭(Duck),那么应该考虑抽象出动物(Animal)来应对以后可能出现马和牛的事情。而向接口中添加新成员则会强制要求修改所有派生类,并重新编译,所以版本式的问题最好以抽象类来实现。

  (5)因为值类型是密封的,所以只能实现接口,而不能继承类。

  5、类与类的关系

  (1)继承

  Class2继承自Class1,任何对基类Class1的更改都有可能影响到子类Class2,继承关系的耦合度较高。

  (2)聚合

  聚合分为三种类型,其耦合度逐级递增。

  无聚合:无聚合类型关系,类的双方彼此不受影响;

  共享聚合:共享型关系,Class2不需要对Class1负责;

  复合:Class1会受控于Class2的更改,因此耦合度更高;

  总之,聚合关系是一种HAS-A式的关系,耦合度没有继承关系高。

  (3)依赖

  依赖关系表明,如果Class2被修改,则Class1会受到影响。

  通过上述三类关系的比较,我们知道类与类之间的关系,通常以耦合度来描述,也就是表示类与类之间的依赖关系程度。没有耦合关系的系统是根本不存在的,因为类与类、模块与模块、系统与系统之间或多或少要发生相互交互,设计应力求将类与类之间的耦合关系降到最低。而面向对象的基本原则之一就是实现低耦合、高内聚的耦合关系。

  将耦合的概念应用到继承机制上,通常情况下子类都会对父类产生紧密的耦合,对基类的修改往往会对子类产生一系列的不良反应。继承之毒瘤主要体现在:

  (1)继承可能造成子类的无限膨胀,不利于类体系的维护和安全。

  (2)继承的子类对象确定于编译期,无法满足需要运行期才确定的情况,而类聚合很好地解决了这一问题。

  (3)随着继承层次的复杂化和子类的多样化,不可避免地会出现对父类的无效继承或者有害继承。子类部分的继承父类的方法或者属性,更能适应实际的设计需求。

  那么,通过上面的分析,我们深知继承机制在满足更加柔性的需求方面有一些弊端,从而可能造成系统设计的漏洞与失衡。解决问题的办法当然是多种多样的,根据不同的需求进行不同的设计变更,例如将对象与行为分离抽象出接口实现来避免大基类设计,以聚合代替继承实现更柔性的子类需求等。

  面向对象的基本原则:

  多聚合,少继承。

  低耦合,高内聚。

  6、Adapter模式(适配器)

  聚合与继承通常体现在设计模式的伟大思想中,在此以Adapter模式的两种方式为例来比较继承和聚合的适应场合与柔性较量。首先对Adapter模式进行简单的介绍。Adapter模式主要用于将一个类的接口转换为另外一个接口,通常情况下在不改变原有体系的条件下应对新的需求变化,通过引入新的适配器类来完成对既存体系的扩展和改造。Adapter模式就其实现方式主要包括:

  (1)类的Adapter模式。通过引入新的类型来继承原有类型,同时实现新加入的接口方法。其缺点是耦合度高,需要引入过多的新类型。

  (2)对象的Adapter模式。通过聚合而非继承的方式来实现对原有系统的扩展,松散耦合,较少的新类型。

/// <summary>
/// 鸣叫 接口
/// </summary>
public interface ITweet
{
	/// <summary>
	/// 鸣叫
	/// </summary>
	void ToTweet();
}

#region 类的Adapter模式
/// <summary>
/// 鹰,类的适配器
/// </summary>
public class EagleAdapter : Eagle, ITweet
{
	public void ToTweet()
	{
		Console.WriteLine("Eagle:Tweet");
	}
}

/// <summary>
/// 鸡,类的适配器
/// </summary>
public class ChickenAdapter : Chicken, ITweet
{
	public void ToTweet()
	{
		Console.WriteLine("Chicken:Tweet");
	}
}
#endregion

#region 对象的Adapter模式

/// <summary>
/// 以聚合的方式实现鸟类的适配器,将鸟拥有的方法都重新写一次,并用初始化来的_bird调用,保住了原始方法不被改动,在这个基础上实现鸣叫接口的鸣叫方法
/// 新的BirdAdapter类与Bird类型之间只有松散的耦合关系而不是紧耦合。
/// </summary>
public class BirdAdapter : ITweet
{
	private Bird _bird;
	public BirdAdapter(Bird bird)
	{
		_bird = bird;
	}

	public void ShowType()
	{
		_bird.ShowType();
	}

	public void A()
	{
		_bird.A();
	}

	public void ShowColor()
	{
		Console.WriteLine("Color is {0}", _bird.Color);
	}

	public void ToTweet()
	{
		Console.WriteLine($"{_bird.GetType().Name}:Tweet");
	}

}
#endregion

  运行代码:

// 类的Adapter模式:为鸟儿加上鸣叫ToTweet这一行为
// 问题:如果有成千上万的鸟儿都要鸣叫,怎么办呢?以目前的实现方式我们不得不为每个继承自Bird类的子类提供相应的适配类,这样太累了
EagleAdapter ea = new EagleAdapter();
ea.ToTweet();

ChickenAdapter ca = new ChickenAdapter();
ca.ToTweet();

// 对象的Adapter模式:为鸟儿加上鸣叫ToTweet这一行为
BirdAdapter bae = new BirdAdapter(new Eagle());
bae.ToTweet();

BirdAdapter bac = new BirdAdapter(new Chicken());
bac.ToTweet();

  归纳总结:

  (1)密封类不可以被继承。

  (2)继承关系中,我们更多的是关注其共性而不是特性,因为共性是层次复用的基础,而特性是系统扩展的基点。

  (3)实现单继承,接口多继承。

  (4)从宏观来看,继承多关注于共通性;而多态多着眼于差异性。

  (5)继承的层次应该有所控制,否则类型之间的关系维护会消耗更多的精力。

  (6)面向对象原则:多组合,少继承;低耦合,高内聚。