2.事件

事件

  1. 使用委托时,一般会出现两种角色:广播者(Broadcaster)和订阅者(Subscriber)。

    • 广播者:包含委托字段的类型,它通过调用委托决定何时进行广播。
    • 订阅者:目标方法的接受者。订阅者通过在广播者的委托上调用 +=-= 来决定何时开始监听而何时结束监听。订阅者不知道也不会干涉其他订阅者。
  2. 事件(Events)

    • 事件就是将广播者/订阅者这一模式正式化的语言功能。
    • 事件是一种使用有限的委托功能实现广播者/订阅者模型的结构,只暴露了所需的委托特性的部分子集
    • 使用事件的主要目的在于防止订阅者之间相互干扰。
  3. 声明事件的最简单方法是在委托成员的前面加上 event 关键字

    // Delegate definition
    public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);
    public class Broadcaster
    {
        // Event declaration
        public event PriceChangedHandler PriceChanged;
    }
    
    • Broadcaster 类型的代码对 PriceChanged 有完全的访问权限,并可以将其视为委托;
    • Broadcaster 类型之外的代码仅可以在 PriceChanged 事件上执行 +=-= 操作(不能将 PriceChanged 作为委托对其执行 +- 以及赋值为 null 等操作)。

事件的工作机制 🌴
当声明如下的事件时,在内部发生了三件事:

public class Broadcaster
{
    public event PriceChangedHandler PriceChanged;
}
  • 首先,编译器将事件的声明翻译为如下形式:

    PriceChangedHandler priceChanged; // private delegate
    public event PriceChangedHandler PriceChanged
    {
        add { priceChanged += value; }
        remove { priceChanged -= value; }
    }
    

    addremove 关键字明确了事件的访问器,类似于属性访问器。

  • 然后,编译器查看 Broadcaster 类内部对 PriceChanged 的引用,如果不是 +=-= 的操作,将其重定向到内部的 priceChanged 委托字段。

  • 最后,编译器对事件上的 +=-= 操作相应地调用 addremove 访问器(应用于事件时,+=-= 是唯一的,而不像其他情况下是 +- 运算符和赋值运算符的简写)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fXnkY44z-1602725273694)(_v_images/20200818152623023_17736.png =108x)]

Stock 类中,每当 StockPrice 发生变化,PriceChanged 事件就会触发。

public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);

public class Stock
{
    decimal price;
    public event PriceChangedHandler PriceChanged;
    public decimal Price
    {
        get { return price; }
        set
        {
            if (price == value) return;  // Exit if nothing has changed
            decimal oldPrice = price;
            price = value;
            if (PriceChanged != null)  // If invocation list not
                PriceChanged(oldPrice, price);  // empty, fire event.
        }
    }
}

class Test
{
    static void Main(string[] args)
    {
        Stock st = new Stock();
        st.PriceChanged += PrintChangedPrice;
        st.Price = 3;  // oldPrice:0 newPrice:3
    }

    static void PrintChangedPrice(decimal oldPrice, decimal newPrice)
        => Console.WriteLine($"oldPrice:{oldPrice} newPrice:{newPrice}");
}

本例中,如果将 event 关键字去掉, PriceChanged 就变成了普通的委托字段,虽然运行结果是不变的,但 Stock 类就没有原来健壮了。因为这个时候订阅者就可以通过以下方式相互影响:

  • 通过重新指派 PriceChanged 替换其他的订阅者(不用 += 运算符)。
  • 清除所有的订阅者(将 PriceChanged 设置为 null)。
  • 通过调用其委托广播到其他的订阅者。
Stock st = new Stock();
st.PriceChanged += PrintChangedPrice;
st.PriceChanged = null;  // Compile-time error: 事件“Stock.PriceChanged”只能出现在 += 或 -= 的左边(从类型“Stock”中使用时除外)

1 标准事件模式

.Net Framework 为事件编程定义了一个标准模式,它的目的就是保持框架和用户代码的一致性(为了让你秃头 💀)。

  1. 标准事件模式的核心是 System.EventArgs 类。

    • 预定义的框架类,除了静态的 Empty 属性外没有其他成员。

    • EventArgs 是为事件传递信息的基类。
      Stock 类中,可以继承 EventArgs 以便在 PriceChanged 事件触发事传递新的和旧的 Price 值。

      public class PriceChangedEventArgs : System.EventArgs
      {
          public readonly decimal lastPrice;
          public readonly decimal newPrice;
          public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
          {
              this.lastPrice = lastPrice;
              this.newPrice = newPrice;
          }
      }
      
    • 考虑到复用性,EventArgs 子类应当根据它包含的信息来命名(而非根据使用它的事件命名)。

    • EventArgs 子类一般将数据以属性或只读字段的方式暴露给外界。

  2. EventArgs 子类就位后,下一步就是选择或者定义事件的委托了,这一步需要遵循三个原则:

    • 委托必须以 void 作为返回值
    • 委托必须接受两个参数,第一个参数是 object 类型,第二个参数则是 EventArgs 的子类。第一个参数表明了事件的广播者,第二个参数则包含了需要传递的额外信息。
    • 委托的名称必须以 EventHandler 结尾

    框架定义了一个名为 System.EventHandler<> 的泛型委托,该委托满足以上三个条件:
    public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;

  3. 接下来就是定义选定的委托类型的事件了。使用泛型的 EventHandler 委托:

    public class Stock
    {
        ...
        public event EventHandler<PriceChangedEventArgs> PriceChanged;
    }
    
  4. 最后,该模式需要编写一个 protected virtual 方法来触发事件方法名必须和事件名称一致,以 On 为前缀,并接收唯一EventArgs 参数

    public class Stock
    {
        ...
        public event EventHandler<PriceChangedEventArgs> PriceChanged;
        protected virtual void OnPriceChanged(PriceChangedEventArgs e)
        {
            if (PriceChanged != null)
                PriceChanged(this, e);
        }
    }
    

    🐳 在多线程情形下,为了保证线程安全,在测试和调用委托之前,需将它保存在一个临时变量中:

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

    我们可以使用 csharp 6 的 null 条件运算符来避免临时变量的声明:

    PriceChanged?.Invoke(this, e);
    

    这种方式既线程安全又书写简明,是现阶段最好的事件触发方式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i7ds23VT-1602725273703)(_v_images/20200818152636368_25502.png =108x)]

