今天我们来学习抽象类和接口,因为他们在很多方面有相似的用法,所以放到一堂课中讲解,看到这个标题的时候,一定有的同学会说接口,没类字呀,它也是类吗?从本质上接口也是类,和抽象类一样,接口也是一种特殊的类。
作为编程设计者在程序的开发中,设计抽象类和接口就是为了让他们用来被继承的,当一个项目经过需求分析后,一般正规的软件开发公司会由项目经理或经验丰富的开发人员,先根据需求和要实现的大部分功能来设计抽象类和接口,然后再将项目功能分发到开发人员或小组,具体来实现这些抽象类和接口,这样做的好处就是,规范了成员的命名和参数类型,合并代码时减少错误同时方便修改,代码的可读性更强,充分利用面向对象中的三大特性,今后大家在开发时团队协作是避免不了的,所以定好各成员的名字和功能是非常重要的,VS中也提供了一个按成员名称查找其运用位置的功能,如果我们的命名各有不同,可以想象一下,后期负责合并的人员将多么的痛苦,而且项目运行后的维护,也将成为一个困难。
上面说的是定义抽象类和接口的一个重要的优点,当然很多人在最初开发时,无法灵活的设计出他们,也不知道在这个项目的什么功能该设计抽象类的什么成员,这就得靠日积月累的开发经验来做基础,同时做完一个项目后,要学会总结,有可能的话,应再次优化,将重复的功能或定义的代码使用到继承性和多态性,只有这样才能不断的进步,是不是有点像毛主席修改自己的著作的感觉,这样才能成功。在学习的最开始,同学们只要能了解他们的定义和实现就可以了。
抽象类
在类五 中我介绍过抽象类,提到过抽象类的存在是因为成员中有可能存在抽象成员,抽象成员就是只有定义而没有具体实现的成员,如果一个类中含有了抽象成员,那么这个类一定要定义成抽象类,抽象类或抽象成员都是使用abstract作为修饰符声明的定义。那么什么是抽象成员呢?没有实现又是什么意思呢?
举这样一个例子,比如人类吃饭是个方法,但是因为全世界的人类太多,吃饭使用的工具也不一样,所以我们可以把吃饭定义成一个抽象的方法,让继承人类的子类去自定义吃饭的动作,如果是中国人就用筷子吃,如果是西方人就用刀叉吃;再比如还是人类吃饭,因为根据信仰和习俗的不同,我们在定义人类时,就应该把人类所吃的食物定义成一个抽象属性,当是继承的人们是伊斯兰教时,食物中要排除猪肉,当是佛教时,食物中要排除荤食等,当然了我们所定义的人类就应该也是抽象类了。
咦!继承的人类的人们自己定义怎么吃饭、吃什么食物,让我们回想起类八 中当子类继承父类时,用自己的定义来实现父类的成员,这不会就是重写吧?答案是对,这就是重写,我们写一个抽象方法的定义和抽象属性的定义,让我们来看看具体的语法:
//抽象方法没有方法体,只有定义,使用非private关键字修饰
abstract public/protected void Method([,]);
//抽象属性get和set访问器没有定义,其实get和set访问器也是二个方法,也可以说抽象属性的get、set访问器没有方法体。有关属性 同学们可以到类二中温习一下。
abstract public int I {get;set;}
abstract可以修饰类、方法、属性、索引器及事件,也就是抽象类中可以包含五种成员:抽象嵌套类、抽象方法、抽象属性、抽象索引器、抽象事件,事件将来会讲解,其他四种成员我们都有讲解,抽象就是只有给他们做定义,如返回值、参数个数、成员标识符、访问修饰符等,具体这些成员要做什么在抽象成员中没有定义,现在我们来想一下,为什么抽象成员中没有常见的字段、构造器等?回想一下,我们在学习构造方法时,说过一个字段即使你不赋初值,在构造的过程时,编译器也会给字段赋一个初值,静态字段调用静态构造方法方法,实例字段调用实例构造方法,而且构造方法是默认存在的,而且有自己的定义,字段会有值、构造方法被默认定义,他们都有实现,所以这两个不能定义为抽象成员,析构方法也是如此,还有以后将要学习到的委托,都不能定义成抽象成员。
这时一定又有同学问到,上节课我们不是学到了重写父类成员时,必须把这些成员规定为虚方法吗?必须使用virtual关键字吗?对你记得没错,其实没有实现的成员就可以认为是虚成员,只是这时的虚成员使用了abstract来修饰,成为抽象成员。
抽象类无法实例化,也就是无法创建抽象类的对象,原因是抽象类中有可能存在抽象成员,抽象成员没有设定所完成的功能,所以无法创建抽象类真正的具体的对象。这些抽象成员由继承抽象类的子类来实现,如果子类不完全实现这些抽象成员,其自身又成为了抽象类,还需要子类的子类来实现这些抽象成员。
下面我做了一段代码,使用了抽象属性和抽象方法,并且用子类将抽象类实现,再次调用了覆写的属性和方法,这个例子没有太复杂的代码,定义了一个Ren的抽象类,其中包括信仰属性和吃饭的方法,有继承的子类如中国人类、西方人类来根据自己的需要自己定义他们的含义和使用方法,本例中我只写了一个子类。当讲解到接口时,我再举个实际计算的例子。
抽象类实例
1 abstract class Ren 2 { 3 // 定义了一个只写的抽象的信仰Belief属性,只有set访问器 4 abstract public string Belief 5 { 6 set ; 7 } 8 // 定义一个抽象的Eat方法 9 abstract public void Eat(); 10 } 11 class Chinese : Ren 12 { 13 // 定义一个食物字段。 14 string Foot; 15 // 定义一个标志,当给属性Belief赋值错误时,调用Eat方法时提示错误。 16 bool bz = true ; 17 // 定义一个信仰字段 18 string belief; 19 // 用override关键字来覆写抽象类Ren的属性,同时给实现本类的信仰字段。 20 public override string Belief 21 { 22 // 使用set访问器,判断输入的信仰的类型,同时给食物字段赋值, 23 // 当用户输入不正确的信仰时,标志为False,提示错误。 24 set 25 { 26 switch (value) 27 { 28 case " 佛教 " : 29 belief = value; 30 Foot = " 忌荤食 " ; 31 break ; 32 case " 伊斯兰教 " : 33 belief = value; 34 Foot = " 忌猪肉 " ; 35 break ; 36 case " 其他 " : 37 belief = value; 38 Foot = " 无忌口 " ; 39 break ; 40 default : 41 Console.WriteLine(" 信仰应填写:佛教、伊斯兰教或其他 " ); 42 bz = false ; 43 break ; 44 } 45 } 46 } 47 public override void Eat() 48 { 49 if (bz == true ) 50 Console.WriteLine(" 我们信仰是{0},所以我们选择食物{1} " , belief, Foot); 51 else 52 Console.WriteLine(" 操作失败! " ); 53 } 54 } 55 class Program 56 { 57 static void Main( string [] args) 58 { 59 Ren ren = new Chinese(); 60 Console.Write(" 请填写信仰: " ); 61 // 用户输入同时给属性赋值,判断信仰的类型 62 ren.Belief = Console.ReadLine(); 63 // 执行Eat方法 64 ren.Eat(); 65 } 66 }
运行结果
请填写信仰:佛教 我们信仰是佛教,所以我们选择食物忌荤食 请按任意键继续. . .
请填写信仰:邪教 信仰应填写:佛教、伊斯兰教或其他 操作失败! 请按任意键继续. . .
这时两次运行的结果,在第59行中,我们来可以用Chinese类来实例对象,调用Chinese类中的类成员。在这个例子中,用到了属性,可以看到在属性中除了给字段赋值,还可以调用任何的成员,回忆一些在系列中讲解属性的时候,我就曾经说过,属性就是两个方法,get读取方法,set赋值方法。在学习接口的时候我会实现一个接口和抽象类结合的实例。
使用抽象类要注意到两点:一,抽象类中如果包含抽象成员,一定要使用abstract关键字来修饰,同时继承他的子类使用override关键字覆写这个抽象类中所有的抽象成员,并且全部实现为有具体功能或意义的成员,如果继承抽象类的子类没有把父类的抽象成员全部实现,其本身也将成为另一个抽象类,那么他的实现就必须还需要一个子类继续实现。二,如果想用父类的对象调用子类从父类中继承的成员和被子类覆写的成员,就必须通过子类来实例父类的对象,抽象类本身因为可能有抽象的成员,所以C#中规定抽象类无法自身实例化对象,使用语法如例子59行,上一篇类八 中最后一个例子是实例类通过子类来实例其对象的例子,如果你对父类通过子类来实现还是不清楚,可以参考上节课的例子理解一下。
在定义抽象类的时候,要让抽象类包括尽可能多的共同代码,定义尽可能少的数据,在一个以继承关系形成的类结构中,最成功的设计是只有最后一个子类是具体的类,其他的父类或父类的父类都应该是抽象类或接口, 下面我们来看什么是接口。
接口
接口的定义使用interface关键字,接口的成员只有四种:属性、方法、委托、索引器,比抽象类少了一种成员:类,原因就在于接口中不能定义任何有实现的成员,所有的成员都是虚成员、抽象的,无论类中有没有定义,一定会有的就是编译器默认的无参构造函数,它是有定义的,所以接口中不能含有类。同时接口的访问修饰符是默认省略的public,而且不能把public加入到接口的成员中,定义成员只有返回值、成员标识符和参数。接口的命名首字母应为I,这是规范,目的是当一个类继承了一个父类和多个接口时,能够通过I字母,明显的区分出接口和父类,下面定义了一个接口:
interface IJK
{
void Method(int a);
}
继承接口的类必须完全的实现接口中的定义成员,要创建接口的对象和创建抽象类的对象一样,应该使用完全实现其定义的子类来实例化对象。
下面是一个接口的简单实例,代码很少,主要是让同学们了解它的基本用法,本例中定义了一个接口,和两个继承接口的类,接口定义了一个有关两个整数的操作方法,参数使用了引用参数,有关引用参数的使用,同学们可以回顾一下系列类三 ,PF类继承接口后,实现了一个把这两个数进行平方的方法,JH类继承接口后,实现了一个把这两个数进行值交换的方法,在入口方法中,输出结果。
接口实例1
1 2 // 定义了一个接口 3 interface ITwoNum 4 { 5 // 定义了一个带两个引用型参数的无返回值方法, 6 // 不许加public修饰符。 7 void twonum( ref int a, ref int b); 8 } 9 // PF类继承了接口 10 class PF : ITwoNum 11 { 12 // 定义了一个对参数进行平方的方法,实现了接口中的方法 13 // 实现接口的覆写方法时,可以不使用override 14 public void twonum( ref int a, ref int b) 15 { 16 a = a * a; 17 b = b * b; 18 } 19 } 20 // JH类继承了接口 21 class JH : ITwoNum 22 { 23 // 定义了一个对参数进行值交换的方法,实现了接口中的方法 24 // 实现接口的覆写方法时,可以不使用override 25 public void twonum( ref int a, ref int b) 26 { 27 int c = 0 ; 28 c = a; 29 a = b; 30 b = c; 31 } 32 } 33 class Program 34 { 35 static void Main( string [] args) 36 { 37 // 定义了要进行运算的两个数的值 38 int a = 4 ; 39 int b = 5 ; 40 Console.WriteLine(" 两个数分别是a=4和b=5. " ); 41 42 // 分别用子类来实例化接口的对象 43 ITwoNum itwonum1 = new JH(); 44 ITwoNum itwonum2 = new PF(); 45 // 通过对象调用实现了接口中方法的子类覆写方法。 46 itwonum1.twonum( ref a, ref b); 47 Console.WriteLine(" 交换后两个数分别是a={0}和b={1}. " ,a,b); 48 itwonum2.twonum(ref a, ref b); 49 Console.WriteLine(" 平方后两个数分别是a={0}和b={1}. " , a, b); 50 } 51 } 52
结果如下:
两个数分别是a=4和b=5. 交换后两个数分别是a=5和b=4. 平方后两个数分别是a=25和b=16. 请按任意键继续. . .
本例中只用到了一个类实现一个接口,当一个类实现两个以上的接口时,接口中也许会出现成员的标识符相同的情况,此时在继承的类中很难去区分了,遇到这种情况可以使用接口名引用其接口成员的方法进行区分,比如:
接口多义性实例
1 // 定义了两个接口IA、IB,接口中都有叫做Method的同名方法。 2 interface IA 3 { 4 void Method(); 5 } 6 interface IB 7 { 8 void Method(); 9 } 10 // 使用多重接口同名实现 11 class C : IA, IB 12 { 13 // 对于明确指定接口的成员,不需要加修饰符 14 // 这些成员被默认为public 15 void IA.Method() 16 { 17 Console.WriteLine(" 我是接口IA中的方法 " ); 18 } 19 // 此时不做注明的将视为从另一个接口中继承来的方法 20 public void Method() 21 { 22 Console.WriteLine(" 我是接口IB中的方法 " ); 23 } 24 } 25 class Program 26 { 27 static void Main( string [] args) 28 { 29 // ----调用接口IB中的Method方法有两种。----- 30 // 1.通过子类本身创建对象 31 C c = new C(); 32 // 再用对象c调用Method方法 33 c.Method(); 34 Console.WriteLine(); 35 36 // 2.通过对子类的实例,创建接口IB对对象B 37 IB b = new C(); 38 // 再用对象b调用Method方法 39 b.Method(); 40 Console.WriteLine(); 41 42 43 // ----调用接口IA中的Method方法有两种。----- 44 // 因为子类中默认的Method方法是才IB中继承来的,所以无法通过子类对象c直接引用Method方法 45 // 1.使用as关键字,将c重新转型为接口IA,并且将转型后的对象引用,指定给另外一个对象a 46 IA a = c as IA; 47 // 再用对象a调用接口IA中的方法Method,此时a中就含有了c中从的IA接口继承来的方法Method。 48 a.Method(); 49 Console.WriteLine(); 50 51 52 // 2.原理同1,用强行转换将c转换为接口IA,并且将转型后的对象引用,指定给另外一个对象a2 53 IA a2 = (IA)c; 54 // 再用对象a2调用接口IA中的方法Method,此时a2中就含有了c中从的IA接口继承来的方法Method。 55 a2.Method(); 56 } 57 }
结果如下:
我是接口IB中的方法
我是接口IB中的方法
我是接口IA中的方法
我是接口IA中的方法 请按任意键继续. . .
从这个例子我们可以看出在一个类继承一个以上的接口或类时,用","隔开,通常习惯将类放到":"后的第一位,其后才是继承的接口。
学过了接口和抽象类后,一定有同学现在感到很疑惑,什么时候要定义成抽象类、什么情况下应该定义成接口,抽象类和接口除了在定义成员时的区别外(抽象类可以包括实现了的方法,而接口中必须全部是没有实现的抽象方法),还有什么呢?
首先我们要先搞清楚一条原则:类是对对象的抽象,抽象类是对类的抽象,而接口是对行为的抽象。什么是对行为的抽象,比如我们家的防盗门,它除了是门以外,还有很多的功能,比如防寒、防盗,甚至可以起到美观的作用,如果我们要将设计这个实例,就应该把门设计成抽象类,其中可以包括很多字段、属性,比如材质、大小等,将防寒设计成一个接口IH,防盗再设计成另外一个接口ID,然后再设计一个防盗门的类继承门这个抽象类和两个防寒防盗的接口,至于你家的防盗门就是防盗门这个子类的一个对象了,你家防盗门的材质、大小,如何防盗、防寒可以自己定义。那为什么要把防寒、防盗设计成接口呢?因为可以防寒防盗的不仅有门,防寒有衣服、被子,防盗有窗户、防盗器,只要在一个程序集中都可以用到这个接口。
看了我上一段的讲述,一定有同学稍微明白了一点,但是同学们不要把抽象类和接口想成了两个极端的分类,我们在定义的时候只要遵循这条规律就可以了,那就是:如果行为跨越不同类的对象,也就是功能较多的时候,可使用接口;对呀一些相似的类的对象,完全可以设计抽象类,然后继承,不一定非得再设计一个接口。
下面我们来看这个实例,用到了接口、抽象类、子类相结合,定义了两个接口,Iflay接口中定义了canflay方法签名,Isay接口中定义了cansay方法签名,定义了一个抽象类animal动物类,定义了其中的一个抽象方法eat方法签名,又定义了一个抽象类Ren人类,继承了抽象动物类和Isay接口,实现了Isay接口中定义了cansay方法,同时又定义了一个保护Protect的抽象方法,最后定义了两个实例类分别是DMren动漫人物类继承了人类和接口Iflay,和bird类继承了动物抽象类和接口Iflay,这样Dmren就拥有了四种方法:canflay、cansay、eat、Protect,除了已经实现了的cansay方法外,其他方法必须在Dmren类中完全的实现;bird类需要实现eat和canflay方法,代码如下:
接口、抽象类、实例类结合实例
1 // 定义了接口Iflay 2 interface Iflay 3 { 4 // 定义了canflay的方法,所以能飞的都可以继承 5 // 比如飞机、鸟、飞碟 6 void canflay(); 7 } 8 // 定义了接口ISay 9 interface ISay 10 { 11 // 定义了cansay的方法,所以能说话的都可以继承 12 // 比如电视、mp3、会说话的鹦鹉 13 void cansay(); 14 } 15 // 定义了一个动物的抽象类 16 abstract class Animal 17 { 18 // 定义了一个吃东西的方法 19 abstract public void eat(); 20 } 21 // 定义了一个人类,继承了抽象动物类和接口isay, 22 abstract class Ren : Animal, ISay 23 { 24 // 继承接口的子类必须完全实现接口中的成员 25 // 实现了接口Isay中能说话的方法 26 public void cansay() 27 { 28 Console.WriteLine(" 我继承了接口Isay,我能说话 " ); 29 } 30 // 再次定义一个保护的方法 31 abstract public void protect(); 32 } 33 // 定义一个动漫人物类,继承了抽象Ren类和接口Iflay 34 class DMren : Ren,Iflay 35 { 36 // 定义类中的两个字段name、foot,提供给类中的方法 37 string name; 38 string foot; 39 // 定义构造函数,其中有两个参数,第一个的名字DMname,第二个是同名的foot 40 // 在构造函数中调用下面本类中的三个方法,和基类中的cansay方法。 41 // 创建构造函数就是为了在其他类中,创建对象时,无需在其他类中使用DMren类的对象调用的麻烦,可以一次性使用构造方法中的所有方法成员, 42 // 给字段赋值使用到了this关键字,因为是同名变量。 43 public DMren( string DMname, string foot) 44 { 45 this .foot = foot; 46 name = DMname; 47 canflay(); 48 eat(); 49 protect(); 50 cansay(); 51 } 52 public void canflay() 53 { 54 Console.WriteLine(" 我继承了接口Iflay,我是{0},我能飞 " ,name); 55 } 56 // 使用override关键字重写抽象类中的abstract修饰的eat方法 57 public override void eat() 58 { 59 Console.WriteLine(" 我是继承了抽象类animal,我吃 " + foot); 60 } 61 // 使用override关键字重写抽象类中的abstract修饰的Protect方法 62 public override void protect() 63 { 64 Console.WriteLine(" 我是继承了抽象类Ren,我能保护周围的人 " ); 65 } 66 } 67 // 定义了一个鸟类,继承了动物抽象类和接口Iflay 68 class bird : Animal, Iflay 69 { 70 string name; 71 string foot; 72 // 如动漫DMren类一样使用构造方法,在创建对象的同时,一次性调用类中的所有要执行的方法和赋值 73 public bird( string DMname, string foot) 74 { 75 this .foot = foot; 76 name = DMname; 77 canflay(); 78 eat(); 79 } 80 public override void eat() 81 { 82 Console.WriteLine(" 我是继承了抽象类animal,我吃 " + foot); 83 } 84 public void canflay() 85 { 86 Console.WriteLine(" 我继承了接口Iflay,我是{0},我能飞 " , name); 87 } 88 } 89 class Program 90 { 91 static void Main( string [] args) 92 { 93 // 创建DMren对象dm,同时给构造函数传入两个参数 94 DMren dm = new DMren( " 大力水手 " , " 菠菜 " ); 95 Console.WriteLine(); 96 97 // 创建bird对象b,同时给构造函数传入两个参数 98 bird b = new bird( " 鸽子 " , " 玉米 " ); 99 } 100 } 101
结果如下:
我继承了接口Iflay,我是大力水手,我能飞 我是继承了抽象类animal,我吃菠菜 我是继承了抽象类Ren,我能保护周围的人 我继承了接口Isay,我能说话
我继承了接口Iflay,我是鸽子,我能飞 我是继承了抽象类animal,我吃玉米 请按任意键继续. . .
本例中使用了构造函数和this关键字,在系列类四 中有详细的讲解,对抽象类和接口的学习如果想更加的深入一定要有很强的基本功,同时还要学习设计模式,能够把继承用好就是好的程序架构,写到这类系列就结束了,要想学好之后的课程一定要想把类的知识学扎实了,不要急于求成。