• C# 委托与事件的关系-上(委托)


    参考资料:
    《C# 7.0本质论》14
    《C# 7.0核心技术指南》4.2

    一个发布订阅模式的例子
    定义订阅者
    定义发布者
    连接订阅者和发布者
    调用委托
    检查空值
    存在的其他问题

    事件与委托在C#的大部分书籍中都是放在一起讲的,要理解事件,首先要理解委托。本篇是从委托到事件的过度。

    委托是Publish-Subscribe(发布——订阅)或者Observer(观察者)模式的基本单位。该模式可以只通过委托实现,但事件提供额外的封装,使该模式更容易实现且更不容易出错。

    当使用委托时,一般会有广播者(broadcaster)和订阅者(subscriber)两种角色。广播者是包含委托字段的类型,它通过调用委托决定何时进行广播。而订阅者是方法的目标接收者(不太容易理解)。订阅者通过在广播者的委托上调用+=和-=来决定何时开始监听而何时监听结束。订阅者不知道也不会干涉其他的订阅者。

    我在介绍委托的博客中写的“多播委托”概念对理解事件是重要的,单一事件(比如对象状态的改变)的通知可以使用多播委托发布给多个订阅者。

    使用事件的主要目的在于保证订阅者之间不互相影响。

    一个发布订阅模式的例子

    本例子来自《C# 7.0本质论》14.1。

    来考虑一个温度控制的例子。一个加热器(Heater)和一个冷却器(Cooler)连接到同一个恒温器(Thermostat)。控制设备开关需要向它们通知温度变化。恒温器将温度变化发布给多个订阅者——也就是加热器和冷却器。

    定义订阅者

    定义Heater和Cooler对象。

    class Cooler
    {
        public Cooler(float temperature)
        {
            Temperature = temperature;
        }
    
        // 启动冷却器所需的温度
        public float Temperature { get; }
    
        public void OnTemperatureChanged(float newTemperature)
        {
            // 一旦新温度大于启动冷却器所需的温度,就启动冷却器,否则关闭冷却器
            if (newTemperature > Temperature)
            {
                Console.WriteLine("Cooler: On");
            }
            else
            {
                Console.WriteLine("Cooler: Off");
            }
        }
    }
    
    class Heater
    {
        public Heater(float temperature)
        {
            Temperature = temperature;
        }
    
        // 启动加热器所需的温度
        public float Temperature { get; }
    
        public void OnTemperatureChanged(float newTemperature)
        {
            // 一旦新温度小于启动加热器所需的温度,就启动加热器,否则关闭加热器
            if (newTemperature < Temperature)
            {
                Console.WriteLine("Heater: On");
            }
            else
            {
                Console.WriteLine("Heater: Off");
            }
        }
    }
    

    Cooler和Heater中的Temperature分别是启动冷却器和启动加热器的温度临界点。一旦新温度大于启动冷却器所需的温度,就启动冷却器,否则关闭冷却器。一旦新温度小于启动加热器所需的温度,就启动加热器,否则关闭加热器。

    调用OnTemperatureChanged()方法的目的是向Heater和Cooler类指出温度已发生改变。在方法的实现中,用newTemperature同存储好的触发温度进行比较,从而决定是否让设备启动。两个OnTemperatureChanged()方法都是订阅者(或侦听者)方法,其参数和返回类型必须与来自我们即将定义的Thermostat类的委托匹配。

    定义发布者

    定义一个Thermostat类负责向heater和cooler对象实例报告温度变化。

    public class Thermostat
    {
        public Action<float> OnTemperatureChanged { get; set; }
        public float CurrentTemperature { get; set; }
    }
    

    OnTemperatureChanged是一个Action<float>类型的属性,它存储了订阅者列表。一个委托字段即可存储所有订阅者。CurrentTemperature负责设置和获取Thermostat类报告的当前温度值。

    连接订阅者和发布者

    class Program
    {
        public static void Main()
        {
            Thermostat thermostat = new Thermostat();
            Heater heater = new Heater(60); // 设定Heater的触发温度为60
            Cooler cooler = new Cooler(80); // 设定Cooler的触发温度为80
            string temperature;
    
            thermostat.OnTemperatureChanged += heater.OnTemperatureChanged; // 向thermostat.OnTemperatureChanged注册订阅者heater.OnTemperatureChanged
            thermostat.OnTemperatureChanged += cooler.OnTemperatureChanged; // 向thermostat.OnTemperatureChanged注册订阅者cooler.OnTemperatureChanged
    
            Console.Write("Enter temperature: ");
            temperature = Console.ReadLine();
    
            // 从控制台接收到新CurrentTemperature后,赋值给发布者thermostat的CurrentTemperature属性
            thermostat.CurrentTemperature = int.Parse(temperature); 
        }
    }
    

    调用委托

    目前还无法从发布者那里把温度变化发布给订阅者。我们期望Thermostat类的CurrentTemperature属性每次发生变化,都调用委托向订阅者通知温度的变化。需要修改CurrentTemperature属性来保存新值,并向每个订阅者发出通知:

    public class Thermostat
    {
        public Action<float> OnTemperatureChanged { get; set; }
    
        // 新增私有_CurrentTempetature字段
        private float _CurrentTempetature;
    
        public float CurrentTemperature
        {
            get => _CurrentTempetature;
            set
            {
                if (value != CurrentTemperature)
                {
                    _CurrentTempetature = value;
    
                    // 通知订阅者
                    OnTemperatureChanged(value);
                }
            }
        }
    }
    

    修改之后,Thermostat对象的CurrentTemperature属性接收到新值,就执行set代码块。如果新值不等于CurrentTemperature,就更新_CurrentTempetature字段,并且调用Thermostat对象的OnTemperatureChanged委托,把新值传给添加到该委托对象的目标方法们(订阅者们)。现在这个发布者-订阅者模型就勉强可以用了:

    可以看到,我们使用多播委托,执行了一个调用,向多个订阅者发布了通知。Amazing!

    检查空值

    可以看到《C# 7.0本质论》的作者是高水平、用心且认真的,一个简单的demo也做到面面俱到,提醒我们不忘检查订阅者是否为空。

    假如当前没有订阅者注册接收通知,则OnTemperatureChange为null,执行OnTemperatureChange(value)语句会抛出NullReferenceException异常。因此需在触发事件之前检查空值。

    public float CurrentTemperature
    {
        get => _CurrentTempetature;
        set
        {
            if (value != CurrentTemperature)
            {
                _CurrentTempetature = value;
    
                // 通知订阅者
                OnTemperatureChanged?.Invoke(value);
            }
        }
    }
    

    使用?.空条件操作符。它采用特殊逻辑防范在执行空检查后,订阅者调用一个过时的处理程序(空检查后有新变化)造成委托再度为空。

    注意,OnTemperatureChanged(value)等价于OnTemperatureChanged.Invoke(value)。

    虽然一个OnTemperatureChange()调用造成每个订阅者都收到通知,但它们仍然是顺序调用的,而不是同时,因为它们全都在一个执行线程上调用。通常,委托按它们添加的顺序调用,但CLI规范并未对此做出规定,而且该顺序可能被覆盖,所以程序员不应依赖特定调用顺序。

    存在的其他问题

    顺序通知存在一些潜在的问题。一个订阅者引发异常,链中的后续订阅者就收不到通知。可能需要用try-catch进行一些复杂的处理。

    还有一种情形需要遍历委托调用列表而非直接激活一个通知。这种情形涉及的委托要么返回非void类型,要么具有ref或out参数。调用委托可能将一个通知发送给多个订阅者。如每个订阅者都返回值,就无法确定应该使用哪个订阅者的返回值。这样就需要用Func委托。由于所有订阅者方法都要使用和委托一样的方法签名,所以都必须返回同类型值。可能还要遍历所有的订阅者的返回值。类似地,使用ref和out参数的委托类型也需特别对待。虽然极少数情况下需采取这样的做法,但一般原则是通过只返回void来彻底避免。

  • 相关阅读:
    JavaScript 立即执行函数和闭包
    Visual Studio2017 离线安装
    D19 Sping Boot 入门 Sping框架--Java Web之书城项目(九) AJAX
    D18 Sping Boot 入门 Sping框架--Java Web之书城项目(八) 过滤器
    D17 Sping Boot 入门 Sping框架--Java Web之Filter过滤器
    在Django中写mqtt脚本并使用
    Django对models层数据库的增删改查操作
    前端调用mqtt不能使用1883端口的问题
    vue结合mqtt
    mqtt服务器的安装(2)--mosquitto
  • 原文地址:https://www.cnblogs.com/Kit-L/p/13872428.html
Copyright © 2020-2023  润新知