《随笔二十一》——C#中的 “ 接口 、 as 运算符”

目录

什么是接口

使用BCL中的接口IComparable 接口的示例 (269P)

声明接口 (272P)

实现接口 (273P)

接口和抽象类的相同点和不同点

接口是引用类型 (275P)

接口和 as 运算符 (276P)

实现具有重复成员的接口 (277P)

将类对象转换为多个其实现接口的引用 (279P)

显式接口成员实现 (281P)

访问显式接口成员实现 (283P)

派生成员作为实现 (280P)

接口可以继承别的接口 (283P)

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


什么是接口


接口是一种引用类型,它指定一组函数成员,但不实现它们。所以只能类和结构来实现接口。

先看一个使用接口的程序示例:

namespace HelloWorld_Console
{
    interface IInfo  //声明接口
    {
        string GetName();
        string GetAge();
    }
    class CA: IInfo //声明一个类,该类实现了接口IInfo
    {
        public string Name;
        public int Age;
//注意, 在类中实现接口的成员函数,该成员函数必须是public的,不能是其它的访问修饰符,否则错误
        public string GetName()
        {
            return Name;
        }
        public string GetAge()
        {
            return Age.ToString();
        }
    }
    class CB: IInfo
    {
        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)
        {
            WriteLine($"Name:{item.GetName()},Age: {item.GetAge()}");
        }
        static void Main(string[] args)
        {
            CA a = new CA() { Name = "huangchengtao ",  Age = 21 };
            CB b = new CB() { First = "huangchengjian ",  Last = "Doe", PersonsAge = 14 };
            //对象的引用能自动转换为 它们实现的接口的引用
            PrintInfo(a);
            PrintInfo(b);
            ReadKey();
        }
    }
}

我们不管传入什么样的类类型到函数PrintInfo ( )中,它都能正确的处理。

 


使用BCL中的接口IComparable 接口的示例 (269P)


先看一个代码示例:

 class Program
    {
       
        static void Main(string[] args)
        {
            var myInt = new[] { 20, 4, 5, 6, 7, 8, 9 };
            Array.Sort(myInt);
            foreach(var tt in myInt)
            {
                WriteLine(tt);
            }
            ReadKey();
        }
    }

输出结果为:
4
5
6
7
8
9
20

Sort方法在int数组上工作得很好, 但是我们不可以在自定义的类或结构中使用,因为是它不知道如何比较用于自定义的对象以及如何进行排序。    Array 类的Sort方法其实依赖于一个叫做IComparable的接口,它声明在BCL中,包含唯一的方法CompareTo。

下面的代码是IComparable的接口的声明:

public interface IComparable
{
   int ComparaTo(object obj); //该函数在接口中不能有实现
}

该函数的返回值有:

  • 负数值——当前对象小于参数对象;
  • 正数值  —— 当前对象大于参数对象
  • 零  —— 两个对象在比较时相等。

Sort使用的算法可以使用元素的 CompareTo 方法来决定两个元素的次序。int类型实现了 IComparable, 但是自定义的类或结构没有, 因此当Sort尝试调用自定义的类或结构不存在的CompareTo方法时会抛出异常。

 

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

  • 必须在基类列表后面列出接口名称;
  • 必须为接口的每一个成员提供实现。

下面看代码示例:

namespace HelloWorld_Console
{
    class MyClass : IComparable  // Class implements interface.
    {
        public int TheValue;
        public int CompareTo(object obj)    // Implement the method.
        {
            MyClass mc = (MyClass)obj;

            if (this.TheValue < mc.TheValue)
                return -1;

            if (this.TheValue > mc.TheValue)
                return 1;

            return 0;
        }
    }
    class Program
    {
        static void PrintOut(string s, params MyClass[] mc)
        {
            Console.Write(s);
            foreach (var m in mc)
                Console.Write("{0} ", m.TheValue);

            Console.WriteLine("");
        }
        static void Main(string[] args)
        {
            var myInt = new[] { 20, 4, 16, 9, 2 };

            MyClass[] mcArr = new MyClass[5];   // Create array of MyClass objs.
            for (int i = 0; i < 5; i++)     // Initialize the array.
            {
                mcArr[i] = new MyClass();
                mcArr[i].TheValue = myInt[i];
            }

            PrintOut("Initial Order: ", mcArr);           // Print the initial array.

            Array.Sort(mcArr);                            // Sort the array.
            PrintOut("Sorted Order:  ", mcArr);            // Print the sorted array.
            ReadKey();
        }
    }
}

