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

Posted 沉香

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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);
    }
}

使用IComparable接口的示例
  • 第一行代码创建了包含5个无序整数的数组
  • 第二行代码使用了Array类的静态Sort方法来排序元素
  • 用foreach循环输出它们,显式以升序排序的数字
var myInt=new[]{20,4,16,9,2};
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);
    }
}

声明接口


上一节使用的是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(5);
        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(5);
        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[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());
            }
        }
    }
}

以上是关于C#图解教程 第十五章 接口的主要内容,如果未能解决你的问题,请参考以下文章

Flask 学习笔记-第十五章-应用编程接口

C#图解教程 第十九章 LINQ

第十五章 枚举类型和位标志

王爽《汇编语言》第三版 第十五章 外中断

SpringBoot | 第十五章:基于Postman的RESTful接口测试

C# 图解教程 第五章