前言
有一個東西叫做鴨子類型,所謂鴨子類型就是,只要一個東西表現(xiàn)得像鴨子那么就能推出這玩意就是鴨子。
C# 里面其實(shí)也暗藏了很多類似鴨子類型的東西,但是很多開發(fā)者并不知道,因此也就沒法好好利用這些東西,那么今天我細(xì)數(shù)一下這些藏在編譯器中的細(xì)節(jié)。
不是只有 Task 和 ValueTask 才能 await
在 C# 中編寫異步代碼的時候,我們經(jīng)常會選擇將異步代碼包含在一個 Task 或者 ValueTask 中,這樣調(diào)用者就能用 await 的方式實(shí)現(xiàn)異步調(diào)用。
西卡西,并不是只有 Task 和 ValueTask 才能 await。Task 和 ValueTask 背后明明是由線程池參與調(diào)度的,可是為什么 C# 的
async/await 卻被說成是 coroutine 呢?
因?yàn)槟闼?await 的東西不一定是 Task/ValueTask,在 C# 中只要你的類中包含 GetAwaiter() 方法和 bool
IsCompleted 屬性,并且 GetAwaiter() 返回的東西包含一個 GetResult() 方法、一個 bool IsCompleted
屬性和實(shí)現(xiàn)了INotifyCompletion,那么這個類的對象就是可以 await 的 。
因此在封裝 I/O 操作的時候,我們可以自行實(shí)現(xiàn)一個 Awaiter,它基于底層的 epoll/IOCP 實(shí)現(xiàn),這樣當(dāng) await
的時候就不會創(chuàng)建出任何的線程,也不會出現(xiàn)任何的線程調(diào)度,而是直接讓出控制權(quán)。而 OS 在完成 I/O 調(diào)用后通過CompletionPort
(Windows) 等通知用戶態(tài)完成異步調(diào)用,此時恢復(fù)上下文繼續(xù)執(zhí)行剩余邏輯,這其實(shí)就是一個真正的stackless coroutine。
public class MyTask<T> { public MyAwaiter<T> GetAwaiter() { return new
MyAwaiter<T>(); } } public class MyAwaiter<T> : INotifyCompletion { public bool
IsCompleted { get; private set; } public T GetResult() { throw new
NotImplementedException(); } public void OnCompleted(Action continuation) {
throw new NotImplementedException(); } } public class Program { static async
Task Main(string[] args) { var obj = new MyTask<int>(); await obj; } }
事實(shí)上,.NET Core 中的 I/O 相關(guān)的異步 API 也的確是這么做的,I/O 操作過程中是不會有任何線程分配等待結(jié)果的,都是 coroutine
操作:I/O 操作開始后直接讓出控制權(quán),直到 I/O 操作完畢。而之所以有的時候你發(fā)現(xiàn)await 前后線程變了,那只是因?yàn)?Task 本身被調(diào)度了。
UWP 開發(fā)中所用的 IAsyncAction/IAsyncOperation<T> 則是來自底層的封裝,和 Task 沒有任何關(guān)系但是是可以 await
的,并且如果用 C++/WinRT 開發(fā) UWP 的話,返回這些接口的方法也都是可以co_await 的。
不是只有 IEnumerable 和 IEnumerator 才能被 foreach
經(jīng)常我們會寫如下的代碼:
foreach (var i in list) { // ...... }
然后一問為什么可以 foreach,大多都會回復(fù)因?yàn)檫@個 list 實(shí)現(xiàn)了 IEnumerable 或者 IEnumerator。
但是實(shí)際上,如果想要一個對象可被 foreach,只需要提供一個 GetEnumerator() 方法,并且 GetEnumerator()
返回的對象包含一個bool MoveNext() 方法加一個 Current 屬性即可。
class MyEnumerator<T> { public T Current { get; private set; } public bool
MoveNext() { throw new NotImplementedException(); } } class MyEnumerable<T> {
public MyEnumerator<T> GetEnumerator() { throw new NotImplementedException(); }
} class Program { public static void Main() { var x = new MyEnumerable<int>();
foreach (var i in x) { // ...... } } }
不是只有 IAsyncEnumerable 和 IAsyncEnumerator 才能被 await foreach
同上,但是這一次要求變了,GetEnumerator() 和 MoveNext() 變?yōu)?GetAsyncEnumerator() 和
MoveNextAsync()。
其中 MoveNextAsync() 返回的東西應(yīng)該是一個 Awaitable<bool>,至于這個 Awaitable 到底是什么,它可以是 Task/
ValueTask,也可以是其他的或者你自己實(shí)現(xiàn)的。
class MyAsyncEnumerator<T> { public T Current { get; private set; } public
MyTask<bool> MoveNextAsync() { throw new NotImplementedException(); } } class
MyAsyncEnumerable<T> { public MyAsyncEnumerator<T> GetAsyncEnumerator() { throw
new NotImplementedException(); } } class Program { public static async Task
Main() { var x = new MyAsyncEnumerable<int>(); await foreach (var i in x) { //
...... } } }
ref struct 要怎么實(shí)現(xiàn) IDisposable
眾所周知 ref struct 因?yàn)楸仨氃跅I锨也荒鼙谎b箱,所以不能實(shí)現(xiàn)接口,但是如果你的 ref struct 中有一個 void Dispose()
那么就可以用using 語法實(shí)現(xiàn)對象的自動銷毀。
ref struct MyDisposable { public void Dispose() => throw new
NotImplementedException(); } class Program { public static void Main() { using
var y = new MyDisposable(); // ...... } }
不是只有 Range 才能使用切片
C# 8 引入了 Ranges,允許切片操作,但是其實(shí)并不是必須提供一個接收 Range 類型參數(shù)的 indexer 才能使用該特性。
只要你的類可以被計數(shù)(擁有 Length 或 Count 屬性),并且可以被切片(擁有一個 Slice(int, int) 方法),那么就可以用該特性。
class MyRange { public int Count { get; private set; } public object Slice(int
x, int y) => throw new NotImplementedException(); } class Program { public
static void Main() { var x = new MyRange(); var y = x[1..]; } }
不是只有 Index 才能使用索引
C# 8 引入了 Indexes 用于索引,例如使用 ^1 索引倒數(shù)第一個元素,但是其實(shí)并不是必須提供一個接收 Index 類型參數(shù)的 indexer
才能使用該特性。
只要你的類可以被計數(shù)(擁有 Length 或 Count 屬性),并且可以被索引(擁有一個接收 int 參數(shù)的索引器),那么就可以用該特性。
class MyIndex { public int Count { get; private set; } public object this[int
index] { get => throw new NotImplementedException(); } } class Program { public
static void Main() { var x = new MyIndex(); var y = x[^1]; } }
給類型實(shí)現(xiàn)解構(gòu)
如何給一個類型實(shí)現(xiàn)解構(gòu)呢?其實(shí)只需要寫一個名字為 Deconstruct() 的方法,并且參數(shù)都是 out 的即可。
class MyDeconstruct { private int A => 1; private int B => 2; public void
Deconstruct(out int a, out int b) { a = A; b = B; } } class Program { public
static void Main() { var x = new MyDeconstruct(); var (o, u) = x; } }
熱門工具 換一換