输出结果为;

Initial Order: 20 4 16 9 2
Sorted Order:  2 4 9 16 20

注意:如果仅仅声明CompareTo方法是不够的, 必须实现接口,也就是把接口名称放在基类列表中。


声明接口 (272P)


 使用关键字 interface 来定义接口,语法为:

interface  myInterface
{
  //interface members
}

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

  • 数据成员
  • 静态成员
  • 常量
  • 析构和构造函数

接口声明只能包含如下类型的非静态成员函数的声明:

  • 方法
  • 属性
  • 事件
  • 索引器

 

  • 这些函数成员的声明不能包含任何实现代码, 而在每一个成员声明的主体后必须使用分号。
  • 按照惯例,接口名称必须从大写的I开始(比如ISaveable )。
  • 与类和结构一样,接口声明还可以分隔成分部接口声明。
  •  在默认情况下, 接口是内部的(internal)—— 即只有当程序集中的代码能够访问。    可以使用关键字 internal 写在  interface 的前面 来显式指定这一点。
  •    还可以使用关键字public 指定接口是公共的—— 即可以在任何程序集中的代码来访问。接口声明还可以用访问修饰符 protected、private。
  • 接口成员 不能使用   abstract 和 sealed、static、virtual   来定义接口成员
  •   接口的成员是隐式public的,  接口的成员不允许有任何访问修饰符, 包括public.
  • 接口成员不能包含任何代码体,而在每一个成员声明的主体后必须使用分号。

实现接口 (273P)


 只有类或结构才能实现接口。要实现接口,类或结构必须:

  • 在基类列表中包括接口名称;
  • 每一个接口的成员提供实现。

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

  •  如果类实现了接口,它必须实现接口的所有成员。
  • 类或结构可以实现任意数量的接口。
  • 如果类实现了接口(即在该类 :号后面指定接口名), 但是它并没有实现接口的所有成员,就错误。

 也可以使用 与类继承类似的方式来指定接口的继承。 主要区别是可以使用多个接口:

interface IMyInterface : IMyBaseInterface, IMyBaseInterface2 
{ 
// Interface members 
}
  • 接口不是类,所以没有继承object, 但是object 的成员  可以通过接口类型的变量来访问。  如上所述, 不能用实例化类的方式来实例化接口 。

 除了可以在类的后面指定基类外, 还可以在冒号的后面指定支持的接口。  如果指定了基类,它必须紧跟冒号之后, 之后才是接口。  如果未指定基类, 接口就紧跟在冒号的后面。记住, 必须使用逗号来分隔基类名 ( 如果有基类的话 ) 和接口名。

class MyClass : MyBase,IMyBaseInterface, IMyBaseInterface2
{
   // Class members
 
}
  • 上述的类指定了一个基类,两个接口。 支持该接口的类必须实现所有接口的成员, 但如果不想使用给定的接口成员, 可以提供一种 “ 空” 的实现方式 (没有函数代码), 还可以把接口成员实现为抽象类中的抽象成员。
namespace HelloWorld_Console
{
    interface IDataRetrieve { int GetData(); } // Declare interface.
    interface IDataStore { void SetData(int x); } // Declare interface.
    class MyData : IDataRetrieve, IDataStore // Declare class.
    {
        int Mem1; // Declare field.
        public int GetData() { return Mem1; }
        public void SetData(int x) { Mem1 = x; }
    }
    class Program
    {
        static void Main() // Main
        {
            MyData data = new MyData();
            data.SetData(5);
            Console.WriteLine("Value = {0}", data.GetData());
        }
    }
}

 

《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第1张图片 类实现了多个接口

接口和抽象类的相同点和不同点


相同点:

  • 抽象类和接口都包含可以由其派生类继承的成员。 它们都不能使用 new 的方式 直接实例化, 但是可以声明接口和抽象类的变量。

