C#8.0本质论第十四章--事件

C#8.0本质论第十四章–事件

委托本身是一个更大的模式(Pattern)的基本单位,称为Publish-Subscribe(发布-订阅)或Observer(观察者)。

14.1使用多播委托实现Publish-Subscribe模式

14.1.1定义订阅者方法
public class Cooler
{
    public Cooler(float temperature)
    {
        Temperature = temperature;
    }
 
    // Cooler is activated when ambient temperature
    // is higher than this
    public float Temperature { get; set; }
 
    // Notifies that the temperature changed on this instance
    public void OnTemperatureChanged(float newTemperature)
    {
        if(newTemperature > Temperature)
        {
            System.Console.WriteLine("Cooler: On");
        }
        else
        {
            System.Console.WriteLine("Cooler: Off");
        }
    }
}
 
public class Heater
{
    public Heater(float temperature)
    {
        Temperature = temperature;
    }
 
    // Heater is activated when ambient temperature
    // is lower than this
    public float Temperature { get; set; }
 
    // Notifies that the temperature changed on this instance
    public void OnTemperatureChanged(float newTemperature)
    {
        if(newTemperature < Temperature)
        {
            System.Console.WriteLine("Heater: On");
        }
        else
        {
            System.Console.WriteLine("Heater: Off");
        }
    }
}
14.1.2定义发布者
public class Thermostat
{
    // Define the event publisher (initially without the sender)
    public Action<float>? OnTemperatureChange { get; set; }
 
    public float CurrentTemperature { get; set; }
}
14.1.3连接发布者和订阅者
public class Program
{
    public static void Main()
    {
        Thermostat thermostat = new();
        Heater heater = new(60);
        Cooler cooler = new(80);
 
        thermostat.OnTemperatureChange +=
            heater.OnTemperatureChanged;
        thermostat.OnTemperatureChange +=
            cooler.OnTemperatureChanged;
 
        Console.Write("Enter temperature: ");
        string? temperature = Console.ReadLine();
        if (!int.TryParse(temperature, out int currentTemperature))
        {
            Console.WriteLine($"'{temperature}' is not a valid integer.");
            return;
        }
        thermostat.CurrentTemperature = currentTemperature;
    }
}
14.1.4调用委托
public class Thermostat
{
    // ...
    public float CurrentTemperature
    {
        get { return _CurrentTemperature; }
        set
        {
            if (value != CurrentTemperature)
            {
                _CurrentTemperature = value;
 
                // Call subscribers
                // Incomplete, check for null needed
                // ...
                OnTemperatureChange(value);
                // ...
            }
        }
    }
    private float _CurrentTemperature;
}
14.1.5检查空值
public class Thermostat
{
    // Define the event publisher
    public Action<float>? OnTemperatureChange { get; set; }
 
    public float CurrentTemperature
    {
        get { return _CurrentTemperature; }
        set
        {
            if(value != CurrentTemperature)
            {
                _CurrentTemperature = value;
                // If there are any subscribers,
                // notify them of changes in 
                // temperature by invoking said subscribers
                OnTemperatureChange?.Invoke(value);     // C# 6.0
            }
        }
    }
 
    private float _CurrentTemperature;
}

注意:OnTemperatureChange?.Invoke(value);

空条件操作符的优点在于,它采用特殊逻辑防范在执行空检查后订阅者调用一个过时处理程序(空检查后有变)导致委托再度为空

在C#6.0之前不存在这种特殊的、不会被干扰的空检查逻辑。老版本实现稍微麻烦一点。

    public float CurrentTemperature
    {
        get { return _CurrentTemperature; }
        set
        {
            if(value != CurrentTemperature)
            {
                _CurrentTemperature = value;
                
                Action<float>? localOnChange=OnTemperatureChange;
                if(localOnChange!=null)
                {
                    localOnChange(value);
                }
            }
        }
    }

不是一上来就检查空值,而是先将OnTemperatureChange赋给第二个委托局部变量localOnChange。这个简单的修改可确保在检查空值和发送通知之间,如一个不同的线程移除了所有OnTemperatureChange订阅者,将不会引发NullReferenceException异常。

既然委托是引用类型,肯定有人会感到疑惑:为什么赋值给一个局部变量,再用那个局部变量就能保证null检查的线程安全性?因为localOnChange指向的位置就是OnTemperatureChange指向的位置,所以很自然的结论是:OnTemperatureChange中发生的任何变化都将在localOnChange中反映。

但实情并非如此。事实上,对OnTemperatureChange-=< subscriber >的任何调用都不会从OnTemperatureChange删除一个委托,而使它包含的委托比之前少一个。相反,该调用会赋值一个全新的多播委托,原始委托不受任何影响。

