接口

什么是接口


接口是指定一组函数成员而不实现它们的引用类型。所以只能类和结构来实现接口。
这种描述比较抽象,直接来看个示例。
下例中,Main方法创建并初始化了一个CA类的对象,并将该对象传递给PrintInfo方法。

class CA
{
public string Name;
public int Age;
}
class CB
{
public string First;
public string Last;
public double PersonsAge;
}
class Program
{
static void PrintInfo(CA item)
{
Console.WriteLine("Name: {0},Age {1}",item.Name,item.Age);
}
static void Main()
{
CA a=new CA(){Name="John Doe",Age=};
PrintInfo(a);
}
}

只要传入的是CA类型的对象,PrintInfo就能正常工作。但如果传入的是CB,就不行了。
现在的代码不能满足上面的需求,原因有很多。

  • PrintInfo的形参指明了实参必须为CA类型的对象
  • CB的结构与CA不同,字段的名称和类型与CA不一样

接口解决了这一问题。

  • 声明一个IInfo接口,包含两个方法–GetName和GetAge,每个方法都返回string
  • 类CA和CB各自实现了IInfo接口,并实现了两个方法
  • Main创建了CA和CB的实例,并传入PrintInfo
  • 由于类实例实现了接口,PrintInfo可以调用那两个方法,每个类实例执行各自的方法,就好像是执行自己类声明中的方法
interface IInfo       //声明接口
{
string GetName();
string GetAge();
}
class CA:IInfo //声明了实现接口的CA类
{
public string Name;
public int Age;
public string GetName(){return Name;}
public string GetAge(){return Age.ToString();}
}
class CB:IInfo //声明了实现接口的CB类
{
public string First;
public string Last;
public double PersonsAge;
public string GetName(){return First+""+Last;}
public string GetAge(){return PersonsAge.ToString();}
}
class Program
{
static void PrintInfo(IInfo item)
{
Console.WriteLine("Name: {0},Age {1}",item.GetName(),item.GetAge());
}
static void Main()
{
var a=new CA(){Name="John Doe",Age=};
var b=new CB(){First="Jane",Last="Doe",PersonsAge=};
PrintInfo(a);
PrintInfo(b);
}
}

使用IComparable接口的示例
  • 第一行代码创建了包含5个无序整数的数组
  • 第二行代码使用了Array类的静态Sort方法来排序元素
  • 用foreach循环输出它们,显式以升序排序的数字
var myInt=new[]{,,,,};
Array.Sort(myInt);
foreach(var i in myInt)
{
Console.WriteLine("{0}",i);
}

Sort方法在int数组上工作良好,但如果我们尝试在自己的类上使用会发生什么呢?

class MyClass
{
public int TheValue;
}
...
MyClass[] mc=new MyClass[];
...
Array.Sort(mc);

运行上面的代码,将会得到一个异常。Sort并不能对MyClass对象数组排序,因为它不知道如何比较自定义的对象。Array类的Sort方法其实依赖于一个叫做IComparable的接口,它声明在BCL中,包含唯一的方法CompareTo。

public interface IComparable
{
int CompareTo(object obj);
}

尽管接口声明中没有为CompareTo方法提供实现,但IComparable接口的.NET文档中描述了该方法应该做的事情,可以在创建实现该接口的类或结构时参考。
文档中写到,在调用CompareTo方法时,它应该返回以下几个值:

  • 负数值 当前对象小于参数对象
  • 整数值 当前对象大于参数对象
  • 零 两个对象相等

我们可以通过让类实现IComparable来使Sort方法可以用于MyClass对象。要实现一个接口,类或结构必须做两件事情:

  • 必须在基类列表后面列出接口名称
  • 必须实现接口的每个成员

例:MyClass中实现了IComparable接口

class MyClass:IComparable
{
public int TheValue;
public int CompareTo(object obj)
{
var mc=(MyClass)obj;
if(this.TheValue<mc.TheValue)return -;
if(this.TheValue>mc.TheValue)return ;
return ;
}
}

例:完整示例代码