不同点:

  • 派生类只能有一个直接基类,即只能直接继承一个抽象类 (但可以用一个继承链包含多个抽象类); 但是类可以继承多个接口
  • 抽象类可以拥有抽象成员 (没有代码体,但是必须在派生类中实现,否则,派生类也将是一个抽象类) 和 非抽象成员 (可以是普通的函数,也可以是虚函数 —— 可以在派生类重写,也可以不重写)。
  • 继承接口的类,必须实现该接口中所有的成员。
  • 抽象类主要用于某些对象的基类,这些对象共享某些主要的特性。 例如:共同的目的和结构。

    接口主要用于类,这些类存在根本性的区别, 但仍然可以完成某些相同的任务。

《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第2张图片

 


接口是引用类型 (275P)


  接口不仅仅是类或结构要实现的成员列表。它是一个引用类型。

我们不能直接通过类对象的成员直接访问接口。然而, 我们可以通过把类对象引用强制转换为接口类型来获取对接口的引用。一旦有了接口的引用, 我们就可以使用点号来调用接口的方法。

下面看一个代码示例:

namespace HelloWorld_Console
{
    interface IIfc1
    {
        void PrintOut(string s);
    }
    class MyClass : IIfc1
    {
        public void PrintOut(string s)
        {
            WriteLine($"Calling through : {s}");
        }
        public void Test()
        {
            WriteLine("MyClass :: Test()");
        }
    }
    class Program
    {

        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
            mc.PrintOut("huang");

            IIfc1 ifc = (IIfc1)mc; //将类对象的引用转换为接口类型的引用
            ifc.PrintOut("interface"); //调用引用方法

            ifc.Test(); // 接口的引用不可以访问派生类中其它的成员,它只可以访问接口中的成员
            ReadKey();
        }
    }
}
《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第3张图片 类对象的引用和接口的引用

 


接口和 as 运算符 (276P)


在上一节中,我们已经知道了可以使用强制转换运算符来获取对象接口的引用, 另一个更好的方式是使用as运算符。我在这里会提一下,因为它是与接口一起使用的一个很好的选择。

如果我们尝试将类对象引用强制转换为类未实现的接口的引用, 强制转换操作会抛出一个异常。我们可以通过使用as运算符来避免这个问题。具体方法如下所示:

  • 如果类实现了接口,则表达式返回对接口的引用。
  • 如果类没有实现接口, 则表达式返回null,而不是抛出异常。
namespace HelloWorld_Console
{
    interface IIfc1
    {
        void PrintOut(string s);
    }
    class MyClass : IIfc1
    {
        public void PrintOut(string s)
        {
            WriteLine($"Calling through : {s}");
        }
    }
    class Program
    {

        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
            mc.PrintOut("huang");

            IIfc1 ifc = mc as IIfc1; //使用as 运算符 将类对象的引用转换为接口类型的引用
            ifc.PrintOut("interface"); //调用引用方法
            ReadKey();
        }
    }
}

实现具有重复成员的接口 (277P)


 由于类可以实现任意数量的接口, 有可能两个或多个接口成员都有相同的签名和返回类型。编译器如何处理这样的情况呢?

例如, 假设我们有两个接口IIfc1和IIfc2,  每一个接口都有一个名称为Printout的方法,具有相同的签名和返回类型。如果我们要创建实现两个接口的类,怎么样处理重复接口的方法呢?  下面看一个代码示例来理解:

namespace HelloWorld_Console
{
    interface IIfc1 // Declare interface.
    {
        void PrintOut(string s);
    }

    interface IIfc2 // Declare interface
    {
        void PrintOut(string s);
    }

    class MyClass : IIfc1, IIfc2 // Declare class.
    {
        public void PrintOut(string s) // 这里只实现了一次接口的重复成员
        {
            Console.WriteLine("Calling through:  {0}", s);
        }
    }
    class Program
    {
       
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
            mc.PrintOut("object");
            ReadKey();
        }
    }
}

通过代码可以看出当一个类实现多个接口的重复成员方法是:如果一个类实现了多个接口, 并且其中一些接口有相同签名和返回类型的成员, 那实现单个成员来重复成员的接口(说白了,就是不同的接口的重复成员都统一来实现)。

《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第4张图片 由同一个类成员实现的多个接口

将类对象转换为多个其实现接口的引用 (279P)


