Treat events as objects

2019-04-16 20:46发布

问题:

C# is still not OO enough? Here I'm giving a (maybe bad) example.

public class Program
{
   public event EventHandler OnStart;
   public static EventHandler LogOnStart = (s, e) => Console.WriteLine("starts");

   public class MyCSharpProgram
   {
      public string Name { get; set; }
      public event EventHandler OnStart;
      public void Start()
      {
          OnStart(this, EventArgs.Empty);
      }
   }

   static void Main(string[] args)
   {
      MyCSharpProgram cs = new MyCSharpProgram { Name = "C# test" };
      cs.OnStart += LogOnStart;  //can compile
      //RegisterLogger(cs.OnStart);   // Line of trouble
      cs.Start();   // it prints "start" (of course it will :D)

      Program p = new Program();
      RegisterLogger(p.OnStart);   //can compile
      p.OnStart(p, EventArgs.Empty); //can compile, but NullReference at runtime
      Console.Read();
    }

    static void RegisterLogger(EventHandler ev)
    {
      ev += LogOnStart;
    }
}

RegisterLogger(cs.OnStart) leads to compile error, because "The event XXX can only appear on the left hand side of += or -= blabla". But why RegisterLogger(p.OnStart) can? Meanwhile, although I registered p.OnStart, it will also throw an NullReferenceException, seems that p.OnStart is not "really" passed to a method.

回答1:

"The event XXX can only appear on the left hand side of += or -= blabla"

This is actually because C# is "OO enough." One of the core principles of OOP is encapsulation; events provide a form of this, just like properties: inside the declaring class they may be accessed directly, but outside they are only exposed to the += and -= operators. This is so that the declaring class is in complete control of when the events are called. Client code can only have a say in what happens when they are called.

The reason your code RegisterLogger(p.OnStart) compiles is that it is declared from within the scope of the Program class, where the Program.OnStart event is declared.

The reason your code RegisterLogger(cs.OnStart) does not compile is that it is declared from within the scope of the Program class, but the MyCSharpProgram.OnStart event is declared (obviously) within the MyCSharpProgram class.

As Chris Taylor points out, the reason you get a NullReferenceException on the line p.OnStart(p, EventArgs.Empty); is that calling RegisterLogger as you have it assigns a new value to a local variable, having no affect on the object to which that local variable was assigned when it was passed in as a parameter. To understand this better, consider the following code:

static void IncrementValue(int value)
{
    value += 1;
}

int i = 0;
IncrementValue(i);

// Prints '0', because IncrementValue had no effect on i --
// a new value was assigned to the COPY of i that was passed in
Console.WriteLine(i);

Just as a method that takes an int as a parameter and assigns a new value to it only affects the local variable copied to its stack, a method that takes an EventHandler as a parameter and assigns a new value to it only affects its local variable as well (in an assignment).



回答2:

Make the following change to RegisterLogger, declare ev as a reference argument to the event handler.

static void RegisterLogger(ref EventHandler ev) 
{ 
  ev += LogOnStart; 
}

Then your call point will also need to use the 'ref' keyword when invoking the method as follows

RegisterLogger(ref p.OnStart);


回答3:

The reason this fails to compile:

RegisterLogger(cs.OnStart);

... is that the event handler and the method you are passing it to are in different classes. C# treats events very strictly, and only allows the class that the event appears in to do anything other than add a handler (including pass it to functions, or invoke it).

For example, this won't compile either (because it is in a different class):

cs.OnStart(cs, EventArgs.Empty);

As for not being able to pass an event handler to a function this way, I'm not sure. I am guessing events operate like value types. Passing it by ref will fix your problem, though:

static void RegisterLogger(ref EventHandler ev)
{
    ev += LogOnStart;
}


回答4:

When an object declares an event, it only exposes methods to add and/or remove handlers to the event outside of the class (provided it doesn't redefine the add/remove operations). Within it, it is treated much like an "object" and works more or less like a declared delegate variable. If no handlers are added to the event, it's as if it were never initialized and is null. It is this way by design. Here is the typical pattern used in the framework:

public class MyCSharpProgram
{
    // ...

    // define the event
    public event EventHandler SomeEvent;

    // add a mechanism to "raise" the event
    protected virtual void OnSomeEvent()
    {
        // SomeEvent is a "variable" to a EventHandler
        if (SomeEvent != null)
            SomeEvent(this, EventArgs.Empty);
    }
}

// etc...

Now if you must insist on exposing the delegate outside of your class, just don't define it as an event. You could then treat it as any other field or property.

I've modified your sample code to illustrate:

public class Program
{
    public EventHandler OnStart;
    public static EventHandler LogOnStart = (s, e) => Console.WriteLine("starts");

    public class MyCSharpProgram
    {
        public string Name { get; set; }

        // just a field to an EventHandler
        public EventHandler OnStart = (s, e) => { /* do nothing */ }; // needs to be initialized to use "+=", "-=" or suppress null-checks
        public void Start()
        {
            // always check if non-null
            if (OnStart != null)
                OnStart(this, EventArgs.Empty);
        }
    }

    static void Main(string[] args)
    {
        MyCSharpProgram cs = new MyCSharpProgram { Name = "C# test" };
        cs.OnStart += LogOnStart;  //can compile
        RegisterLogger(cs.OnStart);   // should work now
        cs.Start();   // it prints "start" (of course it will :D)

        Program p = new Program();
        RegisterLogger(p.OnStart);   //can compile
        p.OnStart(p, EventArgs.Empty); //can compile, but NullReference at runtime
        Console.Read();
    }

    static void RegisterLogger(EventHandler ev)
    {
        // Program.OnStart not initialized so ev is null
        if (ev != null) //null-check just in case
            ev += LogOnStart;
    }
}