重学c#系列——订阅发布与事件[二十六]

时间:2022-11-25 15:06:23

前言

简单介绍一下订阅发布与事件。

正文

先来看一下委托的订阅与发布。

public delegate void TestDelegate();

public class Cat
{
	public TestDelegate testDelegate;

	public void call()
	{
		testDelegate?.Invoke();
	}
}
 public class BlackMouse
{
	public void listen()
	{
		
	}
}
public class WhiteMouse
{
	public void listen()
	{

	}
}

代码还是经典的猫和老鼠。

然后运行:

static void Main(string[] args)
{
	WhiteMouse whiteMouse = new WhiteMouse();
	BlackMouse blackMouse = new BlackMouse();
	Cat cat = new Cat();
	cat.testDelegate += whiteMouse.listen;
	cat.testDelegate += blackMouse.listen;
	cat.call();
}

当猫调用call的时候,那么白老鼠和黑老鼠就会听到。

这种就是发布订阅模式了,通过委托多播实现的。

值得注意的是委托多播是按照顺序执行的,比如whiteMouse.listen 中抛出异常,那么blackMouse.listen是不会进行执行的。

这是委托实现发布订阅的一个特点。那么有没有什么办法解决呢?有的。

在c# 中,通过委托实现事件。

public class Cat
{
	public TestDelegate testDelegate;

	public void call()
	{
	   var methods =  testDelegate?.GetInvocationList();
		if (methods == null)
		{
			return;
		}
		foreach (var m in methods)
		{
			((TestDelegate)m)();
		}
	}
}

手动执行委托列表,这样可以根据自己的业务来执行。

static void Main(string[] args)
{
	WhiteMouse whiteMouse = new WhiteMouse();
	BlackMouse blackMouse = new BlackMouse();
	Cat cat = new Cat();
	cat.TestEvent += whiteMouse.listen;
	cat.TestEvent += whiteMouse.listen;
	cat.call();
}

那么什么是事件呢? 事件是当做出一系列操作的时候能做根据这些操作做出另外一些列操作,类似发布订阅模式,这是事件的概念。

那么c# 怎么来使用事件呢?

重学c#系列——订阅发布与事件[二十六]

就是委托封装了一层。

那么这样做有什么用处呢?

重学c#系列——订阅发布与事件[二十六]

这样调用和以前没什么区别啊。

区别在于:

事件只能用于+=和-=,不能用于=号。

重学c#系列——订阅发布与事件[二十六]

这样更加符合事件模型,不让其他地方直接进行修改操控。

同样不能直接调动,只能cat 内部调用。

重学c#系列——订阅发布与事件[二十六]

重学c#系列——订阅发布与事件[二十六]

然后事件使用规范是下面这种。

重学c#系列——订阅发布与事件[二十六]

就是有一个EventHandler这样的委托。

重学c#系列——订阅发布与事件[二十六]

里面指明了要传递事件是谁触发的,然后参数要继承EventArgs,EventArgs 没什么特别的,就是有一个概念里面有一个空的选项。

重学c#系列——订阅发布与事件[二十六]

表示没有传递任何参数,之所以有EventArgs 是为了抽象,统一模型。

订阅的也要这样写。

重学c#系列——订阅发布与事件[二十六]

第一个object 是来源,第二个是参数。

如果想自定义参数的话,就是下面这样写:

重学c#系列——订阅发布与事件[二十六]

这样满足我们的大部分需求了,如果有些需要特殊需求的,可以根据自己来定制,看自己的设计了。

然后来看下event 原理。

重学c#系列——订阅发布与事件[二十六]

里面就是对委托的封装。

看第一个框,是把委托定位私有,那么外部就无法直接访问了。

然后生成了两个公共方法add_TestEvent 和 remove_TestEvent来添加订阅。

最后一个框,发现il语句中有event这个字眼,说明程序运行时候真的识别了event,所以event不仅仅是语法糖。

当il中调用的时候的确是调用了add_TestEvent。

重学c#系列——订阅发布与事件[二十六]

可以理解为事件是对委托的封装,实现了一些操作触发了另外一些操作。