namespace HelloWorld_Console
{
    interface IIfc1 // Declare interface.
    {
        void PrintOut(string s);
    }
    interface IIfc2 // Declare interface
    {
        void PrintOut(string s);
    }
    class MyClass : IIfc1, IIfc2 // Declare class.
    {
        public void PrintOut(string s)
        {
            Console.WriteLine("Calling through: {0}", s);
        }
    }
    class Program
    {
        static void Main() 
        {
            MyClass mc = new MyClass();
            IIfc1 ifc1 = (IIfc1)mc; //获取 IIfc1 的引用
            IIfc2 ifc2 = (IIfc2)mc; 

            mc.PrintOut("object"); // 通过类对象调用
            ifc1.PrintOut("interface 1"); // 通过 IIfc1 调用
            ifc2.PrintOut("interface 2"); // 通过 IIfc2 调用 
        }
    }
}
《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第5张图片 对类中不同接口的单独引用

 


显式接口成员实现 (281P)


 如果说该类想为其继承的多个接口中的重复成员提供不同的实现,应该怎么办呢?在这种情况下, 我们可以创建显式接口成员实现,显式接口成员实现有如下特点:

  • 与所有接口实现相似,位于实现了接口的类或结构中。
  • 它使用限定接口名称来声明,由接口名称和成员名称以及它们中间的点分隔符号构成。

下面直接看代码:


namespace HelloWorld_Console
{
    interface IIfc1 { void PrintOut(string s); }    // Declare interface.

    interface IIfc2 { void PrintOut(string t); }  // Declare interface.

    class MyClass : IIfc1, IIfc2
    {
        void IIfc1.PrintOut(string s)  //显示实现接口成员
        {// 显式实现接口成员的类,该类在实现接口成员时的函数前面不需要添加任何修饰符,比如 public, 也不能private 作前辍
            WriteLine(s);
        }

        void IIfc2.PrintOut(string t)
        {
            WriteLine(t);
        }
    }
    class Program
    {

        static void Main(string[] args)
        {
            MyClass mc = new MyClass();     // Create class object.

            mc.PrintOut("interface 1"); //错误,显式接口成员实现只可以通过接口的引用成员来访问,其它的方式不可以直接访问
//((IIfc1)mc).PrintOut("interface 1"); 正确,这样 把对象的引用强制转换为其接口的引用,然后使用这个指向接口的引用来调用显式接口实现
            IIfc1 ifc1 = (IIfc1)mc;      // Get reference to IIfc1.
            ifc1.PrintOut("interface 1");    // Call explicit implementation.

            IIfc2 ifc2 = (IIfc2)mc;      // Get reference to IIfc2.
            ifc2.PrintOut("interface 2");  // Call explicit implementation.
            ReadKey();
        }
    }

}

《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第6张图片

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

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

访问显式接口成员实现 (283P)


 上述的显式接口成员实现只可以通过指向接口的引用来访问。也就是说其他的类成员或类实例都不可以直接访问它们。

下面直接看代码:

namespace HelloWorld_Console
{
    interface IIfc1 { void PrintOut(string s); }                    // Declare interface.

    class MyClass : IIfc1
    {
       void IIfc1.PrintOut(string s)  //显示实现接口成员
        {
            WriteLine(s);
        }
        public void Method() // 即使成员Method 也不可以直接访问显式接口成员
        {
            PrintOut("huang"); // Compile error
            this.PrintOut("huang"); // Compile error
            ((IIfc1)this).PrintOut("huang"); // Compile correctly,
           
        }
      
    }
    class Program
    {
       
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();     // Create class object.

            IIfc1 ifc1 = (IIfc1)mc;      // Get reference to IIfc1.
            ifc1.PrintOut("interface 1");    // Call explicit implementation.
            mc.Method();// 正确
            ifc1.Method(); //错误,接口引用只能调用接口中的成员,不可以调用类中的成员
            ReadKey();
        }
    }
}

输出结果为:
interface 1
huang
  • 只有 Method 函数 中的最后一行才会编译,因为它将对当前对象(this)的引用转换为对接口类型的引用,并使用该接口引用来调用显式接口实现。
  • 这一限制对继承有重要的影响。由于其他类成员不能直接访问显式接口成员实现,从该类派生的类成员显然也不能直接访问它们。必须始终通过对接口的引用来访问它们。

派生成员作为实现 (280P)


实现接口的类可以从它的基类继承实现的代码。例如,如下的代码演示了类从它的基类代码继承了实现。下面直接看代码示例:


namespace HelloWorld_Console
{
    interface IIfc1 // Declare interface.
    {
        void PrintOut(string s);
    }
    class MyBaseClass //声明基类
    {
        //注意, 在类中实现接口的成员函数,该成员函数必须是public的,不能是其它的访问修饰符,否则错误
        // 如果在基类中该函数的的访问修饰符不是public的,那么在该派生类中必须实现接口成员,否则错误
        public void PrintOut(string s)
        {
            WriteLine(s);
        }
       
    }

    class MyClass : MyBaseClass, IIfc1  //声明派生类,同时继承基类和接口
    {
        /* 如果想在派生类中重写该函数,如果该函数在基类中并不是virtual函数,那么在派生类中重写
         该函数时使用关键字new做前辍,该成员的修饰符可以是任意.
         如果该函数在基类中是virtual函数,那么在派生类中重写该函数时, 如果是new作前辍,该成员的修饰符可以是任意.
         如果是override 作前辍,那么该成员的修饰符只能是public的.
         记住,不管是override 还是 new 作前辍,它们的重写的函数的函数原型要一致*/

        /*   public void PrintOut(string s)
          {
              WriteLine(s);
          }*/
    }
    class Program
    {

        static void Main(string[] args)
        {
            MyClass my = new MyClass();
            my.PrintOut("object");

            MyBaseClass mbs = new MyBaseClass();
            mbs.PrintOut("object2");

            IIfc1 ifc = my as IIfc1; //将派生类对象的引用强制转换为接口的引用
            ifc.PrintOut("object3");

            IIfc1 bifc = mbs as IIfc1; //将基类对象的引用强制转换为接口的引用
            mbs.PrintOut("object4");
            ReadKey();
        }
    }
}

输出结果为:

object
object2
object3
object4

 


接口可以继承别的接口 (283P)


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

interface IPort2
{
  //Interface members
}
interface IPort3
{

   //Interface members
}
interface IPort1: IPort2,IPort3
{

  //Interface members
}


 与类不同, 它在基类列表中只能有一个类名 (C#中的派生类只能有一个直接的基类), 而接口可以在基接口列表中有任意多个接口。

  • 列表中的接口本身可以继承其他接口。
  • 结果接口包含它声明的所有接口和所有基接口的成员。
namespace HelloWorld_Console
{
   interface IDataRetrieve
    {
        int GetData();
    }
    interface IDataStore
    {
        void SetData(int x);
    }
    // 从前面两个接口继承,该接口中的所有成员是 自己的接口成员+ 继承接口的成员
    interface IPort1: IDataRetrieve, IDataStore
    {
        
    }
    class MyData: IPort1
    {
        int nPrivateData;
        //注意, 在类中实现接口的成员函数,该成员函数必须是public的,不能是其它的访问修饰符,否则错误
        public int GetData()
        {
            return nPrivateData;
        }
        public void SetData(int x)
        {
            nPrivateData = x;
        }
    }
    class Program
    {
       
        static void Main(string[] args)
        {
            MyData data = new MyData();
            data.SetData(5);
            WriteLine(data.GetData());
            ReadKey();
        }
    }
}
《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第7张图片 类实现的接口继承了多个接口

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


namespace HelloWorld_Console
{
    interface ILiveBirth // 声明接口
    {
        string BabyCaled();
    }
    class Animal // 基类
    {

    }
    class Cat : Animal, ILiveBirth // 派生类继承基类和接口
    {
        //  这里还必须要使用显式接口成员实现的方法, 因为其基类中没有实现接口中的成员
        string ILiveBirth.BabyCaled() 
        {
            return "huang";
        }

    }
    class Dog : Animal, ILiveBirth // 派生类继承基类和接口
    {
        string ILiveBirth.BabyCaled()
        {
            return "huang cheng tao";
        }
    }
    class Bird : Animal // 派生类继承基类
    {

    }
    class Program
    {

        static void Main(string[] args)
        {
            Animal[] animalArray = new Animal[3] { new Cat(), new Bird(), new Dog() };
            foreach (Animal a in animalArray)
            {
                ILiveBirth b = a as ILiveBirth;
                if (b != null)
                {
                    WriteLine(b.BabyCaled());
                }
            }
            ReadKey();
        }
    }
}
《随笔二十一》——C#中的 “ 接口 、 as 运算符”_第8张图片 Animal 基类的不同对象类型在数组中的结构

 


本章完......

你可能感兴趣的:(C#中的随笔)