虽然这样可以防范调用空委托,但不能防范所有可能的竞态条件。例如一个线程拷贝委托,另一个将委托重置为null,然后原始线程调用委托之前的值,向一个已经不在列表中的订阅者发生通知。

14.1.6委托操作符

使用赋值操作符会清除之前的所有订阅者,并允许用新订阅者替换。这是委托很容易让人犯错的地方,因为在本来应该使用“+=”操作符的时候,很容易会错误地写成“=”。

无论“+”“-”还是它们的复合版本,内部都使用静态方法System.Delegate.Combine()和System.Delegate.Remove()来分别实现。

14.1.7顺序调用

MulticastDelegate类事实上维护者一个Delegate对象链表。调用多播委托时,链表中的委托实例被顺序调用。通常,委托按它们添加的顺序调用,但CLI并未对此做出规定,而且顺序可能被覆盖所以程序员不应依赖特定调用顺序

14.1.8错误处理

一个订阅者抛出异常,链中的后续订阅者就收不到通知。

为避免该问题,必须手动遍历订阅者列表,并单独调用它们。可以从委托的GetInvocationList()方法获取一份订阅者列表。

14.1.9方法返回值和传引用

还有一种情况需要遍历委托调用列表而非直接调用一个委托。这种情况的委托要么不返回void,要么具有ref或out参数。

14.2理解事件

14.2.1事件的作用

使用事件的好处是,只有直接持有一个事件对象的类可以调用这个事件对象,其他的类只能使用+=或-=向这个事件对象添加或删除对事件的订阅。(我试了下,自己类是可以用=覆盖的)

event关键字的作用就是提供额外的封装。

所以事件与委托的区别就是:1.只有直接持有一个事件对象的类可以调用这个事件对象。2.其他的类只能使用+=或-=向这个事件对象添加或删除对事件的订阅。

14.2.2声明事件

C#用event关键字声明事件,虽然看起来像是一个字段修饰符,但event定义了新的成员类型。

    public class TemperatureArgs : System.EventArgs
    {
        public TemperatureArgs(float newTemperature)
        {
            NewTemperature = newTemperature;
        }
 
        public float NewTemperature { get; set; }
    }
 
    // Define the event publisher
    public event EventHandler<TemperatureArgs> OnTemperatureChange = delegate { };

普通委托另一个潜在缺陷在于很容易忘记在调用委托之前检查null值。幸好,在声明事件时可以赋值一个空白委托delegate { },就可引发事件而不必检查是否有任何订阅者。

14.2.3编码规范
14.2.4泛型和委托

事件的内部机制:C#编译器获取带有event关键字修饰符的public委托变量,在内部将委托声明为private,并添加了两个方法和两个特殊的事件块。简单地说,event关键字是编译器生成适合封装逻辑的C#快捷方式。

public class Thermostat
// ...
    public event EventHandler<TemperatureArgs>? OnTemperatureChange;
}

C#编译器遇到event关键字后生成的CIL代码等价于下面代码

public class Thermostat
// ...
    // Declaring the delegate field to save the 
    // list of subscribers
    private EventHandler<TemperatureArgs>? _OnTemperatureChange;
 
    public void add_OnTemperatureChange(
        EventHandler<TemperatureArgs> handler)
    {
        System.Delegate.Combine(_OnTemperatureChange, handler);
    }
 
    public void remove_OnTemperatureChange(
        EventHandler<TemperatureArgs> handler)
    {
        System.Delegate.Remove(_OnTemperatureChange, handler);
    }
 
    #if ConceptualEquivalentCode
    public event EventHandler<TemperatureArgs> OnTemperatureChange
    {
        //Would cause a compiler error
        add
        {
            add_OnTemperatureChange(value);
        }
        //Would cause a compiler error
        remove
        {
            remove_OnTemperatureChange(value);
        }
    } 
14.2.5实现自定义事件

C#允许添加自定义的add和remove块。

public class Thermostat
{
    public class TemperatureArgs : System.EventArgs
    // ...
    // Define the event publisher
    public event EventHandler<TemperatureArgs> OnTemperatureChange
    {
        add
        {
            _OnTemperatureChange = 
                (EventHandler<TemperatureArgs>)
                    System.Delegate.Combine(value, _OnTemperatureChange);
        }
        remove
        {
            _OnTemperatureChange = 
                (EventHandler<TemperatureArgs>?)
                    System.Delegate.Remove(_OnTemperatureChange, value);
        }
    }
    protected EventHandler<TemperatureArgs>? _OnTemperatureChange;
 
    public float CurrentTemperature
    // ...
    private float _CurrentTemperature;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值