因为事件是对委托的封装,那么其实官方也允许我们自己来定义事件。

public class Cat
{
	private EventHandler<TestArgs> _eventHandler;

	public event EventHandler<TestArgs> TestEvent
	{
		add {
			_eventHandler = (EventHandler<TestArgs>)Delegate.Combine(value, _eventHandler);
		}
		remove
		{
			_eventHandler = (EventHandler<TestArgs>)Delegate.Remove(_eventHandler,value);
		}
	}

	public void call()
	{
		TestArgs testArgs = new TestArgs("Tom");
		_eventHandler?.Invoke(this, testArgs);
	}
}

事件虽然上面用发布订阅来描述,其实是不准的,发布订阅只是事件的一种模型。

同样因为事件是基于委托实现的,所以依然有那个问题,如果有一个执行有问题,剩下的将不会执行。

重学c#系列——订阅发布与事件[二十六]
重学c#系列——订阅发布与事件[二十六]
重学c#系列——订阅发布与事件[二十六]

执行结果:

重学c#系列——订阅发布与事件[二十六]

如果不符合这种设计模型,还是自己根据业务需求去编写自己的执行代码,上文展示了委托的,事件的是一样的。

重学c#系列——订阅发布与事件[二十六]

然后关于委托是不是链式执行的,有或者更具+=的顺序来执行的,官方并没有说明。

可以简单做下实验:

static void Main(string[] args)
{
	TestDelegate a = null;
	List<int> test = new List<int>();
	var arr = Enumerable.Range(1, 10000).ToArray();
	foreach (var i in arr)
	{
		var c = i;
		a += () => test.Add(c);
	}
	a();
	var newArr = test.ToArray();
	var flag = true;
	for (var i=0; i< arr.Length;i++)
	{
		if (i+1 != newArr[i])
		{
			Console.WriteLine("执行顺序不一致");
			flag = false;
		}
	}
	Console.WriteLine($"{flag}");
	Console.ReadLine();
}

运行多次后,依然是true哈,听说不同机型和net版本不一样运行就不一样,这个先不做判断。

我们来试一下异步。

public delegate Task TestDelegate();

static void Main(string[] args)
{
	TestDelegate a = null;
	List<int> test = new List<int>();
	var arr = Enumerable.Range(1, 10000).ToArray();
	foreach (var i in arr)
	{
		var c = i;
		a += async () => {
			await Task.Delay(100);
			test.Add(c);
			};
	}
	a();
	var newArr = test.ToArray();
	var flag = true;
	for (var i=0; i< arr.Length;i++)
	{
		if (i+1 != newArr[i])
		{
			Console.WriteLine("执行顺序不一致");
			flag = false;
		}
	}
	Console.WriteLine($"{flag}");
	Console.ReadLine();
}

如果是异步的话,那么内部是不会进行等待的,这个是确认的。

重学c#系列——订阅发布与事件[二十六]

另外一个有趣的例子:

public delegate void TestDelegate();

static void Main(string[] args)
{
	TestDelegate a = new TestDelegate(() =>
	{
		Console.WriteLine(1);
	});

	TestDelegate b = new TestDelegate(() =>
	{
		Console.WriteLine(2);
	});

	TestDelegate c = new TestDelegate(() =>
	{
		Console.WriteLine(3);
	});
	a += b;
	a += c;
	a -= b;
	a += b;
	a();
	Console.ReadLine();
}

执行的时候是否空出b,然后再填充b呢? 答案是不是。

重学c#系列——订阅发布与事件[二十六]

上面例子只是再我自己电脑上做的例子,只能说明如果异步是不会形成串联的。

重学c#系列——订阅发布与事件[二十六]

关于多播委托运行的顺序,其实我觉得没有那么重要,如果想设计这种串行的话,最好直接用职责链模式。

因为多播委托,概念主要是多播,没必要关注顺序,如果关注顺序,那么另一种链式模型其实更符合,这是实现业务值得思考的地方。

下一节可能是泛型也可能是linq,不确定,会尽快更新完这100多篇。