.NET 2.0面向对象编程揭秘 继承

4.2  抽象类与接口

我们来看汉语中的一句话:

苹果是一种水果。

在计算机中,将上述关系用“继承”模拟(见图4-1)。

图4-7  用“继承”模拟“苹果是一种水果”关系

根据常识,我们知道世界上存在着真实的苹果实体,却并不存在着一种水果实体。对应地,在程序中只能创建Apple对象,不能创建Fruit对象。

不能创建对象的类称为“抽象类(abstract class)”。

4.2.1  抽象类

1.抽象类与抽象方法

在一个类前面加上“abstract”关键字,此类就成为了抽象类。

对应地,一个方法类前面加上“abstract”关键字,此方法就成为了抽象方法。

abstract class Fruit    //抽象类

{

    public abstract void GrowInArea(); //抽象方法

}

 

提示:Visual Basic.NET使用MustInherit关键字定义抽象类,使用MustOverride关键字定义抽象方法。

注意抽象方法不能有实现代码,在函数名后直接跟一个分号。

抽象类专用于派生出子类,子类必须实现抽象类所声明的抽象方法,否则,子类仍是抽象类。

抽象类一般用于表达一种比较抽象的事物,比如前面所说的“水果”,而抽象方法则说明此抽象类应该具有的某种功能,比如Fruit类中有一个抽象方法GrowInArea(),说明水果一定有一个最适合其生长的地区,但不同的水果这一生长地是不同的。

从同一抽象类中继承的子类拥有相同的方法(即抽象类所定义的抽象方法),但这些方法的具体实现代码每个类都可以不一样,如以下两个类分别代表苹果(Apple)和菠萝(Pineapple)。

class Apple : Fruit //苹果

{

    public override void GrowInArea()

    {

        Console.WriteLine("南方北方都可以种植我。");

    }

}

class Pineapple : Fruit //菠萝

{

    public override void GrowInArea()

    {

        Console.WriteLine("我喜欢温暖,只能在南方看到我。");

    }

}

注意上述代码中的override关键字,这说明子类重写了基类的抽象方法。抽象类不能创建对象,一般用它来引用子类对象。

Fruit f;

f = new Apple();

f.GrowInArea();

f = new Pineapple();

f.GrowInArea();

运行结果:

南方北方都可以种植我。

我喜欢温暖,只能在南方看到我。

 

注意:同一句代码“f.GrowInArea();”会由于f所引用的对象不同而输出不同的结果。可以看到,代码运行结果类似于4.1.3节介绍的“虚方法调用”特性,两者没有本质差别。

可以按照以下格式编写代码:

抽象类 抽象类变量名=new 继承自此抽象类的具体子类名();

一个抽象类中可以包含非抽象的方法和字段。因此:

包含抽象方法的类一定是抽象类,但抽象类中的方法不一定是抽象方法。

在C#中,抽象方法只能出现在抽象类内(即在普通类中不能定义一个抽象方法),而一个抽象类可以没有一个抽象方法(即它的所有方法都不是抽象的)。

这些像绕口令式的结论不用死记,因为在编译程序时,C#编译器会自动帮我们检测代码,并给出详细的提示信息。

 

提示:实现了抽象基类中所有抽象方法的类称为“Concrete Class”,汉语不太好翻译,直译为“具体类”,但笔者认为意译为“可实例化类”更贴切,因为这种类的最大特点之一就是可以使用它来创建对象。

2.抽象属性

除了方法可以是抽象的之外,属性也可以是抽象的,请看以下代码。

abstract class Parent

{

    public abstract String Message   //抽象属性

    {

        get;

        set;

    }

}

class Child : Parent

{

    private String _msg;

    public override  String Message

    {

        get

        {

            return _msg;

        }

        set

        {

            _msg=value;

        }

    }

}

使用代码如下:

Parent p = new Child();

p.Message = "Hello";

3.密封类

抽象类一般作为基类而存在,没有任何子类的抽象类是无意义的。但在C#中,却有一种特殊的类不能拥有任何子类,这就是是密封类。

在一个类前面加上“sealed”关键字,就成了密封类。

sealed class CannotInherit

{

}

除了不能作为基类,密封类的使用与普通类没有什么差别。

