C# 中的事件是一些有趣的事情。它们非常类似于自动属性,但具有私有 get 方法和公共(或您选择的任何访问权限)set 方法。
请允许我演示一下。让我们创建一个包含假设事件的假设类。
class SomeObject{
public event EventHandler SomeEvent;
public void DoSomeStuff(){
OnSomeEvent(EventArgs.Empty);
)
protected virtual void OnSomeEvent(EventArgs e){
var handler = SomeEvent;
if(handler != null)
handler(this, e);
}
}
此类遵循公开事件的类的典型模式。它公开公开该事件,但有一个受保护的虚拟“On...”方法,默认情况下,该方法仅调用该事件(如果它有任何订阅者)。这个受保护的虚方法不仅封装了实际调用事件的逻辑,而且为派生类提供了一种方法:
- 以更少的开销方便地处理事件,
- 在所有外部订阅者收到事件之前或之后执行一些处理,
- 调用一个完全不同的事件,或者
- 完全压制该事件。
但是这个名为 SomeEvent 的“事件”对象是什么?在 C# 中,我们熟悉字段、属性和方法,但事件到底是什么?
在我们开始讨论之前,认识一下 C# 中实际上只有两种类型的类成员:字段和方法。属性和事件或多或少只是它们之上的语法糖。
属性实际上是一种或两种方法,以及存储在元数据中的名称,C# 编译器允许您使用该名称来引用这两种方法之一。也就是说,当您定义这样的属性时:
public string SomeProperty{
get{return "I like pie!";}
set{
if(string.Compare(value, "pie", StringComparison.OrdinalIgnoreCase) == 0)
Console.WriteLine("Pie is yummy!");
else Console.WriteLine("\"{0}\" isn't pie!", value ?? "<null>");
}
}
编译器为你编写了两个方法:
public string get_SomeProperty(){return "I like pie!";}
public void set_SomeProperty(string value){
if(string.Compare(value, "pie", StringComparison.OrdinalIgnoreCase) == 0)
Console.WriteLine("Pie is yummy!");
else Console.WriteLine("\"{0}\" isn't pie!", value ?? "<null>");
}
我并不是间接地表达这个意思。这两个方法实际上成为编译类的一部分,以及有关属性的大量元数据,它告诉编译器下次在读取(获取)或写入(设置)属性时要调用哪些方法。所以当你写这样的代码时:
var foo = someObject.SomeProperty;
someObject.SomeProperty = foo;
编译器找到分配给的 getter 和 setter 方法SomeProperty
,并将您的代码变成:
string foo = someObject.get_SomeProperty();
someObject.set_SomeProperty(foo);
这就是为什么如果您定义一个具有公共字段的类,但后来决定将其更改为属性,以便在读取或写入该类时可以做一些有趣的事情,则必须重新编译包含对此的引用的任何外部程序集成员,因为字段访问指令需要改为方法调用指令。
现在这个属性有些反常,不依赖任何后盾。它的 getter 返回一个常量值,并且它的 setter 没有将其值存储在任何地方。需要明确的是,这是完全有效的,但大多数时候,我们定义的属性更像是这样的:
string someProperty;
public string SomeProperty{get{return someProperty;}set{someProperty = value;}}
除了读取和写入字段之外,此属性不执行任何操作。它与名为的公共字段几乎相同SomeProperty
,除了你could稍后向该 getter 和 setter 添加逻辑,而不会使您的类的使用者重新编译。但这种模式非常常见,以至于 C# 3 添加了“自动属性”来达到相同的效果:
public string SomeProperty{get;set;}
编译器将其转换为与我们上面编写的代码相同的代码,只是支持字段有一个只有编译器知道的超级秘密名称,因此我们只能在代码中引用该属性,即使在类本身中也是如此。
因为我们无法访问支持字段,而您可能具有如下所示的只读属性:
string someProperty;
public string SomeProperty{get{return someProperty;}}
你几乎永远不会看到只读自动属性(编译器允许你编写它们,但你会发现它们很少用):
public string SomeProperty{get;} // legal, but not very useful unless you always want SomeProperty to be null
相反,您通常会看到这样的内容:
public string SomeProperty{get;private set;}
The private
访问修饰符附加到set
使得类内的方法可以设置属性,但该属性在类外仍然显示为只读。
“现在这和事件有什么关系呢?”你可能会问。事实上,事件非常类似于自动属性。通常,当您声明事件时,编译器会生成一个超级秘密支持字段和一对方法。除了支持字段并不是那么超级秘密,并且这对方法不是“获取”和“设置”,而是“添加”和“删除”。让我演示一下。
当你写一个这样的事件时:
public event EventHandler SomeEvent;
编译器写的是这样的:
EventHandler SomeEvent;
public void add_SomeEvent(EventHandler value){
SomeEvent = (EventHandler)Delegate.Combine(SomeEvent, value);
}
public void remove_SomeEvent(EventHandler value){
SomeEvent = (EventHandler)Delegate.Remove(SomeEvent, value);
}
它还添加了一些元数据粘合,以便稍后,当您编写如下代码时:
void Awe_SomeEventHandler(object sender, EventArgs e){}
void SomeMethod(SomeObject Awe){
Awe.SomeEvent += Awe_SomeEventHandler
Awe.SomeEvent -= Awe_SomeEventHandler
}
编译器将其重写为(仅有趣的行):
Awe.add_SomeEvent(Awe_SomeEventHandler);
Awe.remove_SomeEvent(Awe_SomeEventHandler);
这里需要注意的是,唯一可公开访问的成员与SomeEvent
是那些添加和删除方法,当您使用+=
and -=
运营商。支持字段(名为 SomeEvent 的委托对象,保存事件的订阅者)是一个私有字段,只有声明类的成员才能访问。
然而,就像自动属性只是手动编写支持字段以及 getter 和 setter 的快捷方式一样,您也可以显式声明委托并添加和删除方法:
internal EventHandler someEvent;
public event EventHandler SomeEvent{
add{someEvent = (EventHandler)Delegate.Combine(someEvent, value);}
remove{someEvent = (EventHandler)Delegate.Remove(someEvent, value);}
}
然后,程序集中的其他类可以触发您的事件:
var handler = Awe.someEvent;
if(handler != null)
handler(Awe, EventArgs.Empty);
然而,以正常(自动)方式定义事件更简单、更惯用,只需公开“Raise”方法即可:
internal void RaiseSomeEvent(){OnSomeEvent(EventArgs.Empty);}
但现在你希望明白why你必须这样做,并且后台发生了什么。