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

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

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

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

14.1.1定义订阅者方法
c# 复制代码
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定义发布者
c# 复制代码
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连接发布者和订阅者
c# 复制代码
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调用委托
c# 复制代码
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检查空值
c# 复制代码
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之前不存在这种特殊的、不会被干扰的空检查逻辑。老版本实现稍微麻烦一点。

c# 复制代码
    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定义了新的成员类型。

c# 复制代码
    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#快捷方式。

c# 复制代码
public class Thermostat
// ...
    public event EventHandler<TemperatureArgs>? OnTemperatureChange;
}

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

c# 复制代码
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块。

c# 复制代码
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;
}
相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
EIP低代码平台5 小时前
EIP 低代码平台 - 角色维护
低代码·c#·权限·工作流·netcore
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
克里斯蒂亚诺更新6 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨8 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117769 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao9 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言