可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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;
}
}