代理,我不理解的事件约定

4

我查看了这个来自《C#入门经典》一书的例子(http://www.albahari.com/nutshell/ch04.aspx)。

using System;

public class PriceChangedEventArgs : EventArgs
{
  public readonly decimal LastPrice;
  public readonly decimal NewPrice;

  public PriceChangedEventArgs (decimal lastPrice, decimal newPrice)
  {
    LastPrice = lastPrice; NewPrice = newPrice;
  }
}

public class Stock
{
  string symbol;
  decimal price;

  public Stock (string symbol) {this.symbol = symbol;}

  public event EventHandler<PriceChangedEventArgs> PriceChanged;

  ****protected virtual void OnPriceChanged (PriceChangedEventArgs e)
  {
    if (PriceChanged != null) PriceChanged (this, e);
  }****

  public decimal Price
  {
    get { return price; }
    set
    {
      if (price == value) return;
      OnPriceChanged (new PriceChangedEventArgs (price, value));
      price = value;
    }  
  }
}

class Test
{
  static void Main()
  {
    Stock stock = new Stock ("THPW");
    stock.Price = 27.10M;
    // register with the PriceChanged event    
    stock.PriceChanged += stock_PriceChanged;
    stock.Price = 31.59M;
  }

  static void stock_PriceChanged (object sender, PriceChangedEventArgs e)
  {
    if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M)
      Console.WriteLine ("Alert, 10% stock price increase!");
  }
}

我不理解的是为什么要使用这个约定,它与IT技术有关。
  ****protected virtual void OnPriceChanged (PriceChangedEventArgs e)
  {
    if (PriceChanged != null) PriceChanged (this, e);
  }****

我为什么需要这个方法并且为什么要关注给它传递"this"参数?! 我不能直接在测试类中将来自该类的事件与PriceChanged方法连接起来并跳过该方法吗?!


你会如何附加它?那个方法正在执行附加操作。你不必使用该方法,你可以直接使用其中的代码,在需要的地方替换即可。 - Tomislav Markovski
3个回答

6

你需要进行空值检查,因为在有人订阅事件之前,事件将为null。如果直接引发它并且它是null,就会抛出异常。

这个方法用于引发事件,而不是订阅事件。你可以轻松地从另一个类订阅事件:

yourObject.PriceChanged += someMethodWithTheAppropriateSignature;

然而,当您想触发事件时,类需要引发该事件。 "this"参数在EventHandler<T>中提供了发送者参数。按照惯例,用于事件的委托具有两个参数,第一个是object sender,它应该是引发事件的对象。第二个是EventArgsEventArgs的子类,它提供特定于该事件的信息。该方法用于正确检查null并使用适当的信息引发事件。

在这种情况下,您的事件声明如下:

public event EventHandler<PriceChangedEventArgs> PriceChanged;

EventHandler<PriceChangedEventArgs> 是一个委托,其签名为:

public delegate void EventHandler<T>(object sender, T args) where T : EventArgs

这意味着要使用两个参数触发事件 - 一个对象(发送者或"this"),以及一个 PriceChangedEventArgs 实例。

话虽如此,这种约定并不是实际上触发事件的"最佳"方式。实际上更好的做法是:

protected virtual void OnPriceChanged (PriceChangedEventArgs e)
{
    var eventHandler = this.PriceChanged;
    if (eventHandler != null) 
        eventHandler(this, e);
}

这样做可以在多线程场景下保护您,因为如果有多个线程操作,可能会出现单个订阅实际上在您的空值检查和触发之间取消订阅的情况。


你的代码不是线程安全的。任何使用托管资源(或任何其他有状态资源)的类在列表复制和实际调用之间可能已被释放。例如(1. 列表复制,2. 取消订阅/释放,3. 调用)。 - jgauffin
有没有这个约定的原因...为什么我需要传递发送方参数? - Dmitry Makovetskiyd
@jgauffin 我在这个答案的结尾特别提到了线程安全性... - Reed Copsey
@jgauffin 当你进行复制时,你会完全复制调用列表。取消订阅不会影响本地副本。那段代码是处理线程安全的建议和正确方式。(请注意,我使用复制品来触发事件,而不是原始事件。) - Reed Copsey
@ReedCopsey:没错。他说,除非处理程序“强大”,否则代码不是线程安全的。我的代码可以防止这种情况,因为我没有使用副本(这允许在取消订阅后直接处理程序进行处理,而无需使处理程序“强大”)。 - jgauffin
显示剩余2条评论

4

这是调用事件的便利方式。

您需要检查该事件是否有订阅者,并且通常将this作为事件的发送者进行传递。

由于相同的处理程序可以用于多个事件,因此仅通过传递发送者的实例才能可靠地从事件中取消订阅一次它被触发。

我认为最好的调用方式是先分配给一个变量,以免在检查后调用之前,PriceChanged变成null:

var handler = PriceChanged;
if(handler != null) handler(this, e);

3

当 (event) 委托列表没有订阅者时,需要使用 Null 检查,因为它不是空的,而是 null

但是,它不是线程安全的。所以如果你开始使用 BackgroundWorker 或任何其他多线程技术,它可能会爆炸。

我建议你使用一个空委托:

public event EventHandler<PriceChangedEventArgs> PriceChanged = delegate {};

由于它允许您只需编写以下内容:

protected virtual void OnPriceChanged (PriceChangedEventArgs e)
{
   PriceChanged (this, e);
}

它是线程安全的,代码更易读。

我为什么要给它“this”参数?!?

同一事件处理程序可能会被多个事件生成器使用。发送方告诉调用是哪个生成器的。您应该始终发送正确的事件生成器,因为这是预期的,如果不这样做,将打破开闭原则。

我为什么需要那个方法?

除非您否则将重复代码(例如生成EventArgs类),否则不需要。


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