class MyClass:IComparable
{
public int TheValue;
public int CompareTo(object obj)
{
var mc=(MyClass)obj;
if(this.TheValue<mc.TheValue)return -;
if(this.TheValue>mc.TheValue)return ;
return ;
}
}
class Program
{
static void PrintInfo(string s,MyClass[] mc)
{
Console.WriteLine(s);
foreach(var m in mc)
{
Console.WriteLine("{0}",m.TheValue);
}
Console.WriteLine("");
}
static void Main()
{
var myInt=new[] {,,,,};
MyClass[] mcArr=new MyClass[];
for(int i=;i<;i++)
{
mcArr[i]=new MyClass();
mcArr[i].TheValue=myInt[i];
}
PrintOut("Initial Order: ",mcArr);
Array.Sort(mcArr);
PrintOut("Sorted Order: ",mcArr);
}
}

声明接口


上一节使用的是BCL中已有的接口。现在我们来看看如何声明接口。
关于声明接口,需要知道的重要事项如下:

  • 接口声明不能包含以下成员

    • 数据成员
    • 静态成员
  • 接口声明只能包含如下类型的非静态成员函数的声明
    • 方法
    • 属性
    • 事件
    • 索引器
  • 这些函数成员的声明不能包含任何实现代码,只能用分号
  • 按照惯例,接口名称以大写字母I(Interface)开始
  • 与类和结构一样,接口声明也可以分布

例:两个方法成员接口的声明

  关键字     接口名称
↓ ↓
interface IMyInterface1
{
int DoStuff(int nVar1,long lVar2); //分号替代了主体
double DoOtherStuff(string s,long x);
}

接口的访问性和接口成员的访问性之间有一些重要区别

  • 接口声明可以有任何的访问修饰符public、protected、internal或private
  • 然而,接口的成员是隐式public的,不允许有任何访问修饰符,包括public
接口允许访问修饰符

public interface IMyInterface2
{
private int Method1(int nVar1); //错误

接口成员不允许访问修饰符
}

实现接口


只有类和结构才能实现接口。

  • 在基类列表中包括接口名称
  • 实现每个接口成员

例:MyClass实现IMyInterface1接口

class MyClass:IMyInterface1
{
int DoStuff(int nVar1,long lVar2)
{...} //实现代码
double DoOtherStuff(string s,long x)
{...} //实现代码
}

关于实现接口,需要了解以下重要事项:

  • 如果类实现了接口,它必须实现接口的所有成员
  • 如果类从基类继承并实现了接口,基类列表中的基类名称必须放在所有接口之前。
           基类必须放在最前面       接口名
↓ ↓
class Derived:MyBaseClass,IIfc1,IEnumerable,IComparable
简单接口示例
interface IIfc1
{
void PrintOut(string s);
}
class MyClass:IIfc1
{
public void PrintOut(string s)
{
Console.WriteLine("Calling through: {0}",s);
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
mc.PrintOut("object");
}
}

接口是引用类型


接口不仅是类或结构要实现的成员列表。它是一个引用类型。
我们不能直接通过类对象的成员访问接口。然而,我们可以通过把类对象引用强制转换为接口类型来获取指向接口的引用。一旦有了接口引用,我们就可以使用点号来调用接口方法。

例:从类对象引用获取接口引用

IIfc1 ifc=(IIfc1)mc;         //转换为接口,获取接口引用
ifc.PrintOut("interface"); //使用接口的引用调用方法

例:类和接口的引用

interface IIfc1
{
void PrintOut(string s);
}
class MyClass:IIfc1
{
public void PrintOut(string s)
{
Console.WriteLine("Calling through: {0}",s);
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
mc.PrintOut("object"); //调用类对象的实现方法
IIfc1 ifc=(IIfc1)mc;
ifc.PrintOut("interface"); //调用引用方法
}
}

接口和as运算符


上一节,我们已经知道可以使用强制转换运算符来获取对象接口引用,另一个更好的方式是使用as运算符。
如果我们尝试将类对象引用强制转换为类未实现的接口的引用,强制转换操作会抛出异常。我们可以通过as运算符来避免该问题。

  • 如果类实现了接口,表达式返回指向接口的引用
  • 如果类没有实现接口,表达式返回null
