撰写了文章 发布于 2020-03-07 21:44:46
从C#开始的编程入门——接口
之所以配这个图是因为想到了一些。和接口有关的情节。
接口(interface)是更彻底的抽象。
通过重写虚函数,我们让一条继承链上的类变得抽象。而接口会让来自不同继承链的类都变得抽象。
假设你是饭店老板。你是一个非常开放的人,你不管来的是人是狗,或者甚至连动物都算不上,只要能付钱给你,你就接待。
这里的关键是什么,关键是“能付钱”,我们甚至不关心来的客人是不是来自地球生物网,我们只关心其“能力”的体现。这个时候,我们就需要使用接口来描述这种类需要具备的“能力”。
定义
接口本身就是为了进一步抽象而存在的,因此和抽象类一样,接口不能被实例化,并且它不需要包含成员定义(但是C#8.0开始可以包含默认实现)。
public interface IPayable { void Pay(float price); }
如果你在真实生产中需要这样一个接口,可能需要把float改成精度更高的decimal。
interface 关键字代表我们在定义一个接口。后面紧接着是接口名称。 接口中只包含成员的声明。可以在接口中声明的成员包括方法和属性(以及索引器、事件,我们后面再讲。从C#8.0开始,还可以定义常量、运算符重载、静态成员)。因为暴露接口的目的就是给需要的类进行调用,因此接口中的成员不允许使用访问权限修饰符,所有成员都相当于public的。并且也不需要virtual。
实现
一旦定义了接口,我们需要类去“实现接口“来表示我们的类具备这样的能力,能够完成接口定义的要求。 实现接口的语法和继承类似,在类名后加冒号跟接口名称。
public class Dog : Animal, IPayable // ……
如果类本身还继承了其他类,那么需要把继承的父类放在最前面,实现的接口放在后面。尽管C#只支持单继承,但是可以实现多个接口。
隐式实现
隐式实现(implicit interface implementation)的方法是在类内部定义一个能对得上接口中声明的成员即可。由于接口中的成员本来就应该暴露给程序中的其他部分使用,因此显式实现接口的成员只允许为public的。
public void Pay(float price) { System.Console.WriteLine($”The dog named {Name} paid ${price}.”); }
显式实现
显式实现(explicit interface implementation)就稍微有点说道。
显式实现接口时,需要通过接口名称加上成员访问运算符来指定接下来的定义是实现的哪个接口的成员。比如我们在Dog中显式实现IPayable。
void IPayable.Pay(float price) { System.Console.WriteLine($"The dog named {Name} paid ${price}."); }
和显式实现相比,最大的区别在于,显式实现只能通过接口类型的变量访问。隐式实现更像是我本来就想定义这样一个成员,不过顺便实现一下接口。
显式实现还解决了一个问题。由于可以实现多个接口,多个接口中可能有签名相同的成员,而我们需要为不同的接口进行不同的实现。这个时候必须使用显示实现。
利用接口引用对象
一旦类实现了某个接口,我们就可以使用接口类型的变量去引用所有实现了这个接口的类,就像基类变量引用子类对象那样。
IPayable customer = new Dog(){ Name=“Hanako” }; customer.Pay(13.0f);
对于接口类型的变量,只能访问接口中定义成员。我们对各自类进行了更加彻底的抽象,只关心接口中的成员。我们将接口的调用者和实现者隔离开来,我们不需要关心实现的具体细节。即使有一天Dog的实现方式变了,但是只要接口没变,我们的调用方法就可以保持不变。
这正是接口的强大之处,我们不管是谁在付钱,也不管它到底是怎么付的钱。
调用显式实现的成员
对于显式实现的成员,可以和隐式实现的成员共存(严格来说这时隐式实现的成员并不算是实现了接口)。但是此时如果通过接口引用对象的话,只能调用到显式实现的成员。
void IPayable.Pay(float price) { System.Console.WriteLine($”The dog named {Name} paid ${price}.”); } public void Pay(float price) { System.Console.WriteLine($”The dog named {Name} paid ${price}.”); }
如何理解接口
“接口”听起来有一些不太好理解。实际上通常说的(G)UI中的I也是interface,但这里通常会被翻译成“界面”,实际上就是把inter和face直译过来的。这暗示了interface是介于两个系统的两个部分的中间,负责沟通两个部分的一个“面”。
进一步来讲,interface是两个系统达成的一种协议。使用接口的一方要求对方必须提供所需功能,而实现一方承诺实现接口要求实现的功能,最终使用方便可以调用实现方提供的功能,而实现方会为使用方完成工作。
接口这种说法用硬件来解释更加易懂。你的电视支持HDMI设备输入,你的PS4支持把内容通过HDMI输出,我们可以说HDMI规范的相关要求就是一种抽象上的接口,而物理上的接口就是HDMI接口。
继承
接口和类一样,也可以继承。
派生出的接口继承了所有基接口要求实现的成员,派生接口可以进一步进行更严格的要求。而实现派生接口的类和结构必须实现基接口的所有成员。
比如说从IPayable派生一个支付宝支付的接口。我们可能需要要求提供支付宝账号。我们定义一个只读属性即可。
public interface IAlipay: IPayable { public string AlipayAccount { get; } }
这个时候如果要实现这个接口就需要一并实现IPayable中定义的Pay方法。而任何需要利用Pay方法进行收钱的代码不需要进行额外的更改。
class Foo : IAlipay { public void Pay(float price) { System.Console.WriteLine($”Paid {price} with Alipay {AlipayAccount}.”); } public string AlipayAccount { get { return “13333333333”; } } }
命名规则
讲到这里已经学了那么多需要取名字的东西了。我想如果再不提一下命名规则可能正在读文章的你已经满屏abcdxyz123bianliang了。
对于不同的编程语言、框架来说,相关的团体或公司往往会有命名规则上的建议。之所以说是建议,是因为这些东西不会在编译时被判定为错误,纯粹是给人类制定的规矩,以便程序有更高的可读性。你可能会说我觉得我自己能看懂就行了,但是很多时候无法避免需要给别人看你的代码,即便是真的只有你自己看,一周后的你、一个月、一年后的你也不一定看得懂自己写的什么。
对于C#,微软建议对于类、结构、命名空间、方法、属性采用的是PascalCase。意思就是,如果由多个单词组成,首先不使用缩写,每个单词以大写字母或小写字母开始,其他字母全部小写。
class SomeClass{} void SomeMethod();
对于本地变量、字段,采用camelCase(PascalCase被视作是camelCase的一种变体)。第一个单词首字母小写。
var someVariable = 1; string nameField = “name”;
常量通常大写所有字母,以下划线隔开多个单词(称为SCREAMING_SNAKE_CASE)。
public static double THE_PI = 3.1415926;
接口名称是唯一残留的匈牙利命名法。以I开头,I代表interface。然后跟上名称,很多时候会采用表示能动的形容词来命名,比如destructible、accessible、comparable等等。实际上Windows系统的Win32 API主要采用的就是匈牙利命名法。
public interface IEatable { double Coolory { get; } }
Ellsworth 1年前
发布