题外话:因为类六有关索引器的内容比较复杂,我想精心准备出经典实例和通俗易懂的讲解内容再发表博客,所以我先跳过索引器,接着学习继承。
本节课我们来学习一下类的继承,在面向对象二 中我已经讲述了继承的概念、优点,但是没有举出具体的代码实例讲解,在这篇博客中,不会再次细致的讲解继承的原理,建议同学们最好是先阅读面向对象二系列后,再学这篇博客,我们现在来回忆一下继承的概念。
继承
继承是面向对象理论中最重要的一项机制,通过继承一个现有的类,新创建的类可以不需要再写任何代码,就可以按照继承来的父类中的合适的访问权限直接拥有所继承的类的功能,同时还可以创建自己的专门功能。 就向我在面向对象二中提到的,继承的优点就是:当类与类中有共同的类成员时,完全可以提炼出共同的类成员,单独的编写一个类,让其他的只要想包含这个类中共同的类成员的类来继承我们单独编写的类,使得代码可以共享,避免重复。
单独编写的类我们习惯的称为父类,或基类,而继承这个父类的类我们称为子类或派生类。要想使用继承,就必须注意到,当两个类A和B之间具备“B is A”就关系时,就可以用到继承,那么B就可以说是A的子类或派生类,A是B的父类或基类。我们不能把猴子和人看成存在继承关系的两个类,但是如果现在还有一个动物类,那么猴子是动物,人也是动物,就可以把动物作为人和猴子的父类。
于此同时,当我们要修改类中共同的成员时,只需要修改父类中的共同类成员,就能达到修改继承这个父类的子类们的目的,而如果我们想单独的修改子类中的类成员,是不会影响到父类的,更不会影响到从父类中继承的其他子类们,这说明了继承关系只会向下传递 ,子类本身的调整并不会影响基类。
请同学们注意,我说的子类们,说明一个父类可以有多个子类,但是一个子类只可以继承一个父类(和多个接口,接口我会在下一篇博客中讲解),以前听课的时候,每讲到一个子类只能有一个父类时,也就是说一个儿子只能有一个爸爸时,学生们就会捣乱,所以我也要养成一种这样描述的习惯:就是“一个儿子类只能有一个含DNA的亲生父亲类”,相信这样说应该不会有什么疏漏了,DNA就是他们共同的类成员,当然这个DNA也不可能完成一致,否则就是一个人了,也就没有我们创建子类的目的了,在程序继承中我们把从父类中继承下来的用非private修饰,父亲所独有的用private修饰,同时就算儿子从父亲那继承下了的,我们也可以重新的再次赋值。
下面我们来看看继承的基本语法:
class SonClassName:BaseClassName
{
}
其中的SonClassName为新创建的子类,BaseClassName为基类,符号“:”定义了两个类的继承关系,也就是SonClassName类继承自BaseClassName类,通过继承,子类可以访问到基类中的不法成员,基类可以借助使用访问修饰符来有条件的开放基类的功能,供派生类继承。SonClassName类包含了BaseClassName类中所有非private修饰的类成员,这里所提到的private指的是访问修饰符,因为访问修饰符 直接关系到继承结构的限制,虽然面向对象二中也有具体的讲解,这里我们还是再重温一下:
public修饰符修饰的A类类成员,所有的非A类的类的方法中,都可以通过A类的类名(静态的A类成员)或A类的对象a(实例的A类成员)来访问。
protected修饰符修饰的A类类成员,在所有的A类的子类的方法中,都可以直接的访问到,不需要引用过程,而在所有的非A类的类的方法中,都无法访问。
private修饰符修饰的A类类成员,在所有的A类的类成员中,都可以直接的访问到(除嵌套类),嵌套类可以通过A类的类名(静态的A类成员)或A类的对象a(实例的A类成员)来访问,而在所有的非A类的类的方法中,都无法访问。
可以看出,修饰符的权限逐步缩小,在编写继承类代码时,如果父类中的成员,不需要外部类来访问,为了包含父类的成员,尽量避免public修饰符的乱用,而是应该采用protected修饰符,会更加的安全、合理。
我们来看下面的实例,实例中包括四个类,一个包括入口方法的Program类,一个GFClass、一个FClass、一个SClass。在Program类的Main方法中执行对SClass的实例化,创建son对象,我们来看看继承的访问权限,特别是在创建son对象时构造方法的方法继承顺序。
继承实例
1 namespace hello 2 { 3 // 创建一个爷爷类GFClass 4 class GFClass 5 { 6 // 定义一个私有成员MyMoney,私有字段是无法被继承的 7 int MyMoney = 5 ; 8 9 // 定义两个受保护的字段,受保护的字段允许子类访问到 10 protected string GFhouse = " 一套GFHouse房子 " ; 11 protected int GFMoney = 10 ; 12 13 // 定义爷爷类的构造函数,这样在创建爸爸类和儿子类的对象时,都会先执行所以父类以上的构造方法 14 public GFClass() 15 { 16 // 显示出私有字段的值 17 Console.WriteLine( " 我是爷爷!我自己留点养老钱:{0}万,这是我自己的钱,私有财产呀! " , MyMoney); 18 Console.WriteLine(); 19 } 20 21 // 定义一个爷爷类中成员爷爷说的方法,注意它是public的。 22 public void GFSay() 23 { 24 // 显示出爷爷类中的成员字段的值。 25 Console.WriteLine( " 我是爷爷!我给你们留下:{0},还留下:{1}万. " , GFhouse, GFMoney); 26 } 27 } 28 29 // 创建父亲类,继承爷爷类 30 class FClass : GFClass 31 { 32 // 定义一个静态的公有的字段,目的是在Main方法中直接用父亲类的类名引用出字段的值 33 public static int MyMoney = 5 ; 34 35 // 定义两个受保护的字段,受保护的字段允许子类访问到 36 // 注意Fhouse字段,在构造函数中,重新将继承的爷爷类的中的GFhouse字段值,赋给了Fhouse字段。 37 // 说明GFhouse可以直接的访问到。 38 protected string Fhouse = " 无房子 " ; 39 protected int FMoney = 15 ; 40 41 // 定义父亲类的构造函数,这样在创建儿子类的对象时,都会先执行所以父类以上的构造方法 42 public FClass() 43 { 44 // 因为父亲没有房子,住的是爷爷的房子,就采用了Fhouse的值继承GFhouse的值的方式传递下去, 45 // 放到了构造函数中,目的是只要创建父亲类或儿子类的对象时,继承房子的事件就一定会发生。 46 Fhouse = GFhouse; 47 48 // 显示出私有字段的值 49 Console.WriteLine( " 我是爸爸,我没有房子!继承了你爷爷的房子。我把财产拿出:{0}万,做慈善,用我的名字去取。 " , MyMoney); 50 Console.WriteLine(); 51 } 52 53 // 定义一个爸爸类中成员爸爸说的方法,注意它是public的 54 public void FSay() 55 { 56 Console.WriteLine(" 我是爸爸,留下:{0}万,你们爷爷留下来的钱我也没动。 " , FMoney); 57 Console.WriteLine(); 58 } 59 } 60 61 // 定义一个儿子类,继承爸爸类,此处注意不能再继承爷爷类,儿子类中继承了所有父亲类中的非private成员,爷爷类中public成员 62 class SClass : FClass 63 { 64 // // 定义一个儿子类中成员儿子说的方法,注意它是public的 65 public void SSay() 66 { 67 // 可以直接访问到爷爷说的方法,因为是public修饰的 68 Console.Write( " 爷爷对我说: " ); 69 GFSay(); 70 Console.WriteLine(); 71 // 可以直接访问到爸爸说的方法,因为是public修饰的 72 Console.Write( " 爸爸对我说: " ); 73 FSay(); 74 Console.WriteLine(); 75 76 // 可以直接访问到爸爸的受保护字段Fhouse,因为有继承爸爸类的构造函数, 77 // 此时爸爸类中的Fhouse的值不再是“无房子”,而是“一套GFhouse房子” 78 Console.WriteLine( " 我现在住的是,爸爸继承下来的: " + Fhouse); 79 80 // 打印出儿子可以用的爷爷留下的钱和爸爸留下的钱 81 Console.WriteLine( " 我现在一共用的遗产:{0}万 " , FMoney + GFMoney); 82 } 83 } 84 class Program 85 { 86 static void Main( string [] args) 87 { 88 // 创建一个儿子类的实例son 89 SClass son = new SClass(); 90 91 // 用对象son调用儿子说的方法 92 son.SSay(); 93 Console.WriteLine(); 94 95 // 因为父亲类中的MyMoney字段是静态的,所以使用父亲类的类名引用 96 Console.WriteLine( " 我要把捐出爸爸留下的慈善金{0}万 " ,FClass.MyMoney); 97 } 98 } 99 100 }
运行结果
我是爷爷!我自己留点养老钱:5万,这是我自己的钱,私有财产呀!
我是爸爸,我没有房子!继承了你爷爷的房子。我把财产拿出:5万,做慈善,用我的名字去取。
爷爷对我说:我是爷爷!我给你们留下:一套GFHouse房子,还留下:10万.
爸爸对我说:我是爸爸,留下:15万,你们爷爷留下来的钱我也没动。
我现在住的是,爸爸继承下来的:一套GFHouse房子 我现在一共用的遗产:25万
我要把捐出爸爸留下的慈善金5万
通过这个实例的注解,希望大家能够明确的体会到访问权限的作用,不知道大家能不能发现一点,就是第81行中能够访问到爷爷类中的protected成员GFMoney,这就说明了protected也是允许隔代继承的。
同学们可以在课下试试以下两个操作:一,在GFClass中和FClass中定义的MyMoney是在其子类中访问不到的,因为他们是private修饰的(在类中缺省修饰符的字段都是private修饰的);二,在Main方法中,无法访问到GFClass中和FClass中定义的protected和private修饰的字段,验证一下访问修饰符的权限作用。
构造方法的继承
在这个实例中用到了构造方法的继承,在继承关系中,构造方法不同于一般的方法成员,基类和派生类的构造方法都是分别独立的,在类四 中我们讲到了在创建A类对象a时,编译器首先要做的就是对所有A类的基类以上的构造方法先执行一次,然后在执行自己的构造方法。 在类四中,我只是举了基类中的构造器都是无参时的情况,其实继承中构造方法的继承是很复杂的,构造函数的继承是很多企业面试题当中的重要考点,下面我们来通过实例来总结一下构造方法的继承原则:
如果在子类的构造方法中,没有任何的指定的继承父类的哪个构造方法时,C#会默认的调用没有参数的构造方法,同时我们要知道这样一个机制,就是当一个类中规定了有参的构造函数时,无参的构造函数编译器是不会再默认的创建了,所以为了避免子类中出现无参的构造方法,我们最好是显式的调用基类的构造方法,必须最好养成在创建父类时,及时父类的无参构造函数无作用,也应该把它写上去,避免子类在创建对象时,出现构造方法的向上继承出现错误。
显式的调用基类的构造方法使用base关键字,如
public SonClass(int i,int j):base ([i,j])
{
}
其中base指的就是基类,base()指的就是基类的构造方法,当然,“()”中代表的是参数。使用构造方法继承时要注意两个原则:一、子类中构造方法有两个参数,那么它有三种继承父类构造方法的方式:无参、一个参数、二个参数,当然父类必须有这3种;二、如果父类中只有一个无参构造方法的,而无其他构造方法时,那么子类中必须也构造一个无参的构造方法,这时这个有两个参数的构造方法就只有会有一种父类的无参构造方法的继承。
下面我们来看这个例子体会我上面所说的两个原则:
构造方法的base继承
1 // 定义一个父类F 2 class F 3 { 4 // 定义F中的无参构造方法 5 public F() 6 { 7 Console.WriteLine(" 如果子类中的构造器没有明确的使用:base([…]),创建实例时将会出现我无参构造器! " ); 8 } 9 // 定义F中的有一个参构造方法 10 public F( string i) 11 { 12 Console.WriteLine(" 想在创建实例中出现我,必须在子类的构造方法后使用:base(i) " ); 13 }14 // 定义F中的有两个参构造方法 15 public F( string i, string j) 16 { 17 Console.WriteLine(" 子类中没有使用:base(i,j)来继承我这个构造方法,所以创建s2不会出现我这句话 " ); 18 }19 }20 // 定义一个子类S,继承父类F 21 class S:F 22 { 23 // 定义F中的无参构造方法 24 // 如果子类中定义了无参构造器,那么父类中一定也要定义无参构造器, 25 // 因为如果父类中有有参构造器后,编译器默认的无参构造器就会失去. 26 public S() 27 { 28 Console.WriteLine(" 有我就必须规定父类的无参构造器 " ); 29 }30 31 // 定义S中的有一个参构造方法,没有继承用父类中的有参构造。 32 public S( string i) 33 { 34 Console.WriteLine(i);35 }36 37 // 定义了两个参数的子类构造方法,继承了一个参数的构造方法 38 // 此处说明,子类可以根据base(参数)中参数的个数,调用父类中的相应的构造器 39 // 如果缺省的时候,就会调用无参的构造器。 40 public S( string i, string j): base (i) 41 { 42 Console.WriteLine(" 我没有使用:base(i,j)来继承父类两参构造方法,继承一个参数的构造方法,所以创建s2时只会出现调用父类的有一个参构造方法 " ); 43 }44 45 }46 class Program 47 { 48 static void Main( string [] args) 49 { 50 // 会调用两个无参的构造方法 51 S s = new S(); 52 Console.WriteLine(" ------- " ); 53 54 // 因为子类的一参构造方法没有直接调用父类中的一参构造方法,所以结果应该是调用父无参和子一参 55 S s1 = new S( " 我是s1,调用了父类无参构造器和本类中使用一参构造器 " ); 56 Console.WriteLine(" ------- " ); 57 58 // 因为子类的两参构造方法直接调用父类中的一参构造方法,所以结果应该是调用父一参和子两参 59 S s2 = new S( " 使用两参构造器 " , " 注意父类的出现的构造方法 " ); 60 Console.WriteLine(" ------- " ); 61 }62 }
结果如下:
如果子类中的构造器没有明确的使用:base([…]),创建实例时将会出现我无参构造器!
有我就必须规定父类的无参构造器 ------- 如果子类中的构造器没有明确的使用:base([…]),创建实例时将会出现我无参构造器!
我是s1,调用了父类无参构造器和本类中使用一参构造器 ------- 想在创建实例中出现我,必须在子类的构造方法后使用:base(i) 我没有使用:base(i,j)来继承父类两参构造方法,继承一个参数的构造方法,所以创建s 2时只会出现调用父类的有一个参构造方法 ------- 请按任意键继续. . .
看了上面的例子,相信大家应该会使用构造方法的继承了,使用过程一定要注意那两个原则。
Object类
在c#中继承扮演了很重要的角色,所有的类至少都会继承一个基类Object类,当一个类没有明确的注明是继承那个类时,它自身是继承Object类的,这时同学们就会应该考虑这样一个问题,为什么我们在创建一个没有指明继承哪个类的对象时,对象中会不会包含Object类的方法呢?
答案是一定的,我们来看上一个实例,在S类和F父类中,除了构造方法没有定义任何类成员,在我们创建s时,点的时候也就是引用s的成员时,会出现四个成员:
S类因为继承了F类,就无法再继承其他类了,怎么会用这四个Object类的成员,说明了什么呢?当S的父类F类因为没有明确的继承类,就继承了Object类,F中包括了这四个类,所以S类中也包括了Object类的成员。我们可以这样说Object类是任何类的基类,即使你的有自己的父类,也的能够用到Object类的成员的,均会继承Object类的所有基本功能。
Object类位于类库的System命名空间下,本身提供了下面六个方法成员:
Equals:用来判断调用此方法的对象与指定的对象是否相同,返回True和False来代表相同和不同。
GetHashCode:特定类的哈希函数,用于哈希演算与类似哈希表的数据表结构。
GetType:取得目前实例对象的类
ReferenceEquals:比较指定的对象是否引用到相同的实例。
ToString:转换为字符串的方法。
Finalize:在垃圾回收机制开始执行前,给对象提供进行对象资源释放或是清空操作的机会。
我用下面的例子来讲述其中3种常见用法:
Object类的方法实例
1 // 定义一个F类 2 class F 3 { 4 // 定义一个公有的i字段 5 public int i; 6 } 7 class Program 8 { 9 static void Main( string [] args) 10 { 11 // 创建F类的两个对象f和f1 12 F f = new F(); 13 F f1 = new F(); 14 // 使用Equals方法,判断f是否是f1 15 Console.WriteLine( " 对象f是否是对象f1: " + f.Equals(f1)); 16 Console.WriteLine();17 18 // 定义一个Object类型的type变量使用GetType方法接收i的数据类型,打印出来 19 object type = f.i.GetType(); 20 Console.WriteLine(" i的类型是 " + type); 21 Console.WriteLine();22 23 // 使用.ToString()方法将i转换为字符串类型,再用GetType方法接收i的数据类型,打印出来 24 Console.WriteLine( " i的类型是 " + f.i.ToString().GetType()); 25 }26 }27
结果如下:
对象f是否是对象f1:False
i的类型是System.Int32
i的类型是System.String
实际上这六种方法是public的修饰的虚方法,关于虚方法的定义,我们下节课类八继承中的多态性:方法重写。我们将讲解到。