C#夯实基础之接口(《CLR via C#》读书笔记)

一. 接口的类型

接口是引用类型.因此从值类型赋值给接口是需要装箱的.如下所示:

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         ISay catSay = new Cat();
 6         catSay.Say();
 7         Console.Read();
 8     }
 9 }
10
11 interface ISay
12 {
13     void Say();
14 }
15 struct Cat : ISay
16 {
17     public void Say()
18     {
19         Console.WriteLine("Cat Say!");
20    }
21 }

IL代码:

我们看到,这里有一个装箱的操作,而只有把值类型转到引用类型才会产生装箱操作,因而接口是引用类型.

二. 接口的定义

我们知道,在定义一个方法的时候,我们需要定义:

1.方法的签名:也就是这个方法的名称,参数与返回值

2.方法的实现:也就是这个方法具体的内容

      接口只是定义了一系列方法的签名,不包含方法的实现,且签名不能有任何修饰符.这意味着:

1.属性 事件 索引器也可定义入接口中,因为它们本质上都是方法,不能定义字段;

2.接口不可以定义任何构造器方法

下面是IConvertible的接口定义,它定义了很多类型转换的方法,但是并没有任何实现.

三. 接口的实现(单继承与多继承)

一个类可以同时继承m个基类(m=0,1)和n个接口(n≥0),同时类必须显式实现所有接口,接口不能继承基类,但是可以继承接口.

我们定义如下:

 1 interface ITest1
 2 {
 3     void Test1();
 4 }
 5 interface ITtest2
 6 {
 7     void Test2();
 8 }
 9 class Base
10 {
11 }

      单继承:我们先类或接口来看只继承一个基类或接口的情况.如下所示:

1 class Test : Base//类继承类
2 class Test : ITest1//类继承接口
3 interface ITest1: ITtest2//接口继承接口
4 interface ITest1: Base//(错误)接口继承类

其实第4种情况我们也能理解:假设接口能继承类,那么我们接口是不是就有了方法的实现,前面我们说过,接口只能有方法的签名不能有方法的实现,两者就相悖了.

      多继承:我们再来看类或接口继承一个类型和多个接口的情况

1 class Test : Base,ITest1,ITtest2//类继承1个基类和多个接口
2 interface ITtest3:ITest1,ITtest2//接口继承多个接口

使用接口的多继承,可以为一个类同时约束多个方法的实现,规定一个方法必须实现所有的的方法. 这既有好处,也不坏处.

      1:接口有什么好处?

拿我们最简单的例子来说,我们都有电脑,如果我们想为这台电脑升级一块硬盘,我们只需要更新一块符合这个电脑主板上硬盘的接口的新硬盘就可以了,我们不用更换整台电脑,也不用只更新特定厂商的硬盘. 因为所有的硬盘厂商都遵循着公共的接口标准,这样耦合就降低了.

其实,这样的例子生活中随处可见,如我们常用的USB接口,电源插座等等.

      2:接口继承的问题1

接口的坏处也显而易见,最明显的就是我们可能只需要实现一个接口中的一个方法,但被迫实现了接口中的其他方法,哪怕实现的方法体中没有任何代码.

      3:接口继承的问题2—重名问题

还有一个问题,就是多接口继承带过来的问题.

我们定义如下接口:

interface ITest1
{
    void TestMethod ();
}
interface ITtest2
{
    void TestMethod ();
}
class Test : ITest1, ITtest2
{
}

我们看到,ITest1和ITest2中有两个相同的方法,都是TestMethod (),Test类同时继承两个接口,那么问题来了,我们前面说过必须显式实现所有接口方法”,Test类该如何实现这两个同名方法?

C#提供了答案,即是它们可以共用一个实现,我姑且称之为共用实现,也可以分别各自实现,我们称之为显式实现.

在讲共用实现和显式实现之前,我们需要加一个基础知识的铺垫.

在CLR中,当一个类型加载进来时,会为这个类型建立一个方法列表,它包括以下内容:

1.这个类本身的方法记录

      2.从基类继承过来的虚方法记录

      3.从接口接口过来的虚方法记录

那么,在前面Test的例子中,Test的方法列表是什么?

1.TestMethod//Test类本身的方法

2.Ojbect(隐式继承的基类)的方法,如下

3.TestMethod// 继承自ITest1接口的虚方法