ILiveBirth b=a as ILiveBirth;
if(b!=null)
{
Console.WriteLine("Baby is called: {0}",b.BabyCalled());
}

实现多个接口


  • 类或结构可以实现多个接口
  • 所有实现的接口必须列在基类列表中并以逗号分隔(如果有基类名称,则在其后)
interface IDataRetrieve{int GetData();}
interface IDataStore{void SetData(int x);}
class MyData:IDataRetrieve,IDataStore
{
int Mem1;
public int GetData(){return Mem1;}
public void SetData(int x){Mem1=x;}
}
class Program
{
static void Main()
{
var data=new MyData();
data.SetData();
Console.WriteLine("Value = {0}",data.GetData());
}
}

实现具有重复成员的接口


由于接口可以多实现,有可能多个接口有相同的签名和返回类型。编译器如何处理这种情况呢?
例:IIfc1和IIfc2具有相同签名

interface IIfc1
{
void PrintOut(string s);
}
interface IIfc2
{
void PrintOut(string t);
}

答案是:如果一个类实现了多接口,并且其中有些接口有相同签名和返回类型,那么类可以实现单个成员来满足所有包含重复成员的接口。
例:MyClass 类实现了IIfc1和IIfc2.PrintOut满足了两个接口的需求。

class MyClass:IIfc1,IIfc2
{
public void PrintOut(string s)//两个接口单一实现
{
Console.WriteLine("Calling through: {0}",s);
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
mc.PrintOut("object");
}
}

多个接口的引用


如果类实现了多接口,我们可以获取每个接口的独立引用。

例:下面类实现了两个具有当PrintOut方法的接口,Main中以3种方式调用了PrintOut。

  • 通过类对象
  • 通过指向IIfc1接口的引用
  • 通过指向IIfc2接口的引用

interface IIfc1
{
void PrintOut(string s);
}
interface IIfc2
{
void PrintOut(string t);
}
class MyClass:IIfc1,IIfc2
{
public void PrintOut(string s)
{
Console.WriteLine("Calling through: {0}",s);
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
IIfc1 ifc1=(IIfc1)mc;
IIfc2 ifc2=(IIfc2)mc;
mc.PrintOut("object");
ifc1.PrintOut("interface 1");
ifc2.PrintOut("interface 2");
}
}

派生成员作为实现


实现接口的类可以从它的基类继承实现的代码。
例:演示 类从基类代码继承了实现

  • IIfc1是一个具有PrintOut方法成员的接口
  • MyBaseClass包含一个PrintOut方法,它和IIfc1匹配
  • Derived类有空的声明主体,但它派生自MyBaseClass,并在基类列表中包含了IIfc1
  • 即使Derived的声明主体为空,基类中的代码还是能满足实现接口方法的需求
interface IIfc1
{
void PrintOut(string s);
}
class MyBaseClass
{
public void PrintOut(string s)
{
Console.WriteLine("Calling through: {0}",s);
}
}
class Derived:MyBaseClass,IIfc1
{
}
class Program
{
static void Main()
{
var d=new Derived();
d.PrintOut("object");
}
}

显式接口成员实现


上面,我们已经看到了单个类可以实现多个接口需要的所有成员。
但是,如果我们希望为每个接口分离实现该怎么做呢?这种情况下,我们可以创建显式接口成员实现

  • 与所有接口实现相似,位于实现了接口的类或结构中
  • 它使用限定接口名称来声明,由接口名称和成员名称以及它们中间的点分隔符号构成
class MyClass:IIfc1,IIfc2
{
void IIfc1.PrintOut(string s)
{...}
void IIfc2.PrintOut(string s)
{...}
}

例:MyClass为两个解耦的成员声明了显式接口成员实现。

interface IIfc1
{
void PrintOut(string s);
}
interface IIfc2
{
void PrintOut(string t);
}
class MyClass:IIfc1,IIfc2
{
void IIfc1.PrintOut(string s)
{
Console.WriteLine("IIfc1: {0}",s);
}
void IIfc2.PrintOut(string s)
{
Console.WriteLine("IIfc2: {0}",s);
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
IIfc1 ifc1=(IIfc1)mc;
ifc1.PrintOut("interface 1");
IIfc2 ifc2=(IIfc2)mc;
ifc2.PrintOut("interface 2");
}
}



