C# events vs. delegates

来源:互联网 发布:开淘宝网店的步骤视频 编辑:程序博客网 时间:2024/04/30 23:37
 

We have looked at delegates and their implementation in two previous articles. But if you searched some more information about delegates on the web, you surely noticed they are almost always associated with the "event" construct.
Online event tutorials make it look like events are something pretty different from regular delegates instance, although related. Events are usually explained as if they were a special type or construct. But we will see they really are a modifier on the delegate type, which adds some restrictions that the compiler enforces and also adds two accessors (similar to theget and set for properties).

A first look at event vs. regular delegate
As I was finishing my previous posts on delegates, another C# construct started baking my noodle: events. Events definitely seem related to delegates, but I couldn't figure out how they differ.

From their syntax, events look like a field holding a combination of delegates, which is just what a multicast delegate is. Also they support the same combination operators as delegates (+ and-).
In the following sample program (which has no useful functionality what-so-ever) we see thatmsgNotifier (with event construct) and msgNotifier2 (plain delegate) appear to behave exactly the same way for all intents and purposes.

using System;

namespace EventAndDelegate
{
  delegate void MsgHandler(string s);

  class Class1
  {
   public static event MsgHandler msgNotifier;
   public static MsgHandler msgNotifier2;
   [STAThread]
   static void Main(string[] args)
   {
    Class1.msgNotifier += new MsgHandler(PipeNull);
    Class1.msgNotifier2 += new MsgHandler(PipeNull);
    Class1.msgNotifier("test");
    Class1.msgNotifier2("test2");
   }
 
   static void PipeNull(string s)
   {
    return;
   }
  }
}


Looking at the IL code for the Main method in this code, you will notice that both delegatesmsgNotifier and msgNotifier2 are again used exactly the same way.

.method private hidebysig static void Main(string[] args) cil managed
{
  .entrypoint
  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
  // Code size 95 (0x5f)
  .maxstack 4
  IL_0000: ldsfld class EventAndDelegate.MsgHandler  EventAndDelegate.Class1::msgNotifier
  IL_0005: ldnull
  IL_0006: ldftn void EventAndDelegate.Class1::PipeNull(string)
  IL_000c: newobj instance void EventAndDelegate.MsgHandler::.ctor(object,
     native int)
  IL_0011: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
     class [mscorlib]System.Delegate)
  IL_0016: castclass EventAndDelegate.MsgHandler
  IL_001b: stsfld class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier
  IL_0020: ldsfld class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier2
  IL_0025: ldnull
  IL_0026: ldftn void EventAndDelegate.Class1::PipeNull(string)
  IL_002c: newobj instance void EventAndDelegate.MsgHandler::.ctor(object,
     native int)
  IL_0031: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
     class [mscorlib]System.Delegate)
  IL_0036: castclass EventAndDelegate.MsgHandler
  IL_003b: stsfld class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier2
  IL_0040: ldsfld class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier
  IL_0045: ldstr "test"
  IL_004a: callvirt instance void EventAndDelegate.MsgHandler::Invoke(string)
  IL_004f: ldsfld class EventAndDelegate.MsgHandler EventAndDelegate.Class1::msgNotifier2
  IL_0054: ldstr "test2"
  IL_0059: callvirt instance void EventAndDelegate.MsgHandler::Invoke(string)
  IL_005e: ret
} // end of method Class1::Main


Looking at the C# keywords list on MSDN. It turns out that event is only a modifier. The question is what modification does it bring?

The added value of event
Events and interfaces
First, an event can be included in an interface declaration, whereas a field cannot. This is the most important behavior change introduced by theevent modifier. For example:

interface ITest
{
  event MsgHandler msgNotifier; // compiles
  MsgHandler msgNotifier2; // error CS0525: Interfaces cannot contain fields
}
 
class TestClass : ITest
{
  public event MsgHandler msgNotifier; // When you implement the interface, you need to implement the event too
  static void Main(string[] args) {}
}


Event invocation
Furthermore, an event can only be invoked from within the class that declared it, whereas a delegate field can be invoked by whoever has access to it. For example:

using System;

namespace EventAndDelegate
{
  delegate void MsgHandler(string s);

  class Class1
  {
   public static event MsgHandler msgNotifier;
   public static MsgHandler msgNotifier2;

   static void Main(string[] args)
   {
    new Class2().test();
   }
  }
 
