继承与接口(下)
本系列文章主要意在总结笔者在学习过程中学到的有关 C# 特性的知识,分享 C# 中较为重要和突出的部分和有助养成良好编程习惯的提示。
并非旨在系统介绍 C#。
8月考试。
本篇文章是以继承与接口为主题的文章。希望在巩固总结笔者有关知识的基础上,能为读者提供一些额外的小知识。本文以《Visual C# 从入门到精通(第九版)》 第 12-13 章 为蓝本,进行了一定程度的改编,增加了一些笔者的理解和示例,方便读者理解和阅读。
目录:
C# 学习之路(十)勘误
背景
定义,(隐式)实现接口
显式实现接口
通过接口引用类
使用接口的注意事项
C# 学习之路(十)勘误:
C# 学习之路(十)中的如下例子存在错误:
static class Util{ public static int PlusPlusPlus(this int i, int j, int k) { return (i + j + k); }}int x = 591;x.PlusPlusPlus(10, 20); // 只需传入后两个参数Console.WriteLine(x); // 输出 621
PlusPlusPlus方法需要修改 i 参数的实参,而由我们之前在 C# 学习之路(六)(七)中对值与引用的讨论得知,PlusPlusPlus 方法存在如下两个无法修改实参 x 的问题:其一,PlusPlusPlus 方法只返回了表达式 (i + j + k) 的值,而未修改哪怕一个参数。其二,不使用 ref 或 out 关键字修饰参数,意味着就算在方法内修改参数,仍然无法从原变量处体现出变化,因为这种修改作用于原变量的拷贝而非其本身。具体情况可以参见 C# 学习之路(六)(七)。
在不修改 PlusPlusPlus 方法的前提下,应如下调用该方法:
int x = 591;Console.WriteLine(x.PlusPlusPlus(10, 20)); // 输出 621
出现这种低级错误,笔者当深刻反思。
背景:
从类继承是很强大的机制,但继承真正的强大之处是能从接口继承。接口不含任何代码或数据;它只是规定从接口继承的类必须提供哪些方法或属性(属性将在后续文章中讨论)。使用接口,方法的名称/签名可以完全独立于方法的实现。
接口相当于一份协议。继承了接口的类必然实现了接口要求实现的所有方法和属性。这样就能保证我们在使用继承了某个接口的类时,能清晰的认识到,这个类实现了哪些方法和属性。
使用接口,可以真正的将“what”(有什么)和“how”(怎么做)区分开。接口指定有什么,也就是方法的名称、返回类型和参数,以及属性的名称、访问器。至于具体怎么做,或者说接口内容的实现,则不是接口所关心的,应当由继承接口的类来决定怎么做。简言之,接口描述了类提供的功能,但不描述如何实现。
定义,(隐式)实现接口:
定义接口和定义类相似,只不是使用的是 interface 关键字而不是 class。若想在接口中声明方法,只需按照和在类/结构中定义方法一样的步骤即可。需要注意的是,在接口中声明方法时无需也不应该实现方法,它们只是声明在接口中。接口中方法如何实现取决于继承接口的类。因此,我们需要使用分号结束方法语句,无需提供方法主体,示例如下:
interface IComparable // 用 interface 关键字声明接口{ int CompareTo(object obj); // 只声明方法,但不实现。用分号代替方法主体。}
接口不含任何数据,不可在接口中添加任何字段。接口名称建议以大写字母 I 开头。可以注意到,我们并没有对方法做访问限制,访问限制符也应当由实现接口的类提供。
而正如前面所说,接口的实现要放在继承了接口的类中,且该类需要实现接口指定的所有内容。先声明一个 ILanBound 接口:
interface ILandBound // 定义陆上接口声明一个返回陆地动物腿的个数的方法{ int NumberOfLegs();}
然后我们用 Horse 类继承 ILandBound 接口:
class Horse : ILandBound // 继承接口{ // TODO: public int NumberOfLegs() // 实现接口中的方法,只能用 public 修饰可访问性 { return 4; }}
在类中实现接口时有如下几个规则需要遵守:
方法名和返回类型必须与接口中一致。
方法的所有参数与接口中一致,包括参数前缀(如 ref 和 out)。
方法必须具有 public 级别的可访问性。
我们再来看一个例子:
class Mammal{ // TODO:}interface ILandBound{ // TODO:}interface IDebug{ // TODO:}class Horse : Mammal, ILandBound, IDebug // 继承一个类和多个接口{ // TODO:}
在上例中,Horse 类继承了一个基类和两个接口。在 C# 中,如果实现接口的类派生自其他类,则需将其基类置于继承列表的最前方。类可以继承多个接口,但只能继承一个基类。
和类一样,接口也可以从另一个接口继承。事实上,应该将这种继承称为接口扩展。如果接口 InterfaceA 继承自接口 InterfaceB ,那么实现接口 InterfaceA 的类需要实现接口 InterfaceA 和 InterfaceB 规定的所有内容。
显式实现接口:
前面的例子描述了如何在类中隐式实现接口。但这种方式存在一定局限性。试想,如果接口 InterfaceA 和接口 InterfaceB 都声明了 NumberOfLegs 无参方法。但它们的目的不同,InterfaceA 的 NumberOfLegs 方法意在返回动物的腿(legs)的个数,InterfaceB 的 NumberOfLegs 意在返回动物经过了几站(legs)。(在英文中,leg 可以表示路程的一部分,比如“the last leg of a trip”代表此行最后一站,因此此处两个接口中的 NumberOfLegs 方法名所蕴含的意思不同)
现在 Horse 类需要实现 InterfaceA 和 InterfaceB 接口,也就意味着它需要实现两个 NumberOfLegs 方法。但如果隐式实现接口,那么编译器如何知道哪个 NumberOfLegs 方法是 InterfaceA 声明的,而哪个 NumberOfLegs 又是 InterfaceB 声明的呢?
interface InterfaceA{ int NumberOfLegs(); // 返回动物腿的个数}interface InterfaceB{ int NumberOfLegs(); // 返回动物经过了几站}class Horse : InterfaceA, InterfaceB{ public int NumberOfLegs() { return 4; } // 编译器如何知道这是哪个接口的实现?!}
这样的代码是合法的!在编译器看来,虽然 Horse 类只实现了一个 NumberOfLegs 方法,但这一个方法实现了两个接口!
为了解决该问题,并区分哪个方法实现哪个接口,应当显式实现接口。为此,要指出方法从属于哪个接口:
interface InterfaceA{ int NumberOfLegs(); // 返回动物腿的个数}interface InterfaceB{ int NumberOfLegs(); // 返回动物经过了几站}class Horse : InterfaceA, InterfaceB{ int InterfaceA.NumberOfLegs() { return 4; } // 实现 A 接口,返回 4 条腿 int InterfaceB.NumberOfLegs() { return 3; } // 实现 B 接口,返回 3 站}
我们可以发现,显式实现接口时,无需使用访问限制符修饰方法。这样就意味着,这两个 NumberOfLegs 方法都是私有方法!(不添加访问限制符时,默认声明字段或方法为私有)为何要这么设计?这是因为,如果能直接在外部调用这两个 NumberOfLegs 方法,那么此时,又如何得知调用的是哪个方法呢?
Horse horse = new Horse();int legs = horse.NumberOfLegs(); // 无法编译!// 如果能直接调用 NumberOfLegs 方法,编译器如何得知调用的是哪个方法?
此时我们需要通过接口引用类的方式来调用 NumberOfLegs 方法,这种方式能区分调用的是哪个方法,同时还具有其他优势。
通过接口引用类:
和基类变量能引用派生类对象一样,接口变量也能引用实现了该接口的类。
Horse horse = new Horse();InterfaceA iMyA = horse; // 合法InterfaceB iMyB = horse; // 合法
而为了调用 InterfaceA 接口和 InterfaceB 接口中不同的 NumberOfLegs 方法,我们需要使用引用了 Horse 对象的接口变量来调用相应方法。
Horse horse = new Horse();InterfaceA iMyA = horse; // 合法引用InterfaceB iMyB = horse; // 合法引用int legsA = iMyA.NumberOfLegs(); // 调用实现 A 接口的 NumberOfLegsint legsB = iMyB.NumberOfLegs(); // 调用实现 B 接口的 NumberOfLegs
编译器会根据接口变量的类型来决定调用类中的哪个 NumberOfLegs 方法。我们需要创建接口变量并引用类才能通过该接口变量调用类中实现的接口方法。为了让这句话能被看懂,可以将其拆分如下:
定义接口变量并引用类
通过该变量访问类中实现的接口方法,在上例中就是 NumberOfLegs 方法
当接口方法被显式实现时,我们就需要按照如上方式调用接口方法。
通过接口引用类是一项相当有用的技术。
int Find(InterfaceA myA){ // TODO:}
在上面这个 Find 方法中,我们可以获取任意实现了 InterfaceA 的实参。比如,Horse 类和 Person 类都实现了 InterfaceA 接口,那么任意 Horse 类或 Person 类变量都可作为实参传进 Find 方法。这种传参方式即扩展了传入参数的类型——实现接口的所有类型,又防止未实现接口的类型传入。相较将形参设置为 object 类型,是不是有很大的优势呢?
可用 is 操作符验证对象是不是实现了接口的一个类的实例。
Horse horse = new Horse();if(horse is InterfaceA) // is 操作符返回布尔值{ InterfaceA iMyA = horse;}
使用接口的注意事项:
字段本质上是类或结构的实现细节。接口中不允许定义任何字段。
构造器也是类或结构的实现细节。接口中不允许定义任何构造器。
接口中不允许定义析构器。
不能为接口中的方法指定访问限制符。接口所有方法都隐式成为公共方法。
接口不能从类继承,但可从接口继承。
本文中 C# 的有关知识整理归纳自 《Visual C# 从入门到精通(第九版)》清华大学出版社 John Sharp 著 周靖 译 ISBN 978-7-302-51624-8 十分推荐想学习 C# 的朋友购入学习
笔者水平有限,如有错误,欢迎给公众号留言反馈(文章目前不支持留言)