CLR笔记:10.事件

事件也是方法。

定义一个事件成员意味着类型具有三种能力:
    *类型的静态方法/实例方法可以订阅类型事件
    *类型的静态方法/实例方法可以注销类型事件
    *事件发生时通知已订阅事件的方法

.NET2.0的事件仍然是基于Win32的,只不过使用了Observer模式来实现,同时建立在Delegate机制之上。
事件的设计步骤如下(基本上是Observer的实现步骤):

10.1    设计一个对外提供事件的类型

1.定义EventArgs或子类,用于存放附加信息:
    定义一个类,继承于EventArgs,以EventArgs结束,包含一组私有字段以及相应的只读公共属性。
    public class NewMailEventArgs : EventArgs
    {
        
private string from;

        
public string From
        {
            
get { return from; }
        }
    }
   
    这里,EventArgs基类在FCL中是这个样子的:
    [Serializable]
    [ComVisible(
true)]
    
public class EventArgs
    {
        
// Summary:
        
//     表示没有事件数据的事件。
        public static readonly EventArgs Empty;

        
public EventArgs();
    }

    大多数事件没有附加数据,那么就不用定义任何私有字段和属性,直接使用EventArgs基类作为参数。

2.定义事件成员:
    class MailManager
    
{
        
public event EventHandler<NewMailEventArgs> NewMail;
    }

    这条语句等价于:
        public delegate void EventHandler<TVEventArgs>(Object sender, TVEventArgs e) where TVEventArgs: NewMailEventArgs;

    所以方法原型相应为 void MethodName(Object sender, NewMailEventArgs e)

    这里,第一个参数sender类型是Object,因为要兼容所有类型,所以提供一个最广泛的基类型。
            第二个参数名始终是e,而且派生于EventArgs,保持了对Observer模式的一致性,所有人(包括VS2005)都会调用这个e
            事件方法要求都为void,即不允许有回调值,从而事件链易于操作。

3.定义引发事件的方法——负责通知订阅事件的对象:
    这是一个protected的虚方法,并接受EventArgs或其子类的参数。
    这个虚方法可以由派生类重写,以添加新的功能;不重写也可以,因为基本上已经可以使用了
    class MailManager
    
{
        
protected virtual void OnNewMail(NewMailEventArgs e)
        
{
            EventHandler
<NewMailEventArgs> temp = NewMail;

            
if (temp != null)
                temp(
this, e);
        }

    }

    这里,使用临时变量temp,是为了防止可能存在的线程同步问题。

4.定义一个激发事件的方法

将输入转换成EventArgs或其子类的对象,然后激发事件
    internal class MailManager
    
{
        
public void SimulateNewMail(String from, String to, String subject)
        

            NewMailEventArgs e 
= new NewMailEventArgs(from, to, subject);
            OnNewMail(e);
        }

    }


10.3    设计订阅者的类,使用事件
    在ctor中订阅事件,绑定FaxMsg回调方法,在Unregister方法中注销事件
    提供回调方法FaxMsg,当事件激发时自动调用
    internal sealed class Fax
    
{
        
public Fax(MailManager mm)
        
{
            mm.NewMail 
+= FaxMsg;
        }


        
private void FaxMsg(Object sender, NewMailEventArgs e)
        
{
            Console.WriteLine(
"Fax: {0}, {1}, {2}", e.From, e.To, e.Subject);
        }


        
public void Unregister(MailManager mm)
        
{
            mm.NewMail 
-= FaxMsg;
        }

    }
    
注意:使用+=和-=操作符,而不能显示使用add/remove方法
        事件注销的意义:只要有一个对象还有一个方法仍然订阅事件,该对象就不会被垃圾收集
            IDispose接口的Dispose方法,注销所有事件。
        FaxMsg方法的sender参数为MailMessager对象,可以使用sender访问MailMessager的对象成员,

补充:在Main函数中实现:
   public static void Main() {

      MailManager mm 
= new MailManager();

      
//注册pager和fax
      Fax fax = new Fax(mm);
      Pager pager 
= new Pager(mm);

      
//通知pager和fax
      mm.SimulateNewMail("Jeffrey""Kristin""I Love You!");

      
//注销fax,只剩下pager
      fax.Unregister(mm);
    
      
//只通知pager
      mm.SimulateNewMail("Jeffrey""Mom & Dad""Happy Birthday.");
   }


10.2    事件机制
对于public event EventHandler<NewMailEventArgs> NewMail;
    C#编译时,相应为
            //一个初始化为null的私有委托字段:    
            private EventHandler<NewMailEventArgs> NewMail = null;

            
//一个订阅事件的公共方法:
            [MethodImpl(MethodImplOptions.Synchronized)]
            
public void add_NewMail(EventHandler<NewMailEventArgs> value)
            
{
                NewMail 
= (EventHandler<NewMailEventArgs>)Delegate.Combine(NewMail, value);
            }


            
//一个注销事件的公共方法:
            [MethodImpl(MethodImplOptions.Synchronized)]
            
public void remove_NewMail(EventHandler<NewMailEventArgs> value)
            
{
                NewMail 
= (EventHandler<NewMailEventArgs>)Delegate.Remove(NewMail, value);
            }


注:    在IL中也是3个成员:一个私有字段,两个公有方法
          如果将event声明为protected,则两个方法也相应为protected
          event也可以是static或virtual,则两个方法也相应为static或virtual


10.4    事件与线程安全
在上面的实例中,System.Runtime.CompilerServices命名空间下,自定义属性[MethodImpl(MethodImplOptions.Synchronized)]保证了事件的线程同步。
但是这样的同步会有问题。
    对于实例事件,CLR使用自身对象作为线程同步锁;
    对于静态事件,CLR使用类型对象作为线程同步锁。
但是线程同步指导方针指出,方法永远不要在对象本身或类型对象上加锁,否则这个锁对外公开,会导致其它线程死锁

没有好的办法保证值类型的实例事件成员是线程安全的,因为C#不会为其add/remove生成[MethodImpl(MethodImplOptions.Synchronized)]
值类型的静态事件成员肯定是线程安全的。

10.5    显示控制事件的订阅与注销
即显示的实现add和remove访问器方法:
    建立一个临时委托变量m_NewMail与相应的属性,代替原先的事件成员NewMail,
    新建一个作为线程同步锁的私有实例字段m_eventLock
主要改动如下:
    class MailManager
    
{
        
private EventHandler<NewMailEventArgs> m_NewMail;

        
public event EventHandler<NewMailEventArgs> NewMail
        
{
            add 
            
{
                
lock (m_eventLock)
                
{
                    m_NewMail 
+= value;
                }

            }


            remove
            
{
                
lock (m_eventLock)
                
{
                    m_NewMail 
-= value;
                }

            }

        }

    }

注意,C#不能分辨add/remove方法是由编译器自动创建的,还是程序员显示实现的,所以仍可以使用+=和-=这两个操作符处理事件。

10.6    多事件模型
System.Windows.Forms.Control类型有70多个事件,不可能用上述方法实现,会造成未使用事件对内存的浪费。
解决办法:使用注册工厂,建立事件池。具体见设计模式。




posted @ 2007-09-05 22:41  包建强  Views(2379)  Comments(1Edit  收藏  举报