Learning Hard C# 学习笔记: 6.C#中的接口

发布时间 2023-10-05 22:05:25作者: ErgoCogito

目的: 由于C#中的类只能单个继承, 为了满足多重继承(一个子类可以继承多个父类)的需求, 所以产生了接口.

多重继承是指一个类可以从多个父类继承属性和方法。在C#中,只允许单继承,即一个类只能有一个直接父类。这是因为多继承在某些情况下可能导致代码的复杂性和不确定性增加,容易引发命名冲突和歧义。

然而,在某些情况下,可以通过接口、抽象类和组合等技术来模拟多重继承的效果。

下面是一些情况下可能需要多继承的示例:

  • 多个父类具有不同的功能,但又希望在一个类中统一管理。例如,一个类同时需要继承一个窗体类和一个数据访问类,以便同时具有界面交互和数据库操作的功能。

  • 需要从多个父类中继承不同的接口或协议。例如,一个类需要同时实现Comparable接口、Serializable接口和Cloneable接口等。

  • 需要从一个具体类和一个抽象类中继承。抽象类提供了一些通用的实现,而具体类提供了一些特定的实现。这种方式可以实现更灵活的代码复用。

需要注意的是,如果多个父类之间存在命名冲突或歧义,这种情况下多继承可能会导致问题。因此,在使用多继承时,需要仔细考虑继承关系的设计和命名的规范,以避免潜在的问题。

6.1 什么是接口

接口可以理解为对一组方法声明进行的统一命名,但这些方法没有提供任何实现。也就是说,把一组方法声明在一个接口中,然后继承于该接口的类都需要实现这些方法。

例如,很多类型(比如int 类型、string 类型和字符类型等)都需要比较大小,此时可以定义一个比较接口,在该接口中定义比较方法,然后让这些类型去继承该接口,并实现自己的比较方法。

通过接口,你可以对方法进行统一管理,避免了在每种类型中重复定义这些方法。


6.2 如何使用接口来编程

6.2.1 接口的定义

接口的定义与类相似, 只是关键字不同, 类使用class, 接口使用interface.

实际开发中可以直接在项目中添加新建项时添加接口.

添加成功后, 打开接口代码文件, 会看到以下代码:

interface ICustomCompare
{
}

这是一个简单的接口定义, 没有定义任何方法.

下面向该接口中添加一个比较方法,以让所有继承该接口的类都可以实现这个方法。

interface ICustomCompare
{
    // 定义比较方法,继承该接口的类都要实现该方法
    int CompareTo(object other);
}

在接口中定义方法不能添加任何访问修饰符,因为接口中的方法默认为public ,如果显式地指定了修饰符,则会出现编译时错误。

在接口中除了可以定义方法外,还可以包含属性、事件、索引器,或者这4类成员(包括方法)类型的任意组合;但接口类型不能包含字段、运算符重载、实例构造函数和析构函数。

接口中的所有成员都默认是公共的,因此不能再使用public 、private 和protected 等访问修饰符进行修饰,也不能使用static 修饰符。如果在接口中定义了不该包含的成员,就会导致编译错误: 接口不能包含字段.

对于其他不能包含的成员,如果试图将它们添加进接口,同样也会出现编译错误。

6.2.2 继承接口

定义完接口之后,如果有类想继承该接口,则它必须实现接口中定义的所有方法。类继承接口与继承类是一样的,只需要使用英文的冒号“:”,后接接口名就可以了。

// 定义类,并继承接口
public class Person : ICustomCompare
{
    int age;

    public int Age
    {
        get { return age; }
        set { age = value; }
    }

    // 实现接口方法
    public int CompareTo(object value)
    {
        if (value == null)
        {
            return 1;
        }

        // 将object类型强制转换为Person类型
        Person otherp = (Person)value;
        // 把当前对象的Age属性与需要比较的对象的Age属性进行对比
        if (this.Age < otherp.Age)
        {
            return -1;
        }
        if (this.Age > otherp.Age)
        {
            return 1;
        }

        return 0;
    }
}

在以上代码中,定义了一个Person 类继承于ICustomCompare 接口,并且该类实现了接口中定义的CompareTo 方法。

CompareTo 方法会首先判断参数对象是否为null ,如果比较的对象为空,就直接返回1,代表当前对象比传入对象大。如果比较的对象不为null ,则首先把它强制转换为Person 类型(传进来的参数是object 类型,不同类型的对象间不存在可比性),再比较它们的Age 属性。如果当前对象(即Person 对象)的Age 属性值比传入对象的Age 属性值大,则返回1,说明当前对象比传入对象大;如果返回-1,则表示比传入的对象小;如果相等,则返回0。

6.2.3 调用接口中的方法

调用方法如下:

// 接口中方法的调用
class Program
{
    static void Main(string[] args)
    {
        // 创建两个Person对象
        Person p1 = new Person();
        p1.Age = 18;
        Person p2 = new Person();
        p2.Age = 19;

        // 调用接口中方法,对p1和p2进行比较
        if (p1.CompareTo(p2) > 0)
        {
            Console.WriteLine("p1比p2大");
        }
        else if (p1.CompareTo(p2) < 0)
        {
            Console.WriteLine("p1比p2小");
        }
        else
        {
            Console.WriteLine("p1和p2一样大");
        }
        Console.Read();
    }
}        

在以上代码中,首先创建了两个待比较的对象p1 和p2 ,然后调用CompareTo 方法进行比较。因为p1 的Age 属性是18,而p2 的Age 属性时19,所以p1 肯定是比p2 小。运行结果也是如此.


6.3 显式接口实现方式

上面代码中, 使用了隐式的接口实现方式,即在实现代码中没有指定实现哪个接口中的CompareTo 方法。相应地,自然就有显式的接口实现方式,它指的是在实现过程中,明确指出实现哪一个接口中的哪一个方法。

当多个接口中包含相同方法名称、相同返回类型和相同参数时,如果一个类同时实现了这些接口,隐式的接口实现就会出现命名冲突的问题.

如下所示:

// 中国人打招呼接口
interface IChineseGreeting
{
    // 接口方法声明
    void SayHello();
}

// 美国人打招呼接口
interface IAmericanGreeting
{
    // 接口方法声明
    void SayHello();
}

// Speaker类实现了两个接口
public class Speaker : IChineseGreeting, IAmericanGreeting
{
    // 隐式接口实现
    public void SayHello()
    {
        Console.WriteLine("你好");
    }
}

以上代码中定义了一个Speaker 类,它实现了两个接口,并且这两个接口中声明的方法具有相同的返回类型、相同的方法名称和相同的参数(这里都没参数)。

若采用隐式的接口实现方式,下面的代码将调用相同的SayHello 方法,而不管具体获取了哪个接口:

// 隐式接口实现存在的问题
static void Main(string[] args)
{
    // 初始化类实例
    Speaker speaker = new Speaker();

    // 调用中国人打招呼方法
    IChineseGreeting iChineseG = (IChineseGreeting)speaker;
    iChineseG.SayHello();

    // 调用美国人招呼方法
    IAmericanGreeting iAmericanG = (IAmericanGreeting)speaker;
    iAmericanG.SayHello();

    Console.Read();
}

运行结果:

你好
你好

从运行结果可以发现,不管获得了哪个接口,程序都是调用的同一个方法实现,即都输出“你好”。但我们期望的并不是这样,我们希望调用IAmericanGreeting 接口的SayHello 方法可以输出“Hello”,而调用IChineseGreeting 接口的SayHello 方法时才输出“你好”。

显式的接口实现方式可以解决这样的命名冲突问题。

如下所示:

// 中国人打招呼接口
interface IChineseGreeting
{
    // 接口方法声明
    void SayHello();
}

// 美国人打招呼接口
interface IAmericanGreeting
{
    // 接口方法声明
    void SayHello();
}

// Speaker类实现了两个接口
public class Speaker : IChineseGreeting, IAmericanGreeting
{
    // 显式的接口实现
    void IChineseGreeting.SayHello()
    {
        Console.WriteLine("你好");
    }

    void IAmericanGreeting.SayHello()
    {
        Console.WriteLine("Hello");
    }
}
class Program
{
    // 显式接口实现演示
    static void Main(string[] args)
    {
        // 初始化类实例
        Speaker speaker = new Speaker();

        // 调用中国人打招呼方法
        // 显式地转化为通过IChineseGreeting接口来调用SayHello方法
        IChineseGreeting iChineseG = (IChineseGreeting)speaker;
        iChineseG.SayHello();

        // 调用美国人打招呼方法
        // 显式地转化为通过IAmericanGreeting接口来调用SayHello方法
        IAmericanGreeting iAmericanG = (IAmericanGreeting)speaker;
        iAmericanG.SayHello();
        Console.Read();
    }
}    

显式的接口实现解决了命名冲突问题。

在这种实现方式下,代码的运行结果如下所示。

你好
Hello

在使用显式的接口实现方式时,需要注意以下几个问题。

  • 若显式实现接口,方法不能使用任何访问修饰符,显式实现的成员都默认为私有,如下面的代码就是不合法的:

    // 错误!不能有访问修饰符修饰,因为成员默认为私有
    public void IChineseGreeting.SayHello()
    {
       Console.WriteLine("你好");
    }
    
  • 显式实现的成员默认是私有的,所以这些成员都不能通过类的对象进行访问。即使强行在speaker后加上点,智能提示也不会显式SayHello 方法,因此无法完成访问.

此时,正确的访问方式是把speaker 对象显式地转换为对应的接口,通过接口来调用SayHello 方法。

代码如下:

static void Main(string[] args)
{
    // 初始化类实例
    Speaker speaker = new Speaker();

    // 调用中国人打招呼方法
    // 显式地转化为通过IChineseGreeting接口来调用SayHello方法
    IChineseGreeting iChineseG = (IChineseGreeting)speaker;
    iChineseG.SayHello();

    // 调用美国人打招呼方法
    // 显式地转化为通过IAmericanGreeting接口来调用SayHello方法
    IAmericanGreeting iAmericanG = (IAmericanGreeting)speaker;
    iAmericanG.SayHello();
    Console.Read();
}    

前面具体分析了隐式与显式接口实现方式两种情况,下面对这两种实现的区别和使用场景进行总结,帮助大家明确在什么情况下该使用哪种实现方式。

  • 采用隐式接口实现时,类和接口都可以访问接口中的方法;而若采用显式接口实现方式,接口方法只能通过接口来完成访问,因为此时接口方法默认为私有。
  • 当类实现单个接口时,通常使用隐式接口实现方式,这样类的对象可以直接去访问接口方法。
  • 当类实现了多个接口,并且接口中包含相同的方法名称、参数和返回类型时,则应使用显式接口实现方式。即使没有相同的方法签名,在实现多个接口时,仍推荐使用显式的方式,因为这样可以标识出哪个方法属于哪个接口。

6.4 接口与抽象类

抽象类经常与接口一起使用,共同服务于面向对象的编程,这里简单地分析一下接口与抽象类的区别,主要有以下几点。

  • 关键字不同, 抽象类使用abstract关键字, 接口使用interface; 它们都不能实例化.
  • 抽象类中可以包含虚方法、非抽象方法和静态成员;但接口中不能包含虚方法和任何静态成员,并且接口中只能定义方法,不能有具体实现,方法的具体实现由实现类完成。
  • 抽象类不能实现多继承,接口则支持多继承。注意,从严格意义上说,类接触接口应该成为类实现接口。
  • 抽象类是对一类对象的抽象,继承于抽象类的类与抽象类为属于的关系;而类实现接口只是代表实现类具有接口声明的方法,是一种CAN-DO的关系。所以一般接口后都带有able 字段,表示“我能做”的意思,例如微软类库中的IComparable 接口和ICloneable 接口等。

6.5 面向对象编程的应用

前面的内容只是单独介绍了类、面向对象思想和接口,并没有具体分析如何在平时工作中应用它们来实现面向对象编程。下面就简单介绍下面向对象编程的应用.

如果你想设计一个Dog 类,有了类的概念后,你可能会像下面这样去实现它:

public class Dog
{
    public void EatFood()
    {
        // eat some food
    }
    public void Walk()
    {
        // walk
    }
}      

但是Dog 类中的EatFood 和Walk 方法有可能被其他类用到,它们都是动物的共同特性。这时你应该考虑使用面向对象中的继承思想,重新设计代码实现。

使用继承后的代码如下所示。其中添加了Animal 类来封装公用的EatFood 方法和Walk 方法,这使得其他继承于Animal 的子类都可以使用这两个方法,因此更好地重用了代码。

public abstract class Animal
{
    public void EatFood()
    {
        // eat some food
    }
    public void Walk()
    {
        // walk
    }
}

public class Dog : Animal
{

}

以上代码使用继承达到了重用代码的效果,但是不能过度使用继承,否则将导致派生类的膨胀,从而增加维护和管理的成本。如果必须过度使用,可以考虑通过接口来实现。

有些经过训练的狗还具有表演节目的能力,此时你又会怎么设计呢?下面列出了可能的解决方案,并分别进行了分析。

  1. 解决方案一

    此时很容易想到在Animal 类的定义中添加一个Show 方法,具体实现方式如下所示:

    public abstract class Animal
    {
        public void EatFood()
        {
            // eat some food
        }
        public void Walk()
        {
            // walk
        }
        public void Show()
        {
            // show
        }
    }
    
    public class Dog : Animal
    {
    
    }
    

    然而这种设计会把Animal 概念本身固有的行为方法和另外一个特殊概念的“表演节目”的行为方法混在一起。并不是所有的Dog都具有表演节目的行为方法,而上面的设计会导致所有继承于Animal 的动物都获得了表演节目的行为,这显然不符合现实情况。方案一不可取。

  2. 解决方案二

    既然EatFood 、Walk 和Show 方法属于两个不同的概念,所以应该把它们分别定义在代表这个两个概念的类中。

    定义方式有3种:

    • 定义两个抽象类;
    • 一个概念使用抽象类,另一个使用interface 方式定义;
    • 两个概念都使用interface 来定义。

    显然,因为C#语言不支持多重继承,两个概念都使用抽象类来实现是不可行的。而若两个概念都使用interface 定义的话,从程序角度来说并没有什么问题,但从现实角度考虑,则显然不合理,因为Dog 本身属于Animal ,这并非接口所表达的CAN-DO的关系。

    所以正确的设计应该是一个概念使用抽象类,另一个使用interface 。

    具体的设计代码如下:

    public abstract class Animal
    {
        public void EatFood()
        {
            // eat some food
        }
        public void Walk()
        {
            // walk
        }
    }
    
    public interface IAnimalShow
    {
        void Show();
    }
    
    public class Dog : Animal
    {
    
    }
    
    public class SpecialDog : Animal, IAnimalShow
    {
        public void Show()
        {
        }
    }
    

以上代码才是正确的设计方式,它体现了接口和抽象类的区别。


6.6 归纳总结

本章主要介绍了接口的定义、实现以及对其方法的调用;分析了隐式接口实现与显式接口实现间的区别,总结了两种实现使用的一般场景;最后分析了抽象类与接口之间的差异,给出了它们在面向对象编程中的应用。