C#图解教程之接口

作者 Zhendong Ho 日期 2019-07-29
C#
C#图解教程之接口

什么是接口

接口:一组函数成员而未实现的引用类型。只有类和结构能实现接口。

使用接口示例。

// 声明接口
interface IInfo
{
string GetName();
string GetAge();
}

// 声明实现了接口的CA类
class CA : IInfo
{
public string Name;
public int Age;

public string GetName() // 在CA类中实现接口方法
{
return Name;
}

public string GetAge() // 在CA类中实现接口方法
{
return Age.ToString();
}
}

// 声明实现了接口的CB类
class CB : IInfo
{
public string First;
public string Last;
public double PersonAge;

public string GetName() // 在CB类中实现接口方法
{
return First + " " + Last;
}

public string GetAge() // 在CB类中实现接口方法
{
return PersonAge.ToString();
}
}

class Program
{
static void PrintInfo(IInfo item) // 传入接口的引用
{
Console.WriteLine("Name: {0}, Age {1}", item.GetName(), item.GetAge());
}

static void Main()
{
CA a = new CA() { Name = "John Doe", Age = 35 };
CB b = new CB() { First = "Jane", Last = "Doe", PersonAge = 33 };

PrintInfo(a); // 对象的引用能自动转换为它们实现的接口的引用
PrintInfo(b);
}
}

输出结果:

15-1

IComparable接口

假设有如下一段代码,它使用了Array类的一个静态方法Sort对一个未排序的int类型数组进行排序,并输出排序后的结果。

using System;

class Program
{
static void Main()
{
var myInt = new[] { 20, 4, 16, 9, 2 }; // Create an array of ints.

Array.Sort( myInt ); // Sort elements by magnitude.

foreach ( var i in myInt ) // Print them out.
{
Console.Write( "{0} ", i );
}
}
}

Sort方法在int数组上工作得很好,但是如果我们尝试在自己的类上使用就会发生异常,例如下面的MyClass类。

class MyClass
{
public int TheValue;
}

Sort不能对MyClass对象数组进行排序,原因在于:它不知道如何比较自定义对象及如何进行排序。Array类的Sort方法其实依赖于一个叫做IComparable的接口,它声明在BCL中,包含唯一的CompareTo方法。CompareTo方法接收一个object类型的参数,可以匹配任何引用类型。

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

Sort使用的算法依赖于元素的CompareTo方法来决定两个元素的次序int类型实现了IComparable,但是MyClass没有。因此,想要用Sort对MyClass类型进行排序,就要让MyClass类实现IComParable接口

class MyClass : IComparable
{
public int TheValue;

public int CompareTo(object obj) // 实现方法
{
MyClass mc = (MyClass)obj;
if (this.TheValue < mc.TheValue)
{
return -1;
}
if (this.TheValue > mc.TheValue)
{
return 1;
}
return 0;
}
}

15-3

现在MyClass实现了IComparable接口,可以使用Sort来进行排序了。

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

static void Main()
{
var myInt = new[] { 20, 4, 16, 9, 2 };

MyClass[] mcArr = new MyClass[5]; // 创建MyClass对象的数组
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); // 输出排序后的数组
}
}

输出结果:

15-2

声明接口

接口声明不能包含以下成员:数据成员、静态成员

接口声明只能包含如下非静态成员函数的声明:方法、属性、事件、索引器

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

interface IMyInterface1  // 可以有访问修饰符
{
int DoStuff(int nVar1, long lVar2);
double DoOtherStudd(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()
{
MyClass mc = new MyClass(); // 创建实例
mc.PrintOut("object"); // 调用方法
}
}

输出结果:

15-3-2

接口是引用类型

接口是一种引用类型

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

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

15-4

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

实现具有重复成员的接口

如果一个类实现了多个接口,并且其中一些接口有相同签名和返回类型的成员,那么类可以实现单个成员来满足所有包含重复成员的接口。

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()
{
MyClass mc = new MyClass();
mc.PrintOut("object"); // 输出:Calling through: object
}
}

15-6

多个接口的引用

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

interface IIfc1
{
void PrintOut(string s);
}

interface IIfc2
{
void PrintOut(string s);
}

class MyClass : IIfc1, IIfc2
{
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; // 获取IIfc的引用

mc.PrintOut("object"); // 从类对象调用
ifc1.PrintOut("interface1"); // 从IIfc1调用
ifc2.PrintOut("interface2"); // 从IIfc2调用
}
}

输出结果:

15-8

派生成员作为实现

实现接口的类可以从它的基类继承实现的代码

interface IIfc1
{
void PrintOut(string s);
}

class MyBaseClass
{
public void PrintOut(string s)
{
Console.WriteLine("Calling through: {0}", s);
}
}

class Derived : MyBaseClass, IIfc1
{
// 空主体,但是基类MyBaseClass中的代码能满足实现接口方法的需求
}

class Program
{
static void Main()
{
Derived d = new Derived();
d.PrintOut("object"); // 输出:Calling through: object
}
}

15-9

显式接口成员实现

可以使用接口名称.成员名称的方式显式实现接口。

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 t) // 显式接口成员实现
{
Console.WriteLine("IIfc1: {0}", t);
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass(); // 创建类对象

IIfc1 ifc1 = (IIfc1)mc; // 获取IIfc1的引用
ifc1.PrintOut("interface 1"); // 调用显式实现

IIfc2 ifc2 = (IIfc2)mc; // 获取IIfc2的引用
iifc2.PrintOut("interface 2"); //调用显式实现

/*
输出结果:
IIfc1: interface 1
IIfc2: interface 2
*/
}
}

15-10

注意:

  • 表示显式接口成员实现的方框不是灰色的,因为它们表示实际的代码。
  • 在图中接口方法没有指向类级别实现,而是包含了自己的代码
  • 显式接口成员实现,接口名前面不能加public,因为是接口中的成员,默认是public
  • 如果有显式接口成员实现,类级别的实现是允许的,但不是必须的。

三种实现场景

  1. 类级别实现
  2. 显式接口成员实现
  3. 类级别和显式接口成员实现

访问显式接口成员实现

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

class MyClass : IIfc1
{
void IIfc1.PrintOut(string s) // 显式接口实现
{
Console.WriteLine("IIfc1");
}

public void Method1()
{
PrintOut("..."); // 编译错误
this.PrintOut("..."); // 编译错误

((IIfc1)this).PrintOut("..."); // 转为接口的引用后,调用方法
}
}

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

接口可以继承接口

接口可以从一个或多个接口继承。

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()
{
MyData data = new MyData();
data.SetData(5);
Console.WriteLine("{0}", data.GetData()); // 5
}
}

15-11

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

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]; // 创建Animal数组
animalArray[0] = new Cat(); // 插入Cat类对象
animalArray[1] = new Bird(); // 插入Bird对象
animalArray[2] = new Dog(); // 插入Dog对象
foreach (Animal a in animalArray) // 在数组中循环
{
ILiveBirth b = a as ILiveBirth; // 如果实现了ILiveBirth接口
if (b != null)
{
Console.WriteLine("Baby is called: {0}", b.BabyCalled());
}
}
/*
输出:
Baby is called: kitten
Baby is called: puppy
*/
}
}

15-12