zoukankan      html  css  js  c++  java
  • 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来彻底避免。

  • 相关阅读:
    matplotlib 进阶之origin and extent in imshow
    Momentum and NAG
    matplotlib 进阶之Tight Layout guide
    matplotlib 进阶之Constrained Layout Guide
    matplotlib 进阶之Customizing Figure Layouts Using GridSpec and Other Functions
    matplotlb 进阶之Styling with cycler
    matplotlib 进阶之Legend guide
    Django Admin Cookbook-10如何启用对计算字段的过滤
    Django Admin Cookbook-9如何启用对计算字段的排序
    Django Admin Cookbook-8如何在Django admin中优化查询
  • 原文地址:https://www.cnblogs.com/Kit-L/p/13872428.html
Copyright © 2011-2022 走看看