系列三:C#表达式设计(接口,委托,事件)

【解决派生类中不能重写基类中实现了接口中的方法】

(1)在派生类中用new的方法重新建立一个相同的方法(但是用接口类型调用的时候不会调用派生类new的方法)。

(2)在派生类中重新继承该接口并实现(接口类型可以正常调用)。

(3)在基类中把接口中的方法实现为虚方法(virtual)或者抽象(abstract )这样派生类就可以重写基类中实现了接口的方法了,并且接口类型的对象也可以正确调用派生类中的方法。

1.我们可以将接口用做函数的参数,并返回值。由于不相关的类型可以共同实现一个接口,因此我们将有更多机会重用代码。下面两个方法执行的是同样的任务:

public void PrintCollection( IEnumerable collection )

{

foreach( object o in collection )

Console.WriteLine( "Collection contains {0}",   o.ToString( ) );

}

public void PrintCollection( CollectionBase collection )

{

foreach( object o in collection )

Console.WriteLine( "Collection contains {0}",  o.ToString( ) );

}

第2个方法的可重用性比较差,它不能和Arrays、ArrayLists、DataTables、Hashtables、ImageLists或其他很多集合类一起使用。将接口作为方法的参数类型不仅适应面广,而且易于重用

派生类不能重写基类中实现的接口成员。

 

2.我们也可以用一种让派生类可以重写实现的方式来实现接口。

interface IMsg

{

void Message();

}

public class MyClass : IMsg

{

public void Message()

{

    Console.WriteLine( "MyClass" );

}

}

Message()方法是MyClass公有接口的一部分。Message()也可以通过IMsg来访问,因为IMsg是MyClass类型的一部分。现在如果添加一个派生类,情况就会变得稍微有些复杂:

public class MyDerivedClass : MyClass

{

public new void Message()

{

    Console.WriteLine( "MyDerivedClass" );

}

}

注意,上面的Message()方法的定义中必须添加new关键字(参见条款29)。MyClass.Message()并不是一个虚方法。派生类不能为其提供一个重写的版本。MyDerived类实际上创建了一个新的Message()方法。这个新的Message()方法和MyClass.Message()方法并不构成重写关系,它仅仅是把MyClass中的Message()方法给隐藏了。另外,MyClass.Message()仍然可以通过IMsg引用来访问:

MyDerivedClass d = new MyDerivedClass( );

d.Message( ); // 打印"MyDerivedClass"。

IMsg m = d as IMsg;

m.Message( ); // 打印"MyClass"。

 

我们通常要创建接口,然后在基类中实现它们,并在派生类中更改它们的实现。是的,我们可以这么做,有两种做法供我们选择。如果不能访问基类,我们可以在派生类中重新实现接口:

public class MyDerivedClass : MyClass, IMsg

{

public new void Message()

{

    Console.WriteLine( "MyDerivedClass" );

}

}

添加IMsg接口会改变派生类的行为,所以现在IMsg.Message()会使用派生类中提供的实现:

MyDerivedClass d = new MyDerivedClass( );

d.Message( ); // 打印"MyDerivedClass"。

IMsg m = d as IMsg;

m.Message( ); // 打印"MyDerivedClass"

在MyDerivedClass.Message()方法上,我们仍然需要使用new关键字。这可能会让大家感觉存在某种问题(参见条款29)。基类中对IMsg.Message()的实现仍然可以通过一个基类对象的引用来获得:

MyDerivedClass d = new MyDerivedClass( );

d.Message( ); // 打印"MyDerivedClass"。

IMsg m = d as IMsg;

m.Message( ); // 打印"MyDerivedClass"

MyClass b = d;

b.Message( ); // 打印"MyClass"

 

修正这个问题的唯一办法就是修改基类——将接口方法声明为虚方法:

public class MyClass : IMsg

{

public virtual void Message()

{

    Console.WriteLine( "MyClass" );

}

}

public class MyDerivedClass : MyClass

{

public override void Message()

{

    Console.WriteLine( "MyDerivedClass" );

}

}

现在MyDerivedClass——以及所有继承自MyClass的类——都可以声明它们自己的Message()方法。每次被调用的也都是重写的版本,不管是通过MyDerivedClass引用,还是通过IMsg引用,或者通过MyClass引用。

如果不喜欢这种不够纯粹的虚函数,可以对MyClass的定义做一个小的改动:

public abstract class MyClass, IMsg

{

public abstract void Message();

}

是的,我们可以在实现一个接口的同时,不实现接口中的方法。通过将接口中的方法声明为抽象方法,我们实际上是在要求所有派生类都必须提供该接口的实现。IMsg接口是MyClass声明的一部分,但是定义该方法的工作被延迟到了各个派生类中。

显式接口实现(explicit interface implementation)使我们可以在实现一个接口的同时,将其成员从类型的公有接口中隐藏掉。它为实现接口和重写虚方法之间的关系带来了一些其他的变数。当有一个更合适的版本可用时,我们可以使用显式接口实现来限制客户代码直接调用接口方法。条款26中的IComparable设计惯用法向我们详细地展示了这一点。

实现接口拥有的选择要比创建和重写虚函数多。我们可以为类层次创建密封(sealed)的实现、虚实现或者抽象的合同。我们也可以决定派生类如何以及何时修改“基类中实现的接口成员的默认行为”。接口方法不是虚方法,而是一个单独的合同。


 2.回调用于为服务器和客户机之间提供异步的反馈。这中间可能会牵扯到多线程,或者需要为同步更新提供一个入口点。在C#中,回调使用委托来表达。

委托对象中包含一个方法引用,该方法可以是静态方法,也可以是实例方法。使用委托,我们可以和一个或多个在运行时配置的客户对象进行通信。可以将所有包含单个函数调用的委托对象组合为多播委托(multicast delegate)。但在这种构造中有两点需要我们注意:第一,如果有委托调用出现异常,那么这种构造将不能保证安全;第二,整个调用的返回值将为最后一个函数调用的返回值。在一个多播委托调用中,每一个目标会被顺次调用。委托对象本身不会捕捉任何异常。因此,任何目标抛出的异常都会结束委托链的调用。返回值也有类似的问题。我们定义的委托可以有具体的返回类型(非void)。例如,我们可能会编写一个回调来检查用户是否要异常结束: 

public delegate bool ContinueProcessing();

public void LengthyOperation( ContinueProcessing pred )

{

foreach( ComplicatedClass cl in _container )

{

    cl.DoLengthyOperation();

    // 检查用户是否要异常结束:

    if (false == pred())

      return;

}

}

作为单个委托,上面的代码工作得很好,但是如果作为多播委托使用,就会出现问题:

ContinueProcessing cp = new ContinueProcessing (CheckWithUser );

cp += new ContinueProcessing( CheckWithSystem );

c.LengthyOperation( cp );

多播委托返回的值是委托链上最后一个函数调用的返回值。所有其他的返回值都会被忽略。例如,上面代码中CheckWithUser()的返回值就会被忽略。

我们可以自己调用委托链上的每个委托目标,从而避免上述两个问题。所创建的每一个委托都包含一个委托链表。要检查委托链并调用每一个目标,我们需要自己遍历调用链表:

public delegate bool ContinueProcessing();

public void LengthyOperation( ContinueProcessing pred )

{

bool bContinue = true;

foreach( ComplicatedClass cl in _container )

{

    cl.DoLengthyOperation();

    foreach( ContinueProcessing pr in

      pred.GetInvocationList( ))

      bContinue &= pr();

    if (false == bContinue)

      return;

}

}

在上面的代码中,我们要求每一个委托调用都为true时遍历才能继续。委托为我们提供了一种在运行时进行回调的最好方式,这种方式对客户类只有非常简单的要求。我们可以在运行时配置委托目标。另外,委托也支持多个客户目标在.NET中,客户回调应该使用委托来实现。


 3.

 一些类(绝大多数是Windows控件)包含的事件数量非常多。这时候,为每个事件都定义一个字段的做法是不可接受的。在某些情况下,只有很少的事件会在程序中真正发挥作用。当遇到这种情况时,我们可以改变设计,根据运行时的需要动态创建事件对象。 .NET框架内核在Windows控件子系统中包含有这方面的做法示例。为了演示其中的做法,这里会向logger类添加一些子系统。我们可以为每个子系统创建一个事件。客户则会登记和它们的子系统相关的事件。扩展后的Logger类包含一个 System.ComponentModel.EventHandlerList容器该容器中存储着所有的事件对象,这些事件将会针对具体的子系统来被触发。更新后的AddMsg()方法现在接受一个字符串参数,用于指定产生日志消息的子系统。如果子系统有侦听者,那么事件就会被触发。另外,如果一个事件侦听者登记了所有的消息,它的事件也会被触发:

public class Logger

{

private static System.ComponentModel.EventHandlerList  Handlers = new System.ComponentModel.EventHandlerList();//创建事件集合

static public void AddLogger(string system, AddMessageEventHandler ev )

{

    Handlers[ system ] = ev;//添加事件

}

static public void RemoveLogger( string system )

{

    Handlers[ system ] = null;//删除事件

}

static public void AddMsg ( string system, int priority, string msg )

{

    if ( ( system != null ) && ( system.Length > 0 ) )

    {

      AddMessageEventHandler l = Handlers[ system ] as AddMessageEventHandler;//转换成委托连

      LoggerEventArgs args = new LoggerEventArgs(priority, msg );//构造事件的数据参数

      if ( l != null )//判断事件是否为空

        l ( null, args );//调用事件

      // 空字符串意味着接受所有消息:

      l = Handlers[ "" ] as AddMessageEventHandler;

      if ( l != null )

         l( null, args );

    }

}

}

上面的代码会在EventHandlerList 集合中存储各个事件处理器。当客户代码关联到一个特定的子系统上时,新的事件对象就会被创建。对同一个子系统的后续请求会获取相同的事件对象。如果我们的类在其接口中包含有大量的事件,我们就应该考虑使用这样的事件处理器集合。当客户代码真正关联有事件处理器时,我们才会创建事件成员。在.NET框架内部,System.Windows.Forms.Control类使用了一种更复杂的实现,来隐藏所有事件字段操作的复杂性。每一个事件字段在内部会通过访问一个对象集合,来添加和删除特定的处理器。大家可以在C#语言规范(参见条款49)中找到有关这种常见做法的更多信息。

综上所述,我们使用事件来定义类型中的外发接口,任意数量的客户对象都可以将自己的处理器登记到事件上,然后处理它们。这些客户对象不需要在编译时存在。事件也不必非要有订阅者才能正常工作。在C#中使用事件可以对发送者和可能的通知接受者进行解耦。发送者可以完全独立于接收者进行开发。事件是一种广播类型行为信息的标准方式。 

 

 

posted @ 2011-01-19 10:34  yu_liantao  阅读(386)  评论(0)    收藏  举报