C#图解教程 第十五章 接口

接口

什么是接口


接口是指定一组函数成员而不实现它们的引用类型。所以只能类和结构来实现接口。
这种描述比较抽象,直接来看个示例。
下例中,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=35};
        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=35}; 
       var b=new CB(){First="Jane",Last="Doe",PersonsAge=33};
       PrintInfo(a);
       PrintInfo(b);
    }
}
image
使用IComparable接口的示例
  • 第一行代码创建了包含5个无序整数的数组
  • 第二行代码使用了Array类的静态Sort方法来排序元素
  • 用foreach循环输出它们,显式以升序排序的数字
Array.Sort(myInt); 
foreach(var i in myInt)
{
    Console.WriteLine("{0}",i);
}

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

class MyClass
{
 public int TheValue;
}
...
MyClass[] mc=new MyClass[5];
...
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 -1; 
         if(this.TheValue>mc.TheValue)return  1; return 0;
    }
}

例:完整示例代码

class MyClass:IComparable
{ 
    public int TheValue; 
    public int CompareTo(object obj)
    { 
         var mc=(MyClass)obj; 
         if(this.TheValue<mc.TheValue)return -1; 
         if(this.TheValue>mc.TheValue)return  1; return 0;
    }
} 
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[] {20,4,16,9,2};
        MyClass[] mcArr=new MyClass[5];
        for(int i=0;i<5;i++)
        {
            mcArr[i]=new MyClass();
            mcArr[i].TheValue=myInt[i];
        }
        PrintOut("Initial Order: ",mcArr);
        Array.Sort(mcArr);
        PrintOut("Sorted Order: ",mcArr);
    }
}
image

声明接口


上一节使用的是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");
    }
}
image

接口是引用类型


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

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

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

例:类和接口的引用

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"); //调用引用方法
    }
}
image
image

接口和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(5);
        Console.WriteLine("Value = {0}",data.GetData());
    }
}
image

实现具有重复成员的接口


由于接口可以多实现,有可能多个接口有相同的签名和返回类型。编译器如何处理这种情况呢?
例: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");
    }
}
image

多个接口的引用


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

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

  • 通过类对象
  • 通过指向IIfc1接口的引用
  • 通过指向IIfc2接口的引用
image
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");
    }
}
image

派生成员作为实现


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

  • 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");
    }
}
image

显式接口成员实现


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

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

例: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");
    }
}
image
image

如果有显式接口成员实现,类级别的实现是允许的,但不是必需的。显式实现满足了类或结构必须实现的方法。因此,我们可以有如下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(5);
        Console.WriteLine("{0}",data.GetData());
    }
}
image

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


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[3];
        animalArray[0]=new Cat();
        animalArray[1]=new Bird();
        animalArray[2]=new Dog();
        foreach(Animal a in animalArray)
        {
            ILiveBirth b= a as ILiveBirth;//如果实现ILiveBirth
            if(b!=null)
            {
            Console.WriteLine("Baby is called: {0}",b.BabyCalled());
            }
        }
    }
}
image

image

作者:Moonache
出处:http://www.cnblogs.com/moonache/
原文链接:https://www.cnblogs.com/moonache/p/6346424.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • 1.C和C++的区别?C++的特性?面向对象编程的好处? 答:c++在c的基础上增添类,C是一个结构化语言,它的重...
    杰伦哎呦哎呦阅读 9,496评论 0 45
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • 设计模式概述 在学习面向对象七大设计原则时需要注意以下几点:a) 高内聚、低耦合和单一职能的“冲突”实际上,这两者...
    彦帧阅读 3,737评论 0 14
  • 作者:毓涛 今天是参加夏令营的第二天,我们要去一个非常著名的景点——张掖丹霞国家地...
    齐天小圣6阅读 210评论 0 0
  • 执行Maven Install打包的时候,提示以下警告信息: 解决方法: 打开项目属性》Resources,按下图...
    Scorpio_cc阅读 20,933评论 1 4