重學c#系列——訂閱釋出與事件[二十六]

2022-11-25 15:00:30

前言

簡單介紹一下訂閱釋出與事件。

正文

先來看一下委託的訂閱與釋出。

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# 怎麼來使用事件呢?

就是委託封裝了一層。

那麼這樣做有什麼用處呢?

這樣呼叫和以前沒什麼區別啊。

區別在於:

事件只能用於+=和-=,不能用於=號。

這樣更加符合事件模型,不讓其他地方直接進行修改操控。

同樣不能直接調動,只能cat 內部呼叫。

然後事件使用規範是下面這種。

就是有一個EventHandler這樣的委託。

裡面指明瞭要傳遞事件是誰觸發的,然後引數要繼承EventArgs,EventArgs 沒什麼特別的,就是有一個概念裡面有一個空的選項。

表示沒有傳遞任何引數,之所以有EventArgs 是為了抽象,統一模型。

訂閱的也要這樣寫。

第一個object 是來源,第二個是引數。

如果想自定義引數的話,就是下面這樣寫:

這樣滿足我們的大部分需求了,如果有些需要特殊需求的,可以根據自己來客製化,看自己的設計了。

然後來看下event 原理。

裡面就是對委託的封裝。

看第一個框,是把委託定位私有,那麼外部就無法直接存取了。

然後生成了兩個公共方法add_TestEvent 和 remove_TestEvent來新增訂閱。

最後一個框,發現il語句中有event這個字眼,說明程式執行時候真的識別了event,所以event不僅僅是語法糖。

當il中呼叫的時候的確是呼叫了add_TestEvent。

可以理解為事件是對委託的封裝,實現了一些操作觸發了另外一些操作。

因為事件是對委託的封裝,那麼其實官方也允許我們自己來定義事件。

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);
	}
}

事件雖然上面用釋出訂閱來描述,其實是不準的,釋出訂閱只是事件的一種模型。

同樣因為事件是基於委託實現的,所以依然有那個問題,如果有一個執行有問題,剩下的將不會執行。



執行結果:

如果不符合這種設計模型,還是自己根據業務需求去編寫自己的執行程式碼,上文展示了委託的,事件的是一樣的。

然後關於委託是不是鏈式執行的,有或者更具+=的順序來執行的,官方並沒有說明。

可以簡單做下實驗:

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();
}

如果是非同步的話,那麼內部是不會進行等待的,這個是確認的。

另外一個有趣的例子:

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呢? 答案是不是。

上面例子只是再我自己電腦上做的例子,只能說明如果非同步是不會形成串聯的。

關於多播委託執行的順序,其實我覺得沒有那麼重要,如果想設計這種序列的話,最好直接用職責鏈模式。

因為多播委託,概念主要是多播,沒必要關注順序,如果關注順序,那麼另一種鏈式模型其實更符合,這是實現業務值得思考的地方。

下一節可能是泛型也可能是linq,不確定,會盡快更新完這100多篇。