/// <summary>
/// PriceChangedEventArgs 继承 EventArgs 类,用于传递事件信息
/// </summary>
public class PriceChangedEventArgs : EventArgs
{
    public readonly decimal lastPrice, newPrice;
    public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
    {
        this.lastPrice = lastPrice; 
        this.newPrice = newPrice;
    }
}

public class Stock
{
    /// <summary>
    /// 构造函数
    /// </summary>
    string symbol;
    public Stock(string symbol) 
    {
        this.symbol = symbol;
    }

    /// <summary>
    /// 定义事件
    /// </summary>
    public event EventHandler<PriceChangedEventArgs> PriceChanged;

    /// <summary>
    /// 定义方法触发事件
    /// </summary>
    /// <param name="e">事件传递的信息</param>
    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
        PriceChanged?.Invoke(this, e);  // 事件调用目标方法
    }

    /// <summary>
    /// 属性
    /// </summary>
    decimal price;
    public decimal Price
    {
        get { return price; }
        set
        {
            if (price == value) return;
            decimal oldPrice = price;
            price = value;
            OnPriceChanged( new PriceChangedEventArgs(oldPrice, price) );  // 触发事件
        }
    }
}

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;
    }

    /// <summary>
    /// 定义事件目标方法,触发事件后的处理方法
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e">事件传递的信息</param>
    static void stock_PriceChanged(object sender, PriceChangedEventArgs e)
    {
        if ( (e.NewPrice - e.lastPrice) / e.lastPrice > 0.1M )
        Console.WriteLine("Alert, 10% stock price increase!");
    }
}

如果事件不需要传递额外的信息,则可以使用预定义的非泛型委托 EventHandler。本例中,我们重写 Stock 类,当 Price 属性发生变化时,触发 PriceChanged 事件,事件出了传达已发生的消息之外没有必须包含的信息。为了避免创建 EventArgs 实例,我们使用了 EventArgs.Empty 属性:

public class Stock
{
    string symbol;
    decimal price;
    public Stock(string symbol)
    {
        this.symbol = symbol;
    }
    public event EventHandler PriceChanged;
    protected virtual void OnPriceChanged(EventArgs e)
    {
        PriceChanged?.Invoke(this, e);
    }
    public decimal Price
    {
        get { return price; }
        set
        {
            if (price == value) return;
            price = value;
            OnPriceChanged(EventArgs.Empty);
        }
    }
}

2 事件访问器

事件访问器是对事件的 +=-= 功能的实现。默认情况下,访问器由编译器隐式实现。考虑如下的声明:

public event EventHandler PriceChanged;

编译器将其转化为:

  • 一个私有委托字段。
  • 一对公有的事件访问器函数(add_PriceChangedremove_PriceChanged),它们将 +=-= 操作转向了私有字段。
  1. 我们可以显式定义事件访问器来替代上述过程。以下是 PriceChanged 事件的手动实现:

    private EventHandler priceChanged; // Declare a private delegate
    public event EventHandler PriceChanged
    {
        add { priceChanged += value; }
        remove { priceChanged -= value; }
    }
    

    本例从功能上和 csharp 的默认访问器实现是等价的(但是 csharp 还使用了无锁的比较并交换算法,保证了在更新委托时的线程安全性)。有了自定义事件访问器,csharp 就不会生成默认的字段和访问器逻辑。

  2. 显式定义事件访问器,可以在委托的存储和访问上进行更复杂的操作。主要用于:

    • 当前事件访问器仅仅是广播事件的类的中继器。

    • 当类定义了大量的事件,而大部分事件有很少的订阅者。

    • 当显式实现声明事件的接口时,如下:

      public interface IFoo { event EventHandler Ev; }
      class Foo : IFoo
      {
          private EventHandler ev;
          event EventHandler IFoo.Ev
          {
              add { ev += value; }
              remove { ev -= value; }
          }
      }
      

    🌱 事件的 addremove 部分会分别编译为 add_XXXremove_XXX 方法。

3 事件的修饰符

和方法类似,事件可以是虚的(virtual)、可以重写(overridden)、可以是抽象的(abstract)或者密封的(sealed),也可以是静态的(static)。

public class Foo
{
    public static event EventHandler<EventArgs> StaticEvent;
    public virtual event EventHandler<EventArgs> VirtualEvent;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值