1.什么是继承
在程序设计中,继承的问题就是分类的问题一一继承反映了 类和类的关系。例如,我们学过生物,知道马和鲸都属于哺乳动物。这两种动物具有哺乳动物的共性(都能呼吸空气,都能哺乳,都是温血....但是,两者还有自己的个性(马有蹄子,鲸有鳍状肢和尾片)。
那么,如何在程序中对马和鲸进行建模?一个办法是创建两个不同的类,一个叫Horse(马),另一个叫Whale(鲸).每个类都可以实现那种哺乳动物特有的行为,例如为Horse实现Trot(小跑),为Whale类实现Swim(游泳)。那么,如何处理马和鲸通用的行为呢?例如,Breathe(呼吸)和SuckleYoung(哺乳)是哺乳动物的共性。可在刚才两个类中添加具有上述名称的重复的方法,但这无疑会使维护成为盟梦,尤其是考虑到以后可能还要建模其他类型的哺乳动物,例如Human(人)和Aardvark(土豚)等。
在C#中,可以通过类的继承来解决这些问题。马、 鯨、人和土豚都属于Mammal(哺乳动物)类型,所以可以创建名为Mammal的类,用它对所有哺乳动物的共性进行建模。然后,声明Horse, Whale, Human 和Aardvark等类都从Mammal类继承。继承的类自动包含Mammal类的所有功能(Breathe, SuckleYoung等),但还可以为每种具体的哺乳动物添加它独有的功能。例如,可以为Horse类声明Trot方法,为Whale类声明Swim方法。如果需要修改一个通用的方法(例如Breathe)的工作方式,那么只需要在-一个位置修改,也就是在Mammal中。
2.使用继承
语法:
class DerivedClass : BaseClass
{
…
}
声明一个Mammal类:
class Mammal
{
public void Breathe()//呼吸
{
//…
}
public void SuckleYoung()//哺乳
{
//…
}
//…
}
定义一个Horse类
class Horse : Mammal //定义Horse继承自Mammal
{
//…
public void Trot()
{
//…
}
}
class Whale : Mammal //定义Whale继承自Mammal
{
…
public void Swim()
{
//…
}
}
在程序中创建Horse对象后,可像下面这样调用Trot, Breathe和SuckleYoung方法:
Horse myHorse = new Horse();
myHorse.Trot();
myHorse . Breathe();
myHorse.SuckleYoung();
可用类似方式创建Whale对象,但这一次能调用的是Swim. Breathe 和SuckleYoung
方法。Trot 是Horse类定义的,不适用于Whale.
注意:System.object类是所有类的根。所有类都隐式派生自System.object类.所以,C#编译器会悄悄地将Mammal类重写为以下代码(如果愿意,甚至可以自己这样写):
Class Mammal : System.Object
{
//...
}
2.1调用基类构造器
除了继承得到的方法,派生类还自动包含来自基类的所有字段。创建对象时,这些字段通常需要初始化。通常用构造器执行这种初始化。
注意:所有类都至少有-一个构造器(如果你没有提供一一个,编译器会自动生成一个默认构造器)。
作为好的编程实践,派生类的构造器在执行初始化时,最好调用一下它的基类构造器。为派生类定义构造器时,可以使用base关键字调用基类构造器。下面是一个例子:
class Mammal//Mammal是基类
{
public Mammal(string name) //基类构造器
{
//…
}
…
}
class Horse : Mammal//Horse是派生类
{
public Horse(string name) : base(name) //调用Mammal(name)
{
//…
}
//…
}
提示:不在派生类构造器中显式调用基类构造器,编译器会自动插入对基类的默认构造器的调用,然后才会执行派生类构造器的代码。
注意:如果Mammal有公共默认构造器,上述代码就能成功编译。但是,并非所有类都有公共默认构造器(记住,只有在没有写任何非默认构造器的前提下,编译器才会自动生成一个默认构造器):在这种情况下,如果忘记调用正确的基类构造器,就会造成编译时错误。
2.2类的赋值
以我们上面定义的Mammal,Horse和Whale举例:
Horse myHorse = new Horse(…);
Whale myWhale = myHorse; //错误-不同类型
当然,我们可以将一种类型的对象赋给继承层次结构中较高位置的一个类的变量,例如:
Horse myHorse = new Horse(…);
Mammal myMammal = myHorse; //合法,因为Mammal是Horse的基类
其实理解起来也很简单。由于所有Horse(马)都是Mammal(哺乳动物),所以可以安全地将Horse对象赋给Mammal类型地变量。
注意:如果用Mammal变量引用一个Horse或Whale对象,就只能访问Mammal类定义的方法和字段。Horse 或Whale类定义的任何额外的方法都不能通过Mamnal类来访问:
Horse myHorse = new Horse(…);
Mammal myMammal = myHorse;
myMammal.Breathe();//这个调用合法,Breathe是Mammal类地一部分
myMammal.Trot();//这个调用非法,Trot不是Mammal类地一部分
不能直接将Mammal对象赋给Horse变量:
Mammal myMammal = new myMammal(…);
Horse myHorse = myMammal; //错误
这个也很好理解,因为并非所有地Mammal对象都是Horse,也有可能是Whale,或者其它。
所以就可以用我们之前介绍的as或者is操作符进行检查,确认Mammal是不是Horse。
2.3声明新方法(隐藏基类方法)
之前介绍方法重载的时候应该给大家介绍过方法签名,现在先来说一下方法签名的定义:
方法签名:由方法名、参数数量和参数类型共同决定,方法的返回类型不计入签名。两个同名方法如果获取相同的参数列表,就说它们有相同的签名,即使它们的返回类型不同。
如果基类和派生类声明了两个具有相同签名的方法,编译时会显示一个警告。
例如:
class Mammal
{
//…
public void Talk()//假定所有哺乳动物都能talk
{
// …
}
}
class Horse : Mammal
{
// …
public void Talk()//马的talk方式有别于其它的哺乳动物
{
//…
}
}
虽然代码能编译并运行,但应该严肃对待该警告。如果另-一个类从Horse派生,并调用Talk方法,它希望调用的可能是Mamnal类实现的Talk,但该方法被Horse中的Talk隐藏了,所以实际调用的是Horse.Talk.大多数时候,像这样的巧合会成为混乱之源。应该重命名方法以避免冲突。然而,如果确实希望两个方法具有相同的签名,从而隐藏Mammal.Talk方法,可以明确使用new关键字消除警告:
class Mammal
{
//…
public void Talk()//假定所有哺乳动物都能talk
{
//…
}
}
class Horse : Mammal
{
//…
new public void Talk()//马的talk方式有别于其它的哺乳动物
{
// …
}
}
像这样使用new关键字,隐藏仍会发生。它唯-的作用就是关闭警报。事实上,new关键字的意思是说:“ 我知道自已在干什么,不用再烦我了!“
2.4声明虚方法
有时想隐藏方法在基类中的实现。以System. Object的ToString方法为例。
ToString的作用就是将对象转换成其类型名称字符串,例如"Mammal"或"Horse". 这种转换显然没什么用处。那么,为什么要提供一一个没用的方法呢?为了理解这个问题,我们需要多加思考。
显然,ToString是一个很好的概念,所有类都应当提供一个方法将对象转换成字符串,以便进行显示或调试。现在只是实现起来有问题。事实上,根本就不应该调用由System.object定义的ToString方法,它只是一个“占位符"。正确做法是,应该在自己定义的每个类中提供自己版本的ToString 方法,重写System.object中的默认实现。System.object提供的版本只是为了预防万一,因为可能有某个类没有实现自己的ToString方法。这样-来,就可放心大胆地在所有对象上调用ToString,它肯定会返回一个有内容的字符串。
故意设计成被重写的方法称为虚(virtual)方法。“ 重写(overriding)方法"和“隐藏(hiding)方法"的区别现在已经很明显了。重写是提供同一个方法的不同实现,这些方法相互关联,因为它们旨在完成相同的任务,只是不同的类用不同的方式。但是,隐藏是指方法被替换成另一个方法,方法通常不相关,而且可能执行完全不同的任务。对方法进行重写是有用的编程概念;而如果方法被隐藏,则意味着可能发生了-一处编程错误。
虚方法用virtual关键字标记。例如,以下是System.object的ToString方法定义:
namespace System
{
class object
{
public virtual string Tostring()
{
//…
}
//…
}
//…
}
2.5声明重写方法
派生类用override关键字重写基类的虚方法,从而提供该方法的另一个实现,如下例所示:
class Horse : Mammal
{
//…
public override string Tostring()
{
//…
}
}
在派生类中,方法的新实现可用base关键字调用方法的基类版本,如下所示:
public override string ToString()
{
Base.ToString();
…
}
使用virtual和override关键字声明多态性的方法时,必须遵守以下重要规则。
(1)虚方法不能私有。这种方法目的就是通过继承向其他类公开。类似地,重写方法
不能私有,因为类不能改变它继承的方法的保护级别。但是,重写方法可用protected关键字实现所谓的“受保护"保密性,详情下面内容会介绍。
(2)虚方法和重写方法的签名必须完全一致。 必须具有相同的名称、相同的参数类型/数量。除了签名一一致,两个方法还必须返回相同的类型。
(3)只能重写虚方法。对基类的非虚方法进行重写会显示编译时错误。这个设计是合
理的,应该由基类的设计者来决定方法是否能被重写。
(4)如果派生类不用override 关键字声明方法,就不是重写基类方法,而是隐藏方法。也就是说,成为和基类方法完全无关的另-一个方法,该方法只是恰巧与基类方法同名。如前所述,这会造成编译时显示警告称该方法会隐藏继承的同名方法。前面说过,可以使用new关键字消除警告。
(5)重写方法隐式地成为虚方法,可在派生类中被重写。然而,不允许用virtual关键字将重写方法显式声明为虚方法。
2.6理解受保护的访问
public和private关键字代表两种极端的可访问性:类的公共(public)字段和方法可由每个人访问,而类的私有(private)字段 和方法只能由类自身访问。
如果只是孤立地考察一一个类,这两种极端的访问完全够用了。但是,有经验的面向对象程序员会告诉你,孤立的类解决不了复杂问题!继承是将不同的类联系到一起的重要方式,在派生类及其基类之间,明显存在一种特 别而紧密的关系。经常都要允许基类的派生类访问基类的部分成员,同时阻止不属于该继承层次结构的类访问。这时就可以使用protected(受保护)关键字标记成员。
(1)如果类A派生自类B,就能访问B的受保护成员。也就是说,在派生类A中,B
的受保护成员实际是公共的。
(2)如果类A不从类B派生,就不能访问B的受保护成员。也就是说,在A中,B的
受保护成员实际是私有的。
3.理解扩展方法
继承很强大,允许从一个类派生出另一个类来扩展类的功能。但有时为了添加新的行为,继承并不一-定是最佳方案,尤其是需要快速扩展类型,又不想影响现有代码的时候。
例如,假定要为int类型添加新功能,比如一个名为Negate的方法,它返回当前整数的相反数。我知道可以使用一元求反操作符(-)来做这件事情,但请先不要管它。为此,一个办法是定义新类型NegInt32,让它从System. Int32派生(int是System. Int32的别名),在派生类中添加Negate方法:
class NegInt32 : System.Int32//别这样写!
{
public int Negate()
{
…
}
}
NegInt32理论上应继承System. Int32类型的所有功能,并添加自己的Negate方法。但基于以下两方面的原因,这样写是行不通的。
(1)新方法只适合NegInt32类型,要把它用于现有的int变量,就必须将每个int变量的定义修改成NegInt32类型。
(2)System. Int32是结构而不是类,而结构是不能继承的。
这正是扩展方法可以大显身手的时候。
扩展方法允许添加静态方法来扩展现有的类型(无论类还是结构)。引用被扩展类型的
数据即可调用扩展方法。
扩展方法在静态类中定义,被扩展的类型必须是方法的第一个参数,而且必须附加
this关键字。下例展示了如何为int类型实现Negate扩展方法:
static class Util
{
public static int Negate(this int i)
{
return -i;
}
}
语法看起来有点奇怪,但请记住:正是由于为Negate方法的参数附加了this关键字作为前级,才表明这是一一个扩展方法:另外,this 修饰int,表明扩展的是int类型。
使用扩展方法只需让Util类进入作用城(如有必要,添加一个using语句,指定Util类所在的命名空间),然后就可以简单地使用点记号法来引用方法,如下所示:
int x = 591;
Console.WriteLine($"x.Negate {x.Negate()}");
参考书籍:《Visual C#从入门到精通》