4.TestMethod// 继承自ITest2接口的虚方法

      共用实现

      C#编译器一比较,发现TestMethod这个方法的签名相同,也是显式public的,因为它认为ITest1和ITest2的接口方法和Test类本身的TestMethod方法完全一致.在生成元数据的时候,标明Test的TestMethod方法和ITest1的TestMethod方法和Test2的TestMethod方法三者都应该引用同一个实现.因而,下面这段代码的输出是:Hello

 1 class Test : ITest1, ITtest2
 2 {
 3    public void TestMethod()
 4 {
 5 Console.WriteLine("Hello");
 6    }
 7 }
 8 static void Main(string[] args)
 9 {
10   new Test().TestMethod();
11    Console.Read();
12 }

可不可以指定接口自己特定的实现?也就是说ITest1有自己的接口实现, ITest2有自己的接口实现, Test也有自己的同名实现方法.答案是可行的,这就是下面要说的显式实现.

      显式实现:在方法的名称前带上接口的名称,同时接口方法就默认为private,并且不能改变它的访问级别,调用时只能通过接口来调用,如下所示

 1 class Test : ITest1, ITtest2
 2  {
 3     public void TestMethod()
 4     {
 5         Console.WriteLine("Test");
 6     }
 7      void ITest1.TestMethod()
 8     {
 9         Console.WriteLine("Test1");
10     }
11
12      void ITtest2.TestMethod()
13     {
14         Console.WriteLine("Test2");
15     }
16 }

如果要调用Test类本身的TestMethod()方法,我们可以直接调用,如下所示:

new Test().TestMethod();

如果要调用Test1接口的TestMethod()方法,我们就需要通过接口调用了,如下所示:

((ITest1)new Test()).TestMethod();

这里需要说明的是,显式接口实现方法并不是类型的对象模型的一部分,而只是将方法与接口对应起来,同时避免公开行为.《CLR via C#》(P266)

       另一种重名问题是接口继承接口.

如下所示, ITtest2与ITtest1都有TestMethod()方法签名,同时ITtest2继承自ITtest1,那么ITtest2中只能存在一个方法,会默认隐藏掉ITtest1的方法,这个时候,我们可以使用new关键字.

interface ITtest2: ITest1
{
       new void TestMethod();
}

      4:接口继承的问题3—派生问题

当没有重名问题的时候,C#编译器要求将接口方法标记为public,同时CLR会将方法默认标记为virtual和sealed,这样可以公开调用但是不能被派生类重写.如下所示:

class Base : ITest1
{
    public void TestMethod ()
    {
    }
}
class Test : Base
{
}

也就是说Test类不能重写TestMethod方法.但是,如果我们把TestMethod加上virtual,那么CLR会默认去掉sealed标记,这样Test类就可以重写TestMethod方法了.如下所示

class Base : ITest1
{
    public virtual void TestMethod()
    {
    }
}
class Test : Base
{
    public override void TestMethod()
    {
        Console.WriteLine("Hello");
    }
}

      当使用显式接口实现(重名和不重名的时候都可以用)的时候,这样的方法不能加virtural关键字,所以这个时候它不能被Test类所重写.如下所示:

class Base : ITest1
{
     void ITest1.TestMethod()
    {
    }
}
class Test : Base
{
    //这里不能overvide TestMethod方法
}

四. 接口的调用

接口的调用有两种方式,其实文章的前面已经写出来了,这里再总结一下:

类调用:使用实现这个接口的类来调用接口的方法,它能调用类所有的方法,如下所示:

interface ITest1
{
    void TestMethod();
}
class Test : ITest1
{
    public void TestSelf()
    {
    }
    public void TestMethod()
    {
    }
}

接口调用:使用接口来调用接口的方法,它只能调用接口的方法

需要说明的是,类继承的一个重要特点是:凡能使用基类实例的地方,都能使用派生类的实例.

与此相似,接口继承的一个重点特点是:凡能使用接口实例的地方,都能使用实现了接口的一个类型的实例.前面的接口调用依据的就是这样的原理.摘自《CLR via C#》

五. 范型接口

这里直接给出《CLR via C#》中总结的3点.

      1.提供编译时类型安全     

当我们使用如下代码时,编译是没有问题的:

1 int i = 3;
2 int j=  i.CompareTo("2");

但在调用的时候,因为字符串与整形不能直接比较,因而会报错:

这个时候我们就一脸懵逼了,你要是早点告诉我就好了,ok我们使用范型版本:

       2.减少装箱的次数       

1 int i = 3;
2 int m=2;
3 int j=  i.CompareTo(m);

别看这里比较的都是值类型,但是这里m装箱了,因为CompareTo期待的是object类型.使用范型可以有效地规避这一问题,因为它会直接让你传入相应的类型数据.

      3.可以实现同一个接口若干次

class Test : IComparable<string>, IComparable<int>
{
   public int CompareTo(int other)
    {
        throw new NotImplementedException();
     }
     public int CompareTo(string other)
     {
        throw new NotImplementedException();
    }
}