如果有显式接口成员实现,类级别的实现是允许的,但不是必需的。显式实现满足了类或结构必须实现的方法。因此,我们可以有如下3种实现场景。

  • 类级别实现
  • 显式接口成员实现
  • 类级别和显式接口成员实现
访问显式接口成员实现

显式接口成员实现只可以通过指向接口的引用来访问。即其他的类成员都不可以直接访问它们。
例:如下MyClass显式实现了IIfc1接口。注意,即使是MyClass的另一成员Method1,也不可以直接访问显式实现。

  • Method1的前两行编译错误,因为方法在尝试直接访问实现
  • 只有Method1的最后一行代码才可以编译,因此它强制转换当前对象的引用(this)为接口类型的引用,并使用这个指向接口的引用来调用显式接口实现
class MyClass:IIfc1
{
void IIfc1.PrintOut(string s)
{
Console.WriteLine("IIfc1");
}
public void Method1()
{
PrintOut("..."); //编译错误
this.PrintOut("..."); //编译错误
((IIfc1)this).PrintOut("...");

转换为接口引用
}
}

这个限制对继承产生了重要影响。由于其他类成员不能直接访问显式接口成员实现,衍生类的成员也不能直接访问它们。它们必须总是通过接口的引用来访问。

接口可以继承接口


之前我们已经知道接口实现可以从基类继承,而接口本身也可以从一个或多个接口继承。

  • 要指定某个接口继承其他的接口,应在接口声明中把某接口以逗号分隔的列表形式放在接口名称的冒号之后
  • 与类不同,它在基类列表中只能有一个类名,接口可以在基接口列表中有任意多个接口
    • 列表中的接口本身可以继承其他接口
    • 结果接口包含它声明的所有接口和所有基接口的成员
interface IDataIO:IDataRetrieve,IDataStore
{
...
}

例:IDataIO接口继承了两个接口

interface IDataRetrieve
{
int GetData();
}
interface IDataStore
{
void SetData(int x);
}
interface IDaTaIO:IDataRetrieve,IDataStore
{
}
class MyData:IDataIO
{
int nPrivateData;
public int GetData()
{
return nPrivateData;
}
public void SetData(int x)
{
nPrivateData=x;
}
}
class Program
{
static void Main()
{
var data=new MyData();
data.SetData();
Console.WriteLine("{0}",data.GetData());
}
}

不同类实现一个接口的示例


interface ILiveBirth              //声明接口
{
string BabyCalled();
}
class Animal{} //基类Animal
class Cat:Animal,ILiveBirth //声明Cat类
{
string ILiveBirth.BabyCalled()
{
return "kitten";
}
}
class Dog:Animal,ILiveBirth //声明DOg类
{
string ILiveBirth.BabyCalled()
{
return "puppy";
}
}
class Bird:Animal //声明Bird类
{
}
class Program
{
static void Main()
{
Animal[] animalArray=new Animal[];
animalArray[]=new Cat();
animalArray[]=new Bird();
animalArray[]=new Dog();
foreach(Animal a in animalArray)
{
ILiveBirth b= a as ILiveBirth;//如果实现ILiveBirth
if(b!=null)
{
Console.WriteLine("Baby is called: {0}",b.BabyCalled());
}
}
}
}