4.2.2  接口

我们来看下面这句话:

鸭子是一种鸟,会游泳,同时又是一种食物。

如何在面向对象的程序中表达这种关系?

如果使用C++,可以设计成让鸭子(Duck)类继承自两个父类(鸟Bird和食物Food)。但在C# 中,所有的类都只能有一个父类,此方法不可行。

为了解决这一问题,C#引入了接口(Interface)这一概念,并规定“一个类可以实现多个接口”。

1.接口的定义与使用

关键字interface用于定义接口(示例项目InterfaceExamples)。

//定义两个接口   

public interface ISwim

{

    void Swim();

}

public interface IFood

{

    void Cook();

}

接口可以看成是一种“纯”的抽象类,它的所有方法都是抽象方法。

 

注意:接口的成员默认就是public的,不要画蛇添足地在方法前加上“public”关键字。

可以用与继承相同的语法定义一个类实现某些接口。

//定义一个抽象类

public abstract class Bird

{

    public abstract void Fly();

}

//继承自一个抽象类,实现两个接口

public class Duck : Bird, IFood, ISwim

{

    //实现ISwim接口

    public  void Swim()

    {

        Console.WriteLine("是鸭子就会游泳");

    }

    //实现IFood接口

    public void Cook()

    {

        Console.WriteLine("鸭子经常被烧烤,北京烤鸭就很有名");

    }

    //实现抽象类Bird中的抽象方法

    public override void Fly()

    {

        Console.WriteLine("只有野鸭才会飞");

    }

}

上述代码可以图示为图4-8。

图4-8  实现接口

可以看到,抽象类定义了对象所属的类别,而接口实际上定义了一种对象应具有的行为特性。

 

注意:若一个类同时实现多个接口和派生自一个基类,定义时,基类要放在第一位。

可按以下格式使用接口:

接口类型名 变量名=new 实现了接口的类型名();

示例代码如下:

static void Main(string[] args)

{

    Duck d = new Duck();

    //Duck对象d可以使用3种类型的方法:

    //1.自身定义的

    //2.父类定义的

    //3.接口定义的

    d.Fly();

    d.Cook();

    d.Swim();

    //将子类(Duck)对象赋给基类变量

    Bird b = d;

    //现在只能使用基类定义的Fly()方法

    b.Fly();

    //将Duck对象赋给ISwin接口变量

    ISwim s = d;

    //现在只能使用接口定义的Swim()方法

    s.Swim();

    //将Duck对象赋给另一个实现的接口IFood变量

    IFood f = d;

    //现在只能使用接口定义的Cook()方法

    f.Cook();

}

请读者仔细地阅读上述代码的注释,由于Duck类继承自抽象基类Bird,又实现了ISwim和IFood两个接口,所以,Duck对象拥有这三者所定义的所有方法,并且可以赋值给这三种类型的变量。

需要注意的是,虽然程序中始终都只有一个Duck对象,但将其赋值给不同类型的变量后,其可以使用的方法是不一样的。

2.显式实现接口

上面讲到,某个类可以实现多个接口,当创建一个此类的对象之后,通过引用这个对象的对象变量可以访问其所有的公有方法。

请看以下代码:

interface IMyInterface

{

    void func();

}

public class A:IMyInterface

{

    public void func()

    {

        //……

    }

    public void func2()

    {

        //……

    }

}

上述代码定义了一个接口IMyInterface,其中定义了一个func()方法,类A实现了这一方法,同时定义了另一个func2()方法。

当创建一个A对象后,现在可以同时使用func()和func2()两个方法。

A a = new A();

a.func();

a.func2();

在这种情况下,根本分不清哪些方法是由接口定义的,哪些是由类自己定义的。C#提供了一种“显式接口”实现机制,可以区分开这两种情况,其代码如下:

public class A:IMyInterface

{

    void IMyInterface.func()

    {

        //……

    }

    public void func2()

    {

        //……

    }

}

请注意在方法func前以粗体突出显示的接口名称,这就是C#对接口IMyInterface的显式实现方式。同时,不再需要public关键字。

当类A显式实现接口IMyInterface之后,以下代码就无法通过编译。

A a = new A();

a.func();

C#编译器会报告:

