views:

84

answers:

4

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  A: 

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);
Chris Taylor
Yes NullReference will not come out now. But even I add `ref`, `RegisterLogger(cs.OnStart)` won't work.
Danny Chen
@Danny Chen, I addressed your second problem, the reason the first call will not work is because outside of the declaring class an event can only be used to add new handlers, this is to ensure that outside callers are not able to arbitrarily cause the event to fire by invoking the event directly, only the Add/Remove functionality of the Event is exposed outside of the declaring class.
Chris Taylor
Ah, I just learned that delegate variables live on the stack. Good to know. :)
Jeff M
@Jeff M: Be careful not to misunderstand this! "Living on the stack" is something that *all* local variables within a method do, unless passed `ref` or `out` -- value type or reference type. This says nothing of the values/objects to which those local variables are assigned. Really the solution Chris has presented here has nothing to do with delegates; it is simply the case that if you assign a new value to the local variable that is used to hold a parameter, you are doing nothing to the original value passed in. This is **always** the case, except when the parameter is `ref` or `out`.
Dan Tao
@Jeff M: In other words, there is a big difference between **calling methods on objects** (this is where behavior differs between value types and reference types) and **assigning new values to variables using any of the assignment operators: `=`, `+=`, `-=`, `++`, etc.** (this is where type is irrelevant to behavior). Delegates, for the record, actually are reference types.
Dan Tao
@Dan: What I meant was that delegates were not reference types (which I had initially thought).
Jeff M
@Jeff M: Delegate **are** reference types. Try this: `Console.WriteLine(typeof(Action).IsValueType);` It will print `False`. *This is what I am trying to tell you!* Do not confuse the issue of value types vs. reference types with what happens when you assign a local variable to a new value or object. They are two completely unrelated matters.
Dan Tao
@Dan: Oh I see where my confusion is coming from. The issue that was twisting me was that delegate instances are immutable and for whatever reason I thought `+=` was adding to invocation list of the current instance. In my own tests, I tried to replicate the desired result of `RegisterLogger()` and spaced out on why it didn't work. I now see that you explained that in your second comment to me. I guess I can blame that on being in Python mode while working on C# code. Thanks.
Jeff M
+1  A: 

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;
}
Merlyn Morgan-Graham
why doesn't C# allow us to pass events to other classes? Why so strictly?
Danny Chen
@Danny: Ask the C# design team, I guess? It's frustrated me a few times too.
Merlyn Morgan-Graham
@Danny: events are not *values*. Similarly, properties, fields, indexers, classes, structs, interfaces, enums, methods, attributes, local variables, formal parameters and type parameters are not values. You can't pass a *property*. You can pass the *value* that results from calling a property getter, but you can't pass *the property*. You can't pass an *event*; you can pass the *value* of the delegate associated with the event, but the event itself isn't a value any more than a property is a value.
Eric Lippert
@Eric: Thanks for the comment :)
Merlyn Morgan-Graham
A: 

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;
    }
}
Jeff M
+1  A: 

"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).

Dan Tao