C#图解教程 第十五章 接口的更多相关文章

  1. C#图解教程 第二十五章 其他主题

    其他主题 概述字符串使用 StringBuilder类把字符串解析为数据值关于可空类型的更多内容 为可空类型赋值使用空接合运算符使用可空用户自定义类型 Main 方法文档注释 插入文档注释使用其他XM ...

  2. python 教程 第十五章、 结构布局

    第十五章. 结构布局 #!/usr/bin/env python #(1)起始行 "this is a module" #(2)模块文档 import sys #(3)模块导入 d ...

  3. Flask 教程 第十五章:优化应用结构

    本文翻译自The Flask Mega-Tutorial Part XV: A Better Application Structure 这是Flask Mega-Tutorial系列的第十五部分,我 ...

  4. C#图解教程 第十九章 LINQ

    LINQ 什么是LINQLINQ提供程序 匿名类型 方法语法和查询语法查询变量查询表达式的结构 from子句join子句什么是联结查询主体中的from-let-where片段 from子句let子句w ...

  5. C#图解教程 第二十四章 反射和特性

    反射和特性 元数据和反射Type 类获取Type对象什么是特性应用特性预定义的保留的特性 Obsolete(废弃)特性Conditional特性调用者信息特性DebuggerStepThrough 特 ...

  6. C#图解教程 第十四章 事件

    事件 发布者和订阅者源代码组件概览声明事件订阅事件触发事件标准事件的用法 通过扩展EventArgs来传递数据移除事件处理程序 事件访问器 事件 发布者和订阅者 很多程序都有一个共同的需求,既当一个特 ...

  7. C#图解教程 第十六章 转换

    转换 什么是转换隐式转换显式转换和强制转换 强制转换 转换的类型数字的转换 隐式数字转换溢出检测上下文 1.checked和unchecked运算符2.checked语句和unchecked语句 显式 ...

  8. C#图解教程 第十二章 数组

    数组 数组 定义重要细节 数组的类型数组是对象一维数组和矩形数组实例化一维数组或矩形数组访问数组元素初始化数组 显式初始化一维数组显式初始化矩形数组快捷语法隐式类型数组综合内容 交错数组 声明交错数组 ...

  9. C#图解教程 第二十二章 异常

    异常 什么是异常try语句 处理异常 异常类catch 子句使用特定catch子句的示例catch子句段finally块为异常寻找处理程序更进一步搜索 一般法则搜索调用栈的示例 抛出异常不带异常对象的 ...

随机推荐

  1. 小甲鱼OD学习第6讲

    这次我们的任务是破解这个通讯录的软件,首先,我们在通讯录一个分组添加第5个人,发现弹出对话框,限制每组只能添加4个人 并且发现最多只能添加3个分组 我们把程序载入OD,运行,添加人,这个时候点击暂停, ...

  2. Install Centrifugo and quick start

    Install Centrifugo and quick start Go is a perfect language - it gives developers an opportunity to ...

  3. 一个shell脚本给客户使用服务器生成一个序列号

    #!/bin/bash interface=`ls /sys/class/net|grep en|awk 'NR==1{print}'` if [ ! -e /etc/adserver/.seq.in ...

  4. appium+Python 启动app(三)登录

    我们根据前面的知识点,用uiautomatorviewer工具来获取我们当前的元素 (注:uiautomatorviewer 是 android sdk 自带的) 知识点:appium的webdriv ...

  5. [翻译]【目录】编写高性能 .NET 代码

    本篇是 Writing High-Performance .NET Code 的目录索引,翻译内容不定时更新,目录也会同步修改. 性能测量及工具 选择什么来衡量 平均数vs百分比 工具介绍 Visua ...

  6. $.ajax的一些坑啊

    1.如果发送ajax返回的数据为json务必设置其 Content-Type:application/json;charset=UTF-8 不然会导致其success:function(data)中的 ...

  7. js利用闭包封装自定义模块的几种方法

    1.自定义模块: 具有特定功能的js文件 将所有的数据和功能都封装在一个函数的内部 只向外暴露一个包含有n个方法的对象或者函数 模块使用者只需要通过模块暴露的对象调用方法来实现相对应的功能 1.利用函 ...

  8. MFC窗口创建、销毁消息流程

    MFC应用程序创建窗口的顺序 1.PreCreateWindow()该函数是一个重载函数,在窗口被创建前,可以在该重载函数中改变创建参数,(可以设置窗口风格等等) 2.PreSubclassWindo ...

  9. #pragma预处理命令

    #pragma comment(lib,"XXX.lib") 表示链接XXX.lib这个库,和在工程设置里写上XXX.lib的效果一样. #pragma comment(linke ...

  10. LNK2026 模块对于 SAFESEH 映像是不安全的

    解决方法如下: 配置属性 -> 链接器 -> 命令行 位置添加如下内容: /SAFESEH:NO