六. 范型接口的约束

使用范型接口时,我们可以限定,参数T必须实现了哪几个类和接口,才可以作为参数.以便更精细地控制我们的类.

如下所示:

为什么传入Guid类型的不行呢?

因为我们要求参数,同时实现了IComparable, IConvertible两个接口(where T: IComparable, IConvertible),但Guid类只实现了一个.

同时,C#编译器会为接口约束生成特殊的IL指令,以减少装箱操作,这里向M传入x参数不会造成装箱操作.如下所示:

需要说明的是,下面的例子,在实例化t时不会发生装箱,在调用CompareTo方法传递参数3的时候会发生装箱,但此时实例t仍然不会发生装箱,用Jeffrey Richter的话就是:如果值类型实现了一个接口方法,在值类型的实例上调用接口方法不会造成值类型的实例装箱.

      

七. 使用非范型接口的问题

前面我们探讨过使用接口遇到的几个问题,比如,一旦继承了一个接口,即使子类不需要全部的方法,也需要一一实现接口方法.这些是使用接口(范型接口和非范型接口)共同的问题,.NetFramework在非范型接口之后又提供了范型接口,肯定是范型接口解决了非范型接口的某些问题,为我们编程提供了更好的编程体验和性能.

举一例,在使用IComparable接口时,它的Compare方法接收一个object参数,使用这个版本的非范型版本最大的问题是会遇到编译时安全性问题.如下所示

从上面的代码,我们可以看出,在转换的时候,因为不能控制传入的类型,所以,在类型转换时会出错.

我们可以利用范型接口来解决这个问题,使用显式实现接口,使用类实例的时候,这个接口就可以不被外界访问了,同时用一个同名方法来实现我们的方法.

这里我们在编译时就可以查出这个问题,增强了编译时的安全性.如下所示:

但有一个问题,如果我们使用接口实例来访问接口方法,那么前面所述的问题同样会出现.

八. 使用显式实现接口的问题

在第7点,我们利用了显式实现接口时,接口方法就是private了.但这个特性有时也会带来新的问题,如下所示:

在派生类中无法访问基类的接口方法.对于这样的问题,与第7点中的例子相似,我们提供了一个同名方法:

 1 public class Base : IComparable
 2 {
 3     public int CompareTo(object obj)
 4     {
 5         return 0;
 6     }
 7     int IComparable.CompareTo(object obj)
 8     {
 9         return 0;
10    }
11 }
12 public class Drived : Base
13 {
14     public void Test()
15     {
16         base.CompareTo(3);
17     }
18 }

这样就可以避免无法访问的问题.

九. 接口与基类的设计原则

既然接口与基类都在继承方面表现出了自己的特色.那有一个问题摆在我们面前:什么时候选择接口,什么时候选择基类?

要回答这个问题,我们要知道两者在使用时的一些特点:

接口:只是定义规范,每个继承接口的类都要实现一套自己的方法,彼此不能共享代码.如果接口有修正的时候,所有的子类都需要进行相应的变更.

基类:多个子类可以共享代码,对基类进行方法的添加时,子类可以直接用,而不需要进行相应的修改.

至于怎么用,因人因地而异.不过,我比较赞同Jeffrey Richter的建议:当父类与子类的关系是IS-A的关系,可以使用基类,如哺乳动物;当父类与子类的关系是CAN-DO的关系时,可以使用接口,如耕田

十. 总结

1.接口只定义方法签名与实现

2.必须显式实现接口的方法

3.可以显式接口方法实现,但会改变方法的访问级别

4.C#编译器为范型接口提供了约束,并进行了优化

十一. 参考文档

《CLR via C#(第4版)》

时间: 2024-10-11 12:06:54

C#夯实基础之接口(《CLR via C#》读书笔记)的相关文章

《DirectX 9.0 3D游戏开发编程基础》 第一章 初始化Direct3D 读书笔记

REF设备 参考光栅设备,他能以软件计算方式完全支持Direct3D Api.借助Ref设备,可以在代码中使用那些不为当前硬件所支持的特性,并对这此特性进行测试. D3DDEVTYPE 在程序代码中,HAL设备用值D3DDEVTYPE_HAL来表示.该值是一个枚举变量.REF设备用D3DDEVTYPE_REF来表示.这种类型非常重要,你需要铭记,因为在创建设备的时候,我们必须指定使用哪种设备类型. COM(组件对象模型) 创建COM接口时不可以使用c++关键字new.此外使用完接口,应调用Rel

Clr Via C#读书笔记---线程基础

