为什么不能使用访问器来引发事件?

7

为什么我们不能使用自定义实现来触发事件,而在没有它们的情况下是可能的呢?请看以下代码:

public class Program
{
    private EventHandler myEvent;
    public event EventHandler MyEvent
    {
        add { myEvent += value; }
        remove { myEvent -= value; }
    }

    public event EventHandler AnotherEvent;

    public static void Main()
    {
        var target = new Program();
        target.MyEvent(null, null);       // ERROR CS0079
        target.AnotherEvent(null, null);  // compiles
    }
}

你看到两个事件都在我的类中声明。虽然target.AnotherEvent(...)可以编译通过,但target.MyEvent(...)不能:

事件MyEvent只能出现在+=或-=的左侧。

我知道事件只是一个带有添加和删除方法的委托。因此,编译器将AnotherEvent转换为添加和删除方法:

private EventHandler _AnotherEvent;
public event EventHandler AnotherEvent
{ 
    add { _AnotherEvent += value; }
    remove { _AnotherEvent -= value; }
}

我认为编译器将对AnotherEvent 的调用替换为对私有委托_AnotherEvent(...)的调用。

关于第二个调用成功而第一个调用失败的原因是否有相关文档?或者针对编译器在这里所做的工作是否有任何描述?

1
它与 myEvent 协同工作。我猜是因为 MyEvent 是真正事件的门面模式。 - Patrick Hofman
@PatrickHofman 我已经知道它在 myEvent 中可以工作。而且我已经猜到了这一点。 - MakePeaceGreatAgain
1
不知道具体细节或规格,但我认为add/removeget/set不同,它被翻译为void AddToInvocationList(delegate...);void RemoveFromInvocationList(...);,因此内部的EventHandler字段不会暴露给类的使用者。 - René Vogt
1
@Adrian 虽然重复的答案回答了“解决方案”部分(这甚至不是我的问题),但它并没有回答我的问题:“为什么我们可以做一个而不能做另一个”。 - MakePeaceGreatAgain
1
手动/自动添加/删除的IL实现是相同的。但是事件的调用是在字段上完成的。因此,公共事件被替换为字段,并在该字段上调用。 - Jeroen van Langen
显示剩余3条评论
4个回答

4
当使用自动事件时,例如public event EventHandler AnotherEvent;。编译器会为其创建一个字段(和一些方法),并在该字段上执行调用。因此public event不再存在,它只是语法糖。
因此,无法调用非自动事件。因为在编译后的代码中找不到它。它被add_remove_方法替换了。你只能在生成的私有字段上执行调用。
这就解释了为什么不能在类实例外部调用事件。

2

它无法工作,因为根本没有办法获取实际的可调用事件处理程序。正如您已经注意到的那样,只有addremove,没有get

事件处理程序的生成代码如下:

.event [mscorlib]System.EventHandler MyEvent
{
  .addon instance void ConsoleApp1.Program::add_MyEvent(class [mscorlib]System.EventHandler)
  .removeon instance void ConsoleApp1.Program::remove_MyEvent(class [mscorlib]System.EventHandler)
} // end of event Program::MyEvent

它添加了两个方法引用,一个是 add,另一个是 remove。如果你看一下,它怎么知道要调用哪个方法呢?如果 addremove 比现在复杂得多怎么办?没有确定要调用的事件处理程序的方法。


1
这是语法糖。你可以像调用后备字段一样调用AnotherEvent,这是编译器提供的便利(AnotherEvent是所谓的类似字段的事件)。一旦你添加了自己的访问器,事件声明就不再是类似字段的事件,必须通过其后备字段调用。
请参阅C#语言规范相关部分:
引用: 类似字段的事件 在包含事件声明的类或结构体的程序文本中,某些事件可以像字段一样使用。要以这种方式使用事件,它不能是抽象的或外部的,也不能明确包含event_accessor_declarations。这种事件可以在允许字段的任何上下文中使用。该字段包含引用列表的委托(委托)已添加到事件处理程序。如果没有添加事件处理程序,则该字段包含null。
(强调我的)

0

建议在添加或删除新的事件处理程序方法之前锁定事件。

话虽如此,请看一下这段代码:

public event EventHandler MyEvent
{
    add
    {
        lock (objectLock)
        {
            myEvent += value;
        }
    }
    remove
    {
        lock (objectLock)
        {
            myEvent -= value;
        }
    }
}

public event EventHandler AnotherEvent; 可以工作的原因是如果您的代码中未提供自定义事件访问器,编译器将自动添加它们。

按照此文档 如何实现自定义事件访问器获取有关适当实现的更多详细信息,另一个来源请参阅此文章

关于实现:

 private EventHandler myEvent;
 public event EventHandler MyEvent
    {
        add
        {
            lock (objectLock)
            {
                myEvent += value;
            }
        }
        remove
        {
            lock (objectLock)
            {
                myEvent -= value;
            }
        }
    }

    public event EventHandler AnotherEvent;

    public static void Main()
    {
        var target = new Program();
        var myEvent =  target.MyEvent;
        myEvent?.Invoke(EventArgs.Empty, EventArgs.Empty);      
        target.AnotherEvent(null, null); 
    }

编辑以解释实现:

var myEvent =  target.MyEvent;

使用显式事件时,您必须提供自己的后备存储 - 可以是委托字段或类似于 EventHandlerList 的东西,因此我们在这里使用 var


1
超出范围,那单线程应用怎么办?这是关于 C# 的语法糖,它将 event 声明替换为方法和字段。 - Jeroen van Langen
首先,我对代码进行了更改,只是为了通知一下 :) 其次,即使是单线程应用程序,锁定事件处理程序处理委托也是一个好习惯。这是一种额外的预防措施,以确保一切都能正常工作。 - Barr J
这也意味着在调用时应该对其进行锁定。例如,锁定/创建副本/解锁并执行。我宁愿不要跨线程添加/删除事件处理程序。 - Jeroen van Langen
这就像他们喜欢说的那样,是看观者的眼光而定的 :) - Barr J

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接