将事件视为对象
C#还不够OO吗? 在这里,我给出一个(可能是坏的)例子。
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)导致编译错误,因为“事件XXX只能出现在+ =或 – = blabla的左侧”。 但为什么RegisterLogger(p.OnStart)可以? 同时,虽然我注册了p.OnStart,但它也会抛出一个NullReferenceException,似乎p.OnStart并没有“真正”传递给一个方法。
“事件XXX只能出现在+ =或 – = blabla的左侧”
这实际上是因为C#足够“OO”。 OOP的核心原则之一是封装; 事件提供了这种forms,就像属性一样:在声明类中,它们可以直接访问,但在外部它们只暴露给+=
和-=
运算符。 这样,声明类就可以完全控制事件的调用时间。 客户端代码只能对调用它们时发生的事情有发言权。
您的代码RegisterLogger(p.OnStart)
编译的原因是它是在Program
类的范围内声明的,其中声明了Program.OnStart
事件。
您的代码RegisterLogger(cs.OnStart)
无法编译的原因是它是从Program
类的范围内声明的,但MyCSharpProgram.OnStart
事件(显然)在MyCSharpProgram
类中声明。
正如Chris Taylor指出的那样,你在p.OnStart(p, EventArgs.Empty);
行上得到NullReferenceException
的原因p.OnStart(p, EventArgs.Empty);
是调用RegisterLogger
因为它为局部变量赋值,对作为参数传入的局部变量赋值的对象没有影响 。 要更好地理解这一点,请考虑以下代码:
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);
正如将int
作为参数并为其赋值的方法仅影响复制到其堆栈的局部变量一样,将EventHandler
作为参数并为其赋值的方法仅将其局部变量影响为好(在任务中)。
对RegisterLogger进行以下更改,将ev
声明为事件处理程序的引用参数。
static void RegisterLogger(ref EventHandler ev) { ev += LogOnStart; }
然后,在调用方法时,您的调用点还需要使用’ref’关键字,如下所示
RegisterLogger(ref p.OnStart);
无法编译的原因:
RegisterLogger(cs.OnStart);
…是事件处理程序和传递给它的方法是在不同的类中。 C#非常严格地处理事件,并且只允许事件出现的类除了添加处理程序(包括将其传递给函数或调用它)之外的任何事情。
例如,这也不会编译(因为它在不同的类中):
cs.OnStart(cs, EventArgs.Empty);
至于无法以这种方式将事件处理程序传递给函数,我不确定。 我猜测事件就像值类型一样运作。 通过ref传递它将解决您的问题,但:
static void RegisterLogger(ref EventHandler ev) { ev += LogOnStart; }
当一个对象声明一个事件时,它只公开向该类外部的事件添加和/或删除处理程序的方法(前提是它不重新定义添加/删除操作)。 在其中,它被视为“对象”,并且或多或少地像声明的委托变量一样工作。 如果没有向事件添加处理程序,就好像它从未被初始化并且为null
。 这是设计的方式。 以下是框架中使用的典型模式:
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...
现在,如果您必须坚持在类之外公开委托,请不要将其定义为事件。 然后,您可以将其视为任何其他字段或属性。
我修改了您的示例代码来说明:
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; } }