进程与线程 进程:应用程序的一个实例使用的资源的集合.每个进程都被赋予了一个虚拟地址空间. 线程:对CPU进行虚拟化,可以理解为一个逻辑CPU. 线程要素 线程包括以下要素: 1. 线程内核对象, 其中包含 1)一组对线程进行描述的属性 2)线程上下文,即包含CPU寄存器的集合的一个内存块 2. 线程环境块,在用户模式中分配和初始化的一个内存块,其中包含 1)线程的异常处理链首 2)线程的"线程本地存储数据" 3)由GDI和OpenGL图形使用的一些数据结构 3. 用户模式栈 1)存储

Clr Via C#读书笔记---程序集的加载和反射

#1 加载程序集 Assembly.Load: public class Assembly { public static Assembly Load(AssemblyName assemblyRef); public static Assembly Load(String assemblyString); } 在内部,Load导致CLR向应用程序集应用一个版本绑定重定向策略,并在GAC中查找程序集.如果传递的是一个弱命名程序集,不会应用版本绑定重定向策略,也不会去GAC中查找程序集. AppD

Clr Via C#读书笔记---CLR寄宿和应用程序域

#1 CLR寄宿: 开发CLR时,Microsoft实际是将他实现成包含在一个dll中的COM服务器.Microsoft为CLR定义了一个标准的COM接口,并为该接口和COM服务器分配了GUID.安装.NET Framework时,代表CLR的COM服务器在Windows注册表中注册. 任何Windows应用程序都可以寄宿CLR,非托管宿主应该调用MetaHost.h文件中声明的CLRCreateInstance函数,该函数是在MSCoreEE.dll文件中实现的,该dll被称为"垫片"

Clr Via C#读书笔记---垃圾回收机制

#1 垃圾回收平台的基本工作原理: 访问一个资源所需的具体步骤: 1)调用IL指令newobj,为代表资源的类型分配内存.在C#中使用new操作符,编译器就会自动生成该指令.2)初始化内存,设置资源的初始状态,使资源可用.类型的实例构造器负责设置该初始状态.3)访问类型的成员(可根据需要反复)来使用资源.4)摧毁资源的状态以进行清理.正确清理资源的代码要放在Finalize, Dispose和Close方法.5)释放内存.垃圾回收器独自负责这一步. 托管堆如何知道应用程序不再用一个对象? 托管堆

Clr Via C#读书笔记---计算限制的异步操作

线程池基础 1,线程的创建和销毁是一个昂贵的操作,线程调度以及上下文切换耗费时间和内存资源. 2,线程池是一个线程集合,供应你的用程序使用. 3,每个CLR有一个自己的线程池,线程池由CLR控制的所有的AppDomain共享. 4,CLR初始化的时候,线程池没有线程的. 5,线程池维护一个操作请求队列.当应用程序想要执行一个一步操作的时候,就调用某个方法.将记录项(empty)追加到线程池队列中,然后线程池代码从队列中提取这个记录项,然后将记录项派遣(dispatch)给一个线程池的线程.当线程

Clr Via C#读书笔记---I/O限制的异步操作

widows如何执行I/O操作      构造调用一个FileStream对象打开一个磁盘文件-----FileStream.Read方法从文件中读取数据(此时线程从托管代码转为本地/用户模式代码)----Read在内部调用win32ReadFile函数-----ReadFile分配一个小的数据结构(I/O请求包,简称IRP)----IRP请求结构初始化(包括:一个文件句柄,文件一个偏移量,一个byte[]数组地址,要传输的字节数,以及其他常规性内容)------初始化后ReadFile将线程从

CLR via C#读书笔记 CLR寄宿和AppDomain

寄宿 寄宿是指让其他应用程序(非托管代码)使用CLR的能力,比如自己用C++开发的窗体能创建CLR实例. 托管代码也能调用非托管代码 [DllImport("kernel32.dll")] public static extern int WinExec(string exeName, int operType); 通常会调用win32 api,但是要查文档才知道怎么定义extern方法 CLR实际上被实现为COM服务器,可以通过CoCreateInstance或CLRCreateIn

HTML5和CSS3基础教程(第8版)-读书笔记(2)

第7章 CSS构造模块 7.1 构造样式规则 样式表中包含了定义网页外观的规则.样式表中的每条规则都有两个主要部分:选 择 器(selector) 和 声 明 块(declaration block). 选择器决定哪些元素受到影响:声明块由一个或多个属性 - 值对(每个属性 -值对构成一条声明,declaration)组成,它们指定应该做什么. 声明块内的每条声明都是一个由冒号隔开.以分号结尾的属性- 值对. 声明的顺序并不重要,除非对相同的属性定义了两次. 在样式规则中可以添加额外的空格.制表