C#基础知识梳理系列六:抽象类与接口
抽象类,是一种特殊的类,可以定义具有实现的方法,也可以定义未实现的方法契约,本身不能被实例化,只能在派生类中进行实例化。接口,对一组方法签名进行统一的命名,只能定义未实现的方法契约,本身也不能被实例化,只能在实现类中进行实例化。二者都可以有部分数据成员(如:属性),它们貌似有着相同的“契约”功能,但对各自的派生类(实现类)又有着不同的要求,那么,到底它们有何异同呢?这一章将从四个方面来讲解它们的相同与不同之处。
抽象类 不能实例化。抽象类的用途是提供多个派生类可共享的基类的公共定义,是对类进行抽象,可以有实现,也可以不实现。使用关键字abstract进行定义。如下定义一个抽象类:
public abstract class Code_06_03 { }
再来看一下编译器为它生成的IL:
.class public abstract auto ansi beforefieldinit ConsoleApp.Example06.Code_06_03 extends [mscorlib]System.Object { } // end of class ConsoleApp.Example06.Code_06_03
可以看以,抽象类实际上是继承了System.Object类,并且编译器为它生成了一个默认的构造函数。
接口 它是对一组方法签名进行统一命名,是对一组行为规范的定义,各个行为(方法)之间相互疏。使用关键字interface进行定义,如下定义一个接口:
public interface ICode_06_01 { }
再来看一下编译器的工作:
.class interface public abstract auto ansi ConsoleApp.Example06.ICode_06_01 { } // end of class ConsoleApp.Example06.ICode_06_01
可以看到,接口实际上是把它当成抽象类来看待,但是没有构造函数。
无论是抽象类拥有构造函数,还是接口不拥有构造函数,它们都是不能被实例化的。
抽象类:描述
1) 可以定义抽象方法,抽象方法没有具体实现,仅仅是一个方法的契约,在子类中重写该方法。抽象类可以重写父类的虚方法为抽象方法。
2) 可以定义非抽象方法,但要求该方法要有具体实现,如果该方法是虚方法,则在子类中可以重写该方法。
3) 可以定义字段,属性,抽象属性,事件及静态成员。
如下是对类Code_06_03的扩充:
public abstract class Code_06_03 { Dictionary<Guid, string> Root = new Dictionary<Guid, string>(); public string Sex { get; set; } public abstract string Address { get; } public abstract int Add(int a, int b); protected virtual string GetAddress(string addressID) { return addressID + " 北京"; } public void AddRoot(Guid id, string rootName) { this.Root.Add(id, rootName); OnAddRoot(); } public event EventHandler AddRootEvent; void OnAddRoot() { EventHandler handler = AddRootEvent; if (handler != null) { handler(this, null); } } public string this[Guid key] { get { return Root[key]; } set { Root[key] = value; } } }
抽象方法public abstract int Add(int a, int b);的IL:
.method public hidebysig newslot abstract virtual instance int32 Add(int32 a, int32 b) cil managed { } // end of method Code_06_03::Add
编译器把Add方法当作一个虚方法,在子类中可以被重写。
虚方法protected virtual string GetAddress(string addressID)的IL:
.method family hidebysig newslot virtual instance string GetAddress(string addressID) cil managed { //省略 }
它本来就是一个虚方法,所以编译器并没有特殊对待它。
方法public void AddRoot(Guid id, string rootName)的IL:
.method public hidebysig instance void AddRoot(valuetype [mscorlib]System.Guid id, string rootName) cil managed { //省略 }
也是一个普通的对象方法。
接口
1) 可以定义属性及索引器,但不能定义字段。
2) 可以定义事件。
3) 可以定义方法,仅仅是方法签名的约定,不得有实现,在实现类中对该方法进行具体实现,有点类似于抽象类的抽象方法。
4) 不可以定义虚方法。
5) 不可以定义任何静态成员。
6) 接口成员默认是全开放的,不得有访问修饰符。
如下,定义一个接口:
public interface ICode_06_01 { string Name { get; set; } int Add(int a, int b); event EventHandler AddEvent; }
方法int Add(int a, int b);的IL:
.method public hidebysig newslot abstract virtual instance int32 Add(int32 a, int32 b) cil managed { } // end of method ICode_06_01::Add
可以看到,定义的时候,我们并没有为其指定可访问修饰符(编译器也不允许我们明文指定其可访问修饰符),但编译器默认将它的访问级别指定为public。另外是把它当作一个抽象的虚方法。
至于成员属性和事件,编译器则将它们当作普通的对象属性和对象事件对待,会为它们生成相应的get/set和add/remove 方法,并无特别之处。
抽象类的实现
由于抽象类也是类,所以对它的实现就像普通的继承一样,子类通过继承可以得到抽象类的公有成员,且可以重写部分成员,如虚方法和抽象方法等。如下是对Code_06_03类的实现:
public class Code_06_04 : Code_06_03 { public override int Add(int a, int b) { return a + b; } protected override string GetAddress(string addressID) { return "BeiJing"; } string _addressPrefix = "China "; public override string Address { get { return _addressPrefix; } } }
来看一下编译器的工作:
可以看到类Code_06_04是标准、明白、彻底地对类Code_06_03的继承。两个重写的方法Add和GetAddress都是普通的对象方法,只是依然被当作虚方法来看待,来看一下Add方法的IL:
.method public hidebysig virtual instance int32 Add(int32 a, int32 b) cil managed { //省略 }
方法GetAddress的IL:
.method family hidebysig virtual instance string GetAddress(string addressID) cil managed { //省略 }
因为这两个方法保持着虚方法的特性,所以对于Code_06_04类的子类,同样还可以重写这两个方法。
属性成员Address这里还是一普通的对象属性。
接口的实现
对接口的实现跟对抽象类的实现相似,如下是对接口ICode_06_01的实现类:
public class Code_06_02 : ICode_06_01 { string _name; public string Name { get { return _name; } set { _name = value; } } public int Add(int a, int b) { OnAdded(); return a + b; } public event EventHandler AddEvent; void OnAdded() { EventHandler handler = AddEvent; if (handler != null) { handler(this, null); } } }
来看一下编译器的工作:
它与普通类的区别不大,只是很明确的是实现了接口ICode_06_01,来看一下它的IL:
.class public auto ansi beforefieldinit ConsoleApp.Example06.Code_06_02 extends [mscorlib]System.Object implements ConsoleApp.Example06.ICode_06_01 { } // end of class ConsoleApp.Example06.Code_06_02
可以看到,类Code_06_02不仅继承于System.Object类,同时还实现了接口ICode_06_01。再来看一下对于接口中的方法,编译器是如何处理的。Add.IL:
.method public hidebysig newslot virtual final instance int32 Add(int32 a, int32 b) cil managed { //省略 }
编译器认为Add方法具有虚方法的特性。而对于属性和事件,依然是普通的实现,如get/set、add/remove。
另外,接口还支持显示实现接口,我们上面讨论的Code_06_02类对接口的实现默认是隐式实现。
在接口的实现类内部,可以存在一个与接口某一方法名(包括签名)完全相同的方法,但要求那个对接口实现的方法必须是显示实现,如下代码:
public int Add(int a, int b) { return a + b; } int ICode_06_01.Add(int a, int b) { OnAdded(); return a + b; }
可以看出显示实现就是在方法前加上接口名的前缀和点号(ICode_06_01.),同时也可以看到,显示实现接口的方法是不能有可访问修饰符的,编译器会对其进行private处理。那如何才能调用显示实现的接口方法呢?可以将实现类的对象转为一个接口变量,再调用该变量的相应方法。如下代码:
Code_06_02 code0602 = new Code_06_02(); ICode_06_01 icode0602 = code0602; icode0602.Add(1, 2);
而对于抽象类的实现,是不能进行显示实现的!
1) 抽象类保留一普通类的部分特性,定义可能已经实现的方法行为,方法内可以对数据成员(如属性)进行操作,且方法可以相互沟通。而接口仅仅是定义方法的签名,就像规则,只是约定,并没有实现。
2) 抽象类的派生类可以原封不动地得到抽象类的部分成员,接口的实现类如果想要得到接口的数据成员,则必须对其进行重写。
3) 一个类只能继承于一个抽象(类),但可以实现多个接口,并且可以在继承一个基类的基础上,同时实现多个接口。
4) 抽象类和接口都不能对其使用密封sealed,事实上这两者都是为了被其他类继承和实现,对其使用sealed是没有任何意义的。
5) 抽象类可以对接口进行实现。
6) 抽象类更多的用于“复制对象副本”,就是我们常说的“子类与父类有着is a的关系”,它更多关注于一个对象的整体特性。接口更多倾向于一系列的方法操作,这些操作在当前上下文中既有着相同作用对象,又相互隔离。
7) 某些时候,抽象类可以与接口互换
下面我们通过生活中见的红娘拉线的示例,来说明抽象类与接口给我们变成带来的方便性,下面代码既可以使用抽象类也可以使用接口。
一般(这里说明的是一般性况下)只要想通过红娘接线找到自己另一半的,在红娘(Matchmaker)安排甲去见已以前,都会安排他/她(wooer)应该怎么跟对方沟通,加深对方对自己的好感印象,这些话是红娘提供的,但说的动作,还是求婚者通过自己的“说话”本能来表达,只是红娘在教求婚者说哪些套话而已。
一般我们会如下定义:
/// <summary> /// 红娘 /// </summary> public class Matchmaker { string message; /// <summary> /// 指导求婚人如何表达 /// </summary> public void Teach() { message = "从见你的第一眼起,我就认为你会是那个每天早上跟我一起下床的人。但这绝对不是一见钟情,那是什么?我会在我们老去的那一天告诉你。"; Wooer wooer = new Wooer(); wooer.Say(message); } } /// <summary> /// 求婚人 /// </summary> public class Wooer { /// <summary> /// 向对方表达 /// </summary> /// <param name="message"></param> public void Say(string message) { Console.WriteLine(message); } }
客户程序:
public class ClientApp { public void Work() { Matchmaker matchmaker = new Matchmaker(); matchmaker.Teach(); } }
我们通常都会这么写,简单明了,实现功能没问题。
红娘在想:老娘一个人怎么能应付得了你们这一群剩男剩女!个个都有特殊情况,我哪里真有分身术!干脆我搭建一个平台,你们按我的要求填表,按我的步骤走就行了。
然而这里的红娘Matchmaker是强依赖Wooer。因为毕竟无论求婚人是男是女,不仅要口头语言表达,还有自己独特的肢体语言表达,比如男人会哼哼嗨嗨地表达,女人会哼哼唧唧地表达,Wooer要变了,那这个时候Matchmaker就得变。这个要求就是把所有求婚人抽象出来,下面我们引入接口,将Wooer抽象出来为一个接口IWooer,让Matchmaker依赖于IWooer,而不直接依赖于具体求婚人。接下来如下改造代码:
/// <summary> /// 红娘 /// </summary> public class Matchmaker { string message; /// <summary> /// 指导求婚人如何表达 /// </summary> public void Teach(IWooer wooer) { message = "从见你的第一眼起,我就认为你会是那个每天早上跟我一起下床的人。但这绝对不是一见钟情,那是什么?我会在我们老去的那一天告诉你。"; wooer.Say(message); } } /// <summary> /// 求婚人 /// </summary> public interface IWooer { /// <summary> /// 求婚人自己准备的语言,有自己的真家伙拿出来实战! /// </summary> string Message { get; } /// <summary> /// 肢体表达 /// </summary> void Action(); /// <summary> /// 语言表达 /// </summary> /// <param name="message"></param> void Say(string message); } public class ManWooer : IWooer { public string Message { get { return "我拥有制造印超机的技术"; } } public void Action() { //抱着对方、动手动脚、眼神像弯尺 //这些动作无法用代码表达,你懂的,如饿狼一样。。。 } public void Say(string message) { //先动起来预热 this.Action(); //接着再表达 Console.WriteLine(message + this.Message); } } public class WomanWooer : IWooer { public string Message { get { return "我拥有1+2个点"; } } public void Action() { //侧身向着对方、朦胧的眼神含情默默、相见恨晚,总是想看一眼对方,但害羞的脸蛋红红 //这些动作无法用代码表达,你懂的,如羔羊一样。。。 } public void Say(string message) { //先动起来预热 this.Action(); //接着再表达 Console.WriteLine(message + this.Message); } } 再来看看客户程序: public class ClientApp { public void Work() { Matchmaker matchmaker = new Matchmaker(); //matchmaker.Teach(); IWooer wooer; //求婚者是男人 wooer = new ManWooer(); matchmaker.Teach(wooer); //求婚者是女人 wooer = new ManWooer(); matchmaker.Teach(wooer); matchmaker.Teach(wooer); } }
很明显,现在Matchmaker已经不再直接依赖Wooer了,只依赖第三方IWooer,不论来的是男人还是女人,只要你按照红娘要求的步骤就行了,如此开放的平台,说不定明年就能创业板闯关成功!