“OnlyTest.A”并不包含“func”的定义

 

注意:上述结果中的“OnlyTest”是一个C#示例项目的名称,在实际开发时会被替换为您的C#项目名称。

只有像下面这样修改代码,才可以通过编译。

IMyInterface  a = new A();

a.func();

由此得到一个结论:

如果一个类显式实现某个接口,则只能以此接口类型的变量为媒介调用此接口所定义的方法,而不允许通过类的对象变量直接调用。

或者这样说:

被显式实现的接口方法只能通过接口变量访问,而不能通过类实例直接访问。

3.接口的合并

接口实际上是一种特殊的类型,有意思的是,接口还可以“实现”接口。请看以下代码:

interface One

{

    void f1();

}

interface Two

{

    void f2();

}

//实现了两个接口的接口

interface Three:One,Two

{

    void f3();

}

实现Three接口的类必须同时实现3个方法f1()、f2()和f3()。

class SomeClass:Three

{

    public void f3()

    {    //……    }

    public void f1()

    {   //……     }

    public void f2()

    {   //……     }

}

接口其实是一个特殊的CLR类型,没有基类型,因此,一个接口不能“继承”另一个接口。但说一个接口“实现”另外一些接口也是不准确的,它只是在语法上采用了与类实现接口相同的形式,笔者认为,更确切的词应该是“合并”。

接口的“合并”在实践中用得并不多。

4.接口应用实例——回调

接口在面向对象编程中应用极广。回调(CallBack)就是一个典型的示例。

先解释一下回调的概念。

通常情况下,我们创建一个对象,并马上直接调用它的方法。然而,在有些情况下,希望能在某个场景出现后或条件满足时才调用此对象的方法。回调就可以解决这个“延迟调用对象方法”的问题。这个被调用方法的对象称为回调对象。

实现回调的原理简介如下:

首先创建一个回调对象,然后再创建一个控制器对象,将回调对象需要被调用的方法告诉控制器对象。控制器对象负责检查某个场景是否出现或某个条件是否满足。当此场景出现或此条件满足时,自动调用回调对象的方法。

可以举个现实生活中的例子。

一读者想借《编程的奥秘——.NET软件技术学习与实践》这本书,但这本书已被其他读者借走了。于是,读者与图书馆管理员间发生了以下对话:

读者:“我把我的电话号码告诉你,等书一到就马上通知我。”

管理员:“好的。另一读者把书还回来后,马上给您打电话,书我先帮您留着。”

在上述这个场景中,读者就是“回调对象”,管理员就是“控制器对象”,读者的电话号码就是“回调对象的方法”,另一读者的还书事件就是“某一特定的场景”。

本节示例CallBackExamples展示了如何使用接口实现回调,此示例运行截图如图4-9所示。

图4-9  回调示例

程序运行时,从键盘上按任意一个键显示当前时间,整个程序可以“没完没了”地运行下去,除非您按了Esc键。

示例项目CallBackExamples的类图如图4-10所示。

ICallBack接口定义了一个run()方法。

//实现回调的类必须实现此接口

public interface ICallBack

{

    void run();

}

CallBackClass类实现此接口,并在其run()方法中向控制台窗口输出当前时间。

图4-10  CallBackExamples示例项目类图

class CallBackClass:ICallBack

{

    public void run()

    {

        //输出当前时间

        System.Console.WriteLine(DateTime.Now );

    }

}

Controller类中有一个私有的ICallBack类型的字段,用于存放回调对象的引用,此对象引用在构造函数中传入。

class Controller

{

    public ICallBack CallBackObject = null;// 引用回调对象

    public Controller(ICallBack obj)

    {

        this.CallBackObject = obj;

    }

    public void Begin()

    {

        Console.WriteLine("敲任意键显示当前时间,ESC键退出....");

        while (Console.ReadKey(true).Key != ConsoleKey.Escape)

        {

            CallBackObject.run();

        }

    }

}

Controller类的Begin()方法启动整个处理过程。

调用代码如下:

class Program

{

    static void Main(string[] args)

    {

        //创建控制器对象,将提供给它的回调对象传入

        Controller obj = new Controller(new CallBackClass());

        //启动控制器对象运行

        obj.Begin();

    }

}

可以看到,当示例程序运行时,何时调用CallBackClass对象的的run()方法是由用户决定的,用户每敲一个键,控件器对象就调用一次CallBackClass对象的的run()方法。在这个示例中,实现回调的关键在于ICallBack接口的引入。

读者可能在想,如果不用ICallBack接口,而直接使用CallBackClass对象,也可以实现同样的运行效果。

class Controller

{

    public CallBackClass CallBackObject = null;//回调对象的方法引用

    public Controller(CallBackClass obj)

    {

        this.CallBackObject = obj;

    }

    //……

}

但请仔细想一下,这样做的结果就使Controller类与CallBackClass对象绑定在一起,万一如果需要回调其他类型的对象,则必须修改Controller类的代码(本示例中至少得同时修改Controller类CallBackObject字段的数据类型与构造函数参数的数据类型两处代码)。

如果Controller类接收的是一个抽象的接口变量ICallBack,则任何一个实现了此接口的对象都可以被Controller类对象所回调,Controller类的代码可以保持稳定,无疑是一个好的设计方案。

事实上,接口可以看成是一个契约,它规定了某个对象必须“是什么样的”,即“接口所规定的方法,实现此接口的类一定有”。正是有了这种确定性,Controller类才成为一个用键盘来“发出”回调的“万能控制器”。

 

试一试:

(1)请读者再创建另外一个回调类,让Controller类回调它。

(2)请修改Controller类,让它一次可以回调多个对象或多种类型的对象。

 

提示:

(1)将Controller类的CallBackObject字段改为可容纳多个ICallBack对象的容器,比如数组、ArrayList等。

(2)利用本书13.1.3节中介绍的委托,也能实现同样的回调功能。

5.接口与抽象类的区别

小结一下接口与抽象类的区别。

q 抽象类是一个不完全的类,需要子类来完善它。

q 接口只是对类的约束,它仅仅承诺了类能够调用的方法。

q 一个类一次可以实现若干个接口,但一个类只能继承一个父类。

在实际编程中,接口的使用要比抽象类广泛得多。

4.2  抽象类与接口

我们来看汉语中的一句话:

苹果是一种水果。

在计算机中,将上述关系用“继承”模拟(见图4-1)。

图4-7  用“继承”模拟“苹果是一种水果”关系

根据常识,我们知道世界上存在着真实的苹果实体,却并不存在着一种水果实体。对应地,在程序中只能创建Apple对象,不能创建Fruit对象。

不能创建对象的类称为“抽象类(abstract class)”。

4.2.1  抽象类

1.抽象类与抽象方法

在一个类前面加上“abstract”关键字,此类就成为了抽象类。

对应地,一个方法类前面加上“abstract”关键字,此方法就成为了抽象方法。

abstract class Fruit    //抽象类

{

    public abstract void GrowInArea(); //抽象方法

}

 

提示:Visual Basic.NET使用MustInherit关键字定义抽象类,使用MustOverride关键字定义抽象方法。

注意抽象方法不能有实现代码,在函数名后直接跟一个分号。

抽象类专用于派生出子类,子类必须实现抽象类所声明的抽象方法,否则,子类仍是抽象类。

抽象类一般用于表达一种比较抽象的事物,比如前面所说的“水果”,而抽象方法则说明此抽象类应该具有的某种功能,比如Fruit类中有一个抽象方法GrowInArea(),说明水果一定有一个最适合其生长的地区,但不同的水果这一生长地是不同的。

从同一抽象类中继承的子类拥有相同的方法(即抽象类所定义的抽象方法),但这些方法的具体实现代码每个类都可以不一样,如以下两个类分别代表苹果(Apple)和菠萝(Pineapple)。

class Apple : Fruit //苹果

{

    public override void GrowInArea()

    {

        Console.WriteLine("南方北方都可以种植我。");

    }

}

class Pineapple : Fruit //菠萝

{

    public override void GrowInArea()

    {

        Console.WriteLine("我喜欢温暖,只能在南方看到我。");

    }

}

注意上述代码中的override关键字,这说明子类重写了基类的抽象方法。抽象类不能创建对象,一般用它来引用子类对象。

Fruit f;

f = new Apple();

f.GrowInArea();

f = new Pineapple();

f.GrowInArea();

运行结果:

南方北方都可以种植我。

我喜欢温暖,只能在南方看到我。

 

注意:同一句代码“f.GrowInArea();”会由于f所引用的对象不同而输出不同的结果。可以看到,代码运行结果类似于4.1.3节介绍的“虚方法调用”特性,两者没有本质差别。

可以按照以下格式编写代码:

抽象类 抽象类变量名=new 继承自此抽象类的具体子类名();

一个抽象类中可以包含非抽象的方法和字段。因此:

包含抽象方法的类一定是抽象类,但抽象类中的方法不一定是抽象方法。

在C#中,抽象方法只能出现在抽象类内(即在普通类中不能定义一个抽象方法),而一个抽象类可以没有一个抽象方法(即它的所有方法都不是抽象的)。

这些像绕口令式的结论不用死记,因为在编译程序时,C#编译器会自动帮我们检测代码,并给出详细的提示信息。

 

提示:实现了抽象基类中所有抽象方法的类称为“Concrete Class”,汉语不太好翻译,直译为“具体类”,但笔者认为意译为“可实例化类”更贴切,因为这种类的最大特点之一就是可以使用它来创建对象。

2.抽象属性

除了方法可以是抽象的之外,属性也可以是抽象的,请看以下代码。

abstract class Parent

{

    public abstract String Message   //抽象属性

    {

        get;

        set;

    }

}

class Child : Parent

{

    private String _msg;

    public override  String Message

    {

        get

        {

            return _msg;

        }

        set

        {

            _msg=value;

        }

    }

}

使用代码如下:

Parent p = new Child();

p.Message = "Hello";

3.密封类

抽象类一般作为基类而存在,没有任何子类的抽象类是无意义的。但在C#中,却有一种特殊的类不能拥有任何子类,这就是是密封类。

在一个类前面加上“sealed”关键字,就成了密封类。

sealed class CannotInherit

{

}

除了不能作为基类,密封类的使用与普通类没有什么差别。

4.2.2  接口

我们来看下面这句话:

鸭子是一种鸟,会游泳,同时又是一种食物。

如何在面向对象的程序中表达这种关系?

如果使用C++,可以设计成让鸭子(Duck)类继承自两个父类(鸟Bird和食物Food)。但在C# 中,所有的类都只能有一个父类,此方法不可行。

为了解决这一问题,C#引入了接口(Interface)这一概念,并规定“一个类可以实现多个接口”。

1.接口的定义与使用

关键字interface用于定义接口(示例项目InterfaceExamples)。

//定义两个接口   

public interface ISwim

{

    void Swim();

}

public interface IFood

{

    void Cook();

}

接口可以看成是一种“纯”的抽象类,它的所有方法都是抽象方法。

 

注意:接口的成员默认就是public的,不要画蛇添足地在方法前加上“public”关键字。

可以用与继承相同的语法定义一个类实现某些接口。

//定义一个抽象类

public abstract class Bird

{

    public abstract void Fly();

}

//继承自一个抽象类,实现两个接口

public class Duck : Bird, IFood, ISwim

{

    //实现ISwim接口

    public  void Swim()

    {

        Console.WriteLine("是鸭子就会游泳");

    }

    //实现IFood接口

    public void Cook()

    {

        Console.WriteLine("鸭子经常被烧烤,北京烤鸭就很有名");

    }

    //实现抽象类Bird中的抽象方法

    public override void Fly()

    {

        Console.WriteLine("只有野鸭才会飞");

    }

}

上述代码可以图示为图4-8。

图4-8  实现接口

可以看到,抽象类定义了对象所属的类别,而接口实际上定义了一种对象应具有的行为特性。

 

注意:若一个类同时实现多个接口和派生自一个基类,定义时,基类要放在第一位。

可按以下格式使用接口:

接口类型名 变量名=new 实现了接口的类型名();

示例代码如下:

static void Main(string[] args)

{

    Duck d = new Duck();

    //Duck对象d可以使用3种类型的方法:

    //1.自身定义的

    //2.父类定义的

    //3.接口定义的

    d.Fly();

    d.Cook();

    d.Swim();

    //将子类(Duck)对象赋给基类变量

    Bird b = d;

    //现在只能使用基类定义的Fly()方法

    b.Fly();

    //将Duck对象赋给ISwin接口变量

    ISwim s = d;

    //现在只能使用接口定义的Swim()方法

    s.Swim();

    //将Duck对象赋给另一个实现的接口IFood变量

    IFood f = d;

    //现在只能使用接口定义的Cook()方法

    f.Cook();

}

请读者仔细地阅读上述代码的注释,由于Duck类继承自抽象基类Bird,又实现了ISwim和IFood两个接口,所以,Duck对象拥有这三者所定义的所有方法,并且可以赋值给这三种类型的变量。

需要注意的是,虽然程序中始终都只有一个Duck对象,但将其赋值给不同类型的变量后,其可以使用的方法是不一样的。

2.显式实现接口

上面讲到,某个类可以实现多个接口,当创建一个此类的对象之后,通过引用这个对象的对象变量可以访问其所有的公有方法。

请看以下代码:

interface IMyInterface

{

    void func();

}

public class A:IMyInterface

{

    public void func()

    {

        //……

    }

    public void func2()

    {

        //……

    }

}

上述代码定义了一个接口IMyInterface,其中定义了一个func()方法,类A实现了这一方法,同时定义了另一个func2()方法。

当创建一个A对象后,现在可以同时使用func()和func2()两个方法。

A a = new A();

a.func();

a.func2();

在这种情况下,根本分不清哪些方法是由接口定义的,哪些是由类自己定义的。C#提供了一种“显式接口”实现机制,可以区分开这两种情况,其代码如下:

public class A:IMyInterface

{

    void IMyInterface.func()

    {

        //……

    }

    public void func2()

    {

        //……

    }

}

请注意在方法func前以粗体突出显示的接口名称,这就是C#对接口IMyInterface的显式实现方式。同时,不再需要public关键字。

当类A显式实现接口IMyInterface之后,以下代码就无法通过编译。

A a = new A();

a.func();

C#编译器会报告:

“OnlyTest.A”并不包含“func”的定义

 

注意:上述结果中的“OnlyTest”是一个C#示例项目的名称,在实际开发时会被替换为您的C#项目名称。

只有像下面这样修改代码,才可以通过编译。

IMyInterface  a = new A();

a.func();

由此得到一个结论:

如果一个类显式实现某个接口,则只能以此接口类型的变量为媒介调用此接口所定义的方法,而不允许通过类的对象变量直接调用。

或者这样说:

被显式实现的接口方法只能通过接口变量访问,而不能通过类实例直接访问。

3.接口的合并

接口实际上是一种特殊的类型,有意思的是,接口还可以“实现”接口。请看以下代码:

interface One

{

    void f1();

}

interface Two

{

    void f2();

}

//实现了两个接口的接口

interface Three:One,Two

{

    void f3();

}

实现Three接口的类必须同时实现3个方法f1()、f2()和f3()。

class SomeClass:Three

{

    public void f3()

    {    //……    }

    public void f1()

    {   //……     }

    public void f2()

    {   //……     }

}

接口其实是一个特殊的CLR类型,没有基类型,因此,一个接口不能“继承”另一个接口。但说一个接口“实现”另外一些接口也是不准确的,它只是在语法上采用了与类实现接口相同的形式,笔者认为,更确切的词应该是“合并”。

接口的“合并”在实践中用得并不多。

4.接口应用实例——回调

接口在面向对象编程中应用极广。回调(CallBack)就是一个典型的示例。

先解释一下回调的概念。

通常情况下,我们创建一个对象,并马上直接调用它的方法。然而,在有些情况下,希望能在某个场景出现后或条件满足时才调用此对象的方法。回调就可以解决这个“延迟调用对象方法”的问题。这个被调用方法的对象称为回调对象。

实现回调的原理简介如下:

首先创建一个回调对象,然后再创建一个控制器对象,将回调对象需要被调用的方法告诉控制器对象。控制器对象负责检查某个场景是否出现或某个条件是否满足。当此场景出现或此条件满足时,自动调用回调对象的方法。

可以举个现实生活中的例子。

一读者想借《编程的奥秘——.NET软件技术学习与实践》这本书,但这本书已被其他读者借走了。于是,读者与图书馆管理员间发生了以下对话:

读者:“我把我的电话号码告诉你,等书一到就马上通知我。”

管理员:“好的。另一读者把书还回来后,马上给您打电话,书我先帮您留着。”

在上述这个场景中,读者就是“回调对象”,管理员就是“控制器对象”,读者的电话号码就是“回调对象的方法”,另一读者的还书事件就是“某一特定的场景”。

本节示例CallBackExamples展示了如何使用接口实现回调,此示例运行截图如图4-9所示。

图4-9  回调示例

程序运行时,从键盘上按任意一个键显示当前时间,整个程序可以“没完没了”地运行下去,除非您按了Esc键。

示例项目CallBackExamples的类图如图4-10所示。

ICallBack接口定义了一个run()方法。

//实现回调的类必须实现此接口

public interface ICallBack

{

    void run();

}

CallBackClass类实现此接口,并在其run()方法中向控制台窗口输出当前时间。

图4-10  CallBackExamples示例项目类图

class CallBackClass:ICallBack

{

    public void run()

    {

        //输出当前时间

        System.Console.WriteLine(DateTime.Now );

    }

}

Controller类中有一个私有的ICallBack类型的字段,用于存放回调对象的引用,此对象引用在构造函数中传入。

class Controller

{

    public ICallBack CallBackObject = null;// 引用回调对象

    public Controller(ICallBack obj)

    {

        this.CallBackObject = obj;

    }

    public void Begin()

    {

        Console.WriteLine("敲任意键显示当前时间,ESC键退出....");

        while (Console.ReadKey(true).Key != ConsoleKey.Escape)

        {

            CallBackObject.run();

        }

    }

}

Controller类的Begin()方法启动整个处理过程。

调用代码如下:

class Program

{

    static void Main(string[] args)

    {

        //创建控制器对象,将提供给它的回调对象传入

        Controller obj = new Controller(new CallBackClass());

        //启动控制器对象运行

        obj.Begin();

    }

}

可以看到,当示例程序运行时,何时调用CallBackClass对象的的run()方法是由用户决定的,用户每敲一个键,控件器对象就调用一次CallBackClass对象的的run()方法。在这个示例中,实现回调的关键在于ICallBack接口的引入。

读者可能在想,如果不用ICallBack接口,而直接使用CallBackClass对象,也可以实现同样的运行效果。

class Controller

{

    public CallBackClass CallBackObject = null;//回调对象的方法引用

    public Controller(CallBackClass obj)

    {

        this.CallBackObject = obj;

    }

    //……

}

但请仔细想一下,这样做的结果就使Controller类与CallBackClass对象绑定在一起,万一如果需要回调其他类型的对象,则必须修改Controller类的代码(本示例中至少得同时修改Controller类CallBackObject字段的数据类型与构造函数参数的数据类型两处代码)。

如果Controller类接收的是一个抽象的接口变量ICallBack,则任何一个实现了此接口的对象都可以被Controller类对象所回调,Controller类的代码可以保持稳定,无疑是一个好的设计方案。

事实上,接口可以看成是一个契约,它规定了某个对象必须“是什么样的”,即“接口所规定的方法,实现此接口的类一定有”。正是有了这种确定性,Controller类才成为一个用键盘来“发出”回调的“万能控制器”。

 

试一试:

(1)请读者再创建另外一个回调类,让Controller类回调它。

(2)请修改Controller类,让它一次可以回调多个对象或多种类型的对象。

 

提示:

(1)将Controller类的CallBackObject字段改为可容纳多个ICallBack对象的容器,比如数组、ArrayList等。

(2)利用本书13.1.3节中介绍的委托,也能实现同样的回调功能。

5.接口与抽象类的区别

小结一下接口与抽象类的区别。

q 抽象类是一个不完全的类,需要子类来完善它。

q 接口只是对类的约束,它仅仅承诺了类能够调用的方法。

q 一个类一次可以实现若干个接口,但一个类只能继承一个父类。

在实际编程中,接口的使用要比抽象类广泛得多。

posted @ 2007-06-28 15:59  大牛博客  阅读(654)  评论(2编辑  收藏  举报