13 创建接口和定义抽象类

从类继承是很强大的机制,但继承真正强大之处是能从接口继承。接口不包含任何代码或数据;它只规定了从接口继承的类必须提供哪些方法和属性。使用接口,方法的名称/签名可以和方法的具体实现完全隔绝。

抽象类在许多方面都和接口相似,只是它们可以包含代码和数据。然而,可以将抽象类的某些方法指定为虚方法,指示从抽象类继承的类必须以自己的方式实现这些方法。

 

1.理解接口

接口就相当于一份协议(contract)。类实现了接口后(签订了协议后),接口(协议)就能保证类包含了接口所指定的全部方法。

使用接口,可以真正地将“what" (有什么)和"how" (怎么做)区分开。接口指定“有什么”,也就是方法的名称、返回类型和参数。至于具体“怎么做”,或者说方法具体如何实现,则不是接口所关心的。接口描述了类提供的功能,但不描述功能如何实现。

 

1.1定义接口

定义接口和定义类相似,只是使用interface而不是class关键字。在接口中按照与类和结构一样的方式声明方法,只是不允许指定任何访问修饰符(public, private protected都不可以用)。另外,接口中的方法是没有实现的,它们只是声明。实现接口的所有类型都必须提供自己的实现。所以,方法主体被替换成一个分号。下面是-个例子:

interface IComparable
{

    int CompareTo(object obj);

}

注意:接口不含任何数据;不可以向接口添加字段(私有的也不行);

 

1.2实现接口

为了实现接口,需要声明类或结构从接口继承,并实现接口指定的全部方法。这不是真正的“继承"一虽然语法一 样, 而且如同本章稍后会讲到的那样,语义有继承的大量印记。注意,虽然不能从结构派生,但结构是可以实现接口的(从接口“继承”)。

 

现在我们以陆栖哺乳动物为例,要求所有陆栖哺乳动物都提供名为NumberofLegs(腿数)的方法,它返回一个int值,指出一种哺乳动物有几条腿。

定义接口:

interface ILandBound
{
    int NumberOfLegs();
}

然后可以在Horse(马)类中实现该接口,具体就是从接口继承,并为接口定义的所有方法提供实现(本例只有一个NumberofLegs方法):

class Horse : ILandBound
{

    // …
    public int NumberLegs()
    {

        return 4;   //马有四条腿

    }
}

实现接口时,必须保证每个方法都完全匹配对应的接口方法,具体遵循以下几个规则。

(1)方法名和返回类型完全匹配。

(2)所有参数(包括 ref和out关键字修饰符)都完全匹配。

(3)用于实现接口的所有 方法都必须具有public 可访问性。但如果使用显式接口实现(即实现时附加接口名前缀,稍后会解释),则不应该为方法添加访问修饰符。

接口的定义和实现存在任何差异,类都无法编译。

下例定义Horse从Mammal继承,同时实现ILandBound接口:

class Horse : Mammal,ILandBound
{
    …
}

注意:一个接口(InterfaceA)可以从另一个接口(InterfaceB)继承,这在技术上称为接口扩展而不是继承。

 

1.3通过接口来引用类

和基类变量能引用派生类对象一样,接口变量也能引用实现了该接口的类的对象。例如,ILandBound变量能引用Horse对象,如下所示:

Horse myHorse = new Horse(...);

ILandBound MyHorse = myHorse; //合法

通过接口来引用对象是一项相当有用的技术。因为能由此定义方法来获取不同类型的实参一只要类型实现了指定的接口。例如,以下FindL andSpeed方法可获取任何实现了ILandBound接口的实参:

int FindtL andSpeed(ILandBound landBoundMamal)
{

    //…

}

可用is操作符验证对象是实现了指定接口的一个类的实例。

例如,以下代码检查myHorse变量是否实现了ILandBound 接口,如果是就把它赋给一个

ILandBound变量。

if (myHorse is ILandBound)
(

    ILandBound iLandBoundAnimal  = myHorse;

}

1.4使用多个接口

一个类最多只能有 一个基类,但可以实现数量不限的接口。类必须实现这些接口声明

的所有方法。

结构或类要实现多个接口,接口要以逗号分隔。如果还要从一个基类继承,那么接口要在基类之后列出。例如,假定已定义了一个IGrazable(草食)接口,它包含ChewGrass(咀嚼草)方法,规定所有草食类动物都要实现自己的ChewGrass方法。在这种情况下,可以像下面这样定义Horse类,它表明Mammal是基类,而ILandBound和IGrazable是Horse要实现的两个接口。

class Horse : Marmnal, ILandBound, IGrazable
{

    …

}

1.5显示实现接口

前面的例子都是隐式实现接口,比如之前讲的ILandBound接口和Horse类的代码,Horse类的NumberOfLegs方法的实现中,没有指明是ILandBound接口的一部分。

interface ILandBound
{
    int NumberOfLegs();
}


class Horse : ILandBound
{

    …
    public int NumberLegs()
    {
        return 4;   //马有四条腿
    }

}

当Horse类同时实现多个接口时,问题就出现了。例如,假定要实现马车运输系统。一次长途旅行可以被分成几个阶段,或者称为几“站" (legs)"。要跟踪每匹马拉马车跑了几“站”,可以像下面这样定义接口:

interface Ijourney
{

    int NumberOfLegs();//跑的站(leg)数

}

现在Horse同时实现这两个接口

class Horse : ILandBound,IJourney
{
    …
    public int NumberLegs()
    {
        return 4;  
    }
}

代码合法,但到底是马有4条腿,还是它拉车拉了4站呢?

解决办法:井区分哪个方法实现的是哪个接口,应该显式实现接口。

class Horse : ILandBound, IJourney
{

    //...
    int ILandBound.NumberfLegs()
    {
        return 4;
    }

    int Ijourney.Numberflegs()
    {
        return 3;
    }

}

现在可以清楚地定义马有4条腿,马拉车拉了3站。

 

除了为方法名附加接口名前缀,上述语法还有另一个容 易被人忽视的改变:方法没有用public标记。如果方法是显式接口实现的一部分, 就不能为方法指定访问修饰符。这造成另一个有趣的问题。在代码中创建一个Horse变量,两个NumberOfLegs方法都不能通过该变量来调用,因为它们都不可见。两个方法对于Horse类来说是私有的。这个设计是合理的。如果方法能通过Horse类访问,那么以下代码会调用哪一个ILandBound 接口的?还是IJourney接口的?

Horse horse = new Horse();
…

int legs = horse.NumberOfLegs();   //该语句无法编译

那么,怎么访问这些方法呢?答案是通过恰当的接口来引用Horse对象,如下所示:

Horse horse = new Horse();

IJourney journeyHorse = horse;

int legsInJourney = journeytorse .NumberofLegs();

ILandBound landBoundHorse = horse;

int legsOnHorse = landBoundHorse .NumberofLegs();

建议尽量显式实现接口。

 

1.6接口的限制

(1)不能在接口中定 义任何字段,包括静态字段。字段本质上是类或结构的实现细节。

(2)不能在接口中定义任何构造器。构造器也是类或结构的实现细节。

(3)不能在接口中定 义任何析构器。析构器包含用于析构(销毁)对象实例的语句,详情参见下一篇博客。

(4)不能为任何方法指定 访问修饰符。接口所有方法都隐式为公共方法。

(5)不能在接口中 嵌套任何类型(例如枚举、结构、类或其他接口)。

(6)虽然一个接口能从另一个接口继承, 但不允许从结构或类继承。结构和类含有实

现:如果允许接口从它们继承,就会继承实现。

 

2.抽象类

我们知道,Horse和Sheep都是草食(IGrazable)陆栖(ILandBound)哺乳动物(Mammal),像下面这种情况(两个类明显有重复):

// Horse和Sheep都是草食动物

class Horse : Mammal, ILandBound, IGrazable // 马
{
    …
    vold IGrazable .ChewGrass()
    {
        Console.WriteLine("Chevdng grass");
        //用于描述明嚼草的过程的代码
    }

}



class Sheep : Mammal, ILandBound, IGrazable // 羊
{
    vold IGrazable.ChewGrass()
    {
        Console.WriteLine("Chewdng grass");
        //和马咀唱草样的代码
    }
}

重复的代码是警告信号,表明应重构代码以避免重复,并减少维护开销。一个办法是将通用的实现放到专门为此目的而创建的新类中。换言之,要在类的层次结构中插入一个新类。例如:

class GrazingMammal : Marmal, IGrazable // Grazingammal是指草食性贴乳动物
{
    ...
    void IGrazable. ChewGrass()
    {
        //用于表示明嚼草的通用代码
        Console.WriteLine("Chewing grass");
    }

class Horse : GrazingManmnal, ILandBound
{

    ...
}

class Sheep : GrazingMamnal, ILandBound
{

    …
}

}

这是一个不错的方案,但仍然有一件事情不太对:可以实际地创建GrazingMammal类(以及Mammal)的实例,这是不合逻辑的。GrazingMammal(草食性哺乳动物)类存在的目的是提供通用的默认实现。它唯- 的作用就是让一个具体的草食性哺乳动物(例如马、羊)类从它继承。GrazingMamnal类是通用功能的抽象,不是能实际存在的实体。

 

为了明确声明不允许创建某个类的实例,必须将那个类显式声明为抽象类,这是用

abstract关键字实现的。如下所示:

abstract class GrazingMammal : Mammal, IGrazable

{

…

}

试图实例化-一个GrazingMammal对象,代码将无法通过编译。示例如下:

GrazingMammal myGrazingMamnal = new GrazingMammal(…); //非法

 

2.1抽象方法

抽象类可以包含抽象方法。抽象方法原则上与虚方法相似,只是它不含方法主体。派生类必须重写(override)这种方法。

下例将GrazingMammal类中的DigestGrass(消化草)方法定义成抽象方法:草食动物可以使用相同的代码来表示咀嚼草的过程,但它们必须提供自己的DigestGrass方法的实现(即使咀嚼草的过程相同,但消化草的方式不同)。

如果一个方法在抽象类中提供默认实现没有意义,但又需要派生类提供该方法的实现,就适合定义成抽象方法。

abstract class GrazingMamnal : Mamnal, IGrazable
{

    public abstract void DigestGrass();

    …

}

注意:抽象方法不可以私有

 

3.密封类

如果不想一个类作为基类使用,可以使用C#提供的sealed(密封)关键字防止类被用作基类。

例如:

sealed class Horse : GrazingMamal, ILandBound
{

    …

}

任何类试图将Horse用作基类都会发生编译时错误。密封类中不能声明任何虚方法,而且抽象类不能密封。

 

3.1密封方法

可用sealed关键字声明非密封类中的一个单独的方法是密封的,这意味着派生类不能重写该方法。只有用override 关键字声明的方法才能密封,而且方法要声明为sealed override.可像下面这样理解interface, virtual, override 和sealed等关键字:

(1)interface(接口)引入方法的名称。

(2)virtual(虚)方法是方法的第一一个实现。

(3)override(重写)方法是方法的另 一个实现。

(4)sealed(密封)是方法的最后- - .个实现。

 

参考书籍:《Visual C#从入门到精通》

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值