  class Class2
  {
   public void test()
   {
    Class1.msgNotifier("test"); // error CS0070: The event 'EventAndDelegate.Class1.msgNotifier' can only appear on the left hand side of += or -= (except when used from within the type 'EventAndDelegate.Class1')
    Class1.msgNotifier2("test2"); // compiles fine
   }
  }
}

This restriction on invocations is quite strong. Even derived classes from the class declaring the event aren't allowed to fire the event. A way to deal with this is to have aprotected virtual method to trigger the event.


Event accessors
Also, events come with a pair of accessor methods. They have an add and remove method.
This is similar to properties, which offer a pair of get and set methods.

You are allowed to override these accessors, as shown in examples 2 and 3 on thisC# event modifier reference on MSDN. Although I don't see how example 2 is useful, you could imagine that you could have a customadd to send some notification or write a log entry, for example, when a listener is added to your event.
The add and remove accessors need to be customized together, otherwise you get errorCS0065 ('Event.TestClass.msgNotifier' : event property must have both add and remove accessors).
Looking at the IL for a previous example, where the event accessors weren't customized, I noticed compiler generated methods (add_msgNotifier andremove_msgNotifier) for the msgNotifier event. But they weren't used, and whenever the event was accessed the same IL code was duplicated (inlined).
But when you customize these accessors and look at the IL again, you'll notice that the generated accessors are now used when you access the event. For example, this code :

using System;

namespace Event
{
  public delegate void MsgHandler(string msg);

  interface ITest
  {
   event MsgHandler msgNotifier; // compiles
   MsgHandler msgNotifier2; // error CS0525: Interfaces cannot contain fields
  }
 
  class TestClass : ITest
  {
   public event MsgHandler msgNotifier
   {
    add
    {
     Console.WriteLine("hello");
     msgNotifier += value;
    }

   }
 
   static void Main(string[] args)
   {
    new TestClass().msgNotifier += new MsgHandler(TestDel);
   }
   static void TestDel(string x)
   {
   }
  }
}

brings the following IL for the Main method:
{
  .entrypoint
  // Code size 23 (0x17)
  .maxstack 4
  IL_0000: newobj instance void Event.TestClass::.ctor()
  IL_0005: ldnull
  IL_0006: ldftn void Event.TestClass::TestDel(string)
  IL_000c: newobj instance void Event.MsgHandler::.ctor(object,
     native int)
  IL_0011: call instance void Event.TestClass::add_msgNotifier(class Event.MsgHandler)
  IL_0016: ret
} // end of method TestClass::Main


Event signature
Finally, even though C# allows it, the .NET framework adds a restriction on the signature of delegates that can be used as events. The signature should befoo(object source, EventArgs e), where source is the object that fired the event ande contains any additional information about the event.


Conclusion
We have seen that the event keyword is a modifier for a delegate declaration that allows it to be included in an interface, constraints it invocation from within the class that declares it, provides it with a pair of customizable accessors (add and remove) and forces the signature of the delegate (when used within the .NET framework).


Links
Events Tutorial on MSDN.

Event keyword reference on MSDN.

Update:
One question that was left open and that was brought up by some readers was the rationale behind the restriction on event invocation: "Invoking an event can only be done from within the class that declared the event". I am still trying to get a definitive answer via some internal discussion lists, but here is the best idea that I got so far.
I think it is because of a syntaxic problem. When you put an access specifier ("private", "public", ...) on an event it controls who can register or listen to that event.
The question is how would you specify the access control for the invocation of that event. You can't use the same specifiers because it would be confusing.
The solution is to have the event invocation be completely restricted and allow the coder to write a custom invocation method on which he can easily control the access, which is the way it is now.

An alternate solution might have been to use some kind of attribute on the event [EventAccess(PublicInvocation)] or [EventAccess(ProtectedInvocation)]. But that seems uglier because it requires reflection to control the access at runtime.


Update:
Race condition in common event firing pattern:

As any other object, an event object needs to be treated with care in multi-threaded scenarios.

JayBazand EricGu point out a frequent race condition mistake with event firing:

if (Click != null)
    Click(arg1, arg2);

Note that all the MSDN samples I have seen use the dangerous pattern.

Posted by Julien on April 29, 2003. Permalink

 

原文地址:http://blog.monstuff.com/archives/000040.html

原创粉丝点击