• 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");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    14.1.2定义发布者
    public class Thermostat
    {
        // Define the event publisher (initially without the sender)
        public Action? OnTemperatureChange { get; set; }
     
        public float CurrentTemperature { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    14.1.5检查空值
    public class Thermostat
    {
        // Define the event publisher
        public Action? 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    注意:OnTemperatureChange?.Invoke(value);

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

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

        public float CurrentTemperature
        {
            get { return _CurrentTemperature; }
            set
            {
                if(value != CurrentTemperature)
                {
                    _CurrentTemperature = value;
                    
                    Action? localOnChange=OnTemperatureChange;
                    if(localOnChange!=null)
                    {
                        localOnChange(value);
                    }
                }
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    不是一上来就检查空值,而是先将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 OnTemperatureChange = delegate { };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    14.2.3编码规范
    14.2.4泛型和委托

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

    public class Thermostat
    // ...
        public event EventHandler? OnTemperatureChange;
    }
    
    • 1
    • 2
    • 3
    • 4

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

    public class Thermostat
    // ...
        // Declaring the delegate field to save the 
        // list of subscribers
        private EventHandler? _OnTemperatureChange;
     
        public void add_OnTemperatureChange(
            EventHandler handler)
        {
            System.Delegate.Combine(_OnTemperatureChange, handler);
        }
     
        public void remove_OnTemperatureChange(
            EventHandler handler)
        {
            System.Delegate.Remove(_OnTemperatureChange, handler);
        }
     
        #if ConceptualEquivalentCode
        public event EventHandler OnTemperatureChange
        {
            //Would cause a compiler error
            add
            {
                add_OnTemperatureChange(value);
            }
            //Would cause a compiler error
            remove
            {
                remove_OnTemperatureChange(value);
            }
        } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    14.2.5实现自定义事件

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

    public class Thermostat
    {
        public class TemperatureArgs : System.EventArgs
        // ...
        // Define the event publisher
        public event EventHandler OnTemperatureChange
        {
            add
            {
                _OnTemperatureChange = 
                    (EventHandler)
                        System.Delegate.Combine(value, _OnTemperatureChange);
            }
            remove
            {
                _OnTemperatureChange = 
                    (EventHandler?)
                        System.Delegate.Remove(_OnTemperatureChange, value);
            }
        }
        protected EventHandler? _OnTemperatureChange;
     
        public float CurrentTemperature
        // ...
        private float _CurrentTemperature;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
  • 相关阅读:
    drools的简单入门案例
    vue3 + element plus实现侧边栏
    Selinux
    C++知识精讲15 | 三类基于贪心思想的区间覆盖问题【配套资源详解】
    用loadrunner监视远程服务器的方法
    C++ Reference: Standard C++ Library reference: C Library: cwchar: wscanf
    【遗传算法】求解TSP问题
    报错解决:RuntimeError: expected scalar type Long but found Float
    【中间件篇-Redis缓存数据库03】Redis高级特性和应用(发布 订阅、Stream)
    基于SSM的宿舍公共财产管理系统设计与实现
  • 原文地址:https://blog.csdn.net/Story_1419/article/details/134499853