你所不知道的 C# 中的细节
前言
有一个东西叫做鸭子类型,所谓鸭子类型就是,只要一个东西表现得像鸭子那么就能推出这玩意就是鸭子。
c# 里面其实也暗藏了很多类似鸭子类型的东西,但是很多开发者并不知道,因此也就没法好好利用这些东西,那么今天我细数一下这些藏在编译器中的细节。
不是只有 task
和 valuetask
才能 await
在 c# 中编写异步代码的时候,我们经常会选择将异步代码包含在一个 task
或者 valuetask
中,这样调用者就能用 await
的方式实现异步调用。
西卡西,并不是只有 task
和 valuetask
才能 await
。task
和 valuetask
背后明明是由线程池参与调度的,可是为什么 c# 的 async
/await
却被说成是 coroutine
呢?
因为你所 await
的东西不一定是 task
/valuetask
,在 c# 中只要你的类中包含 getawaiter()
方法和 bool iscompleted
属性,并且 getawaiter()
返回的东西包含一个 getresult()
方法、一个 bool iscompleted
属性和实现了 inotifycompletion
,那么这个类的对象就是可以 await
的 。
因此在封装 i/o 操作的时候,我们可以自行实现一个 awaiter
,它基于底层的 epoll
/iocp
实现,这样当 await
的时候就不会创建出任何的线程,也不会出现任何的线程调度,而是直接让出控制权。而 os 在完成 i/o 调用后通过 completionport
(windows) 等通知用户态完成异步调用,此时恢复上下文继续执行剩余逻辑,这其实就是一个真正的 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; } }
事实上,.net core 中的 i/o 相关的异步 api 也的确是这么做的,i/o 操作过程中是不会有任何线程分配等待结果的,都是 coroutine
操作:i/o 操作开始后直接让出控制权,直到 i/o 操作完毕。而之所以有的时候你发现 await
前后线程变了,那只是因为 task
本身被调度了。
uwp 开发中所用的 iasyncaction
/iasyncoperation<t>
则是来自底层的封装,和 task
没有任何关系但是是可以 await
的,并且如果用 c++/winrt 开发 uwp 的话,返回这些接口的方法也都是可以 co_await
的。
不是只有 ienumerable
和 ienumerator
才能被 foreach
经常我们会写如下的代码:
foreach (var i in list) { // ...... }
然后一问为什么可以 foreach
,大多都会回复因为这个 list
实现了 ienumerable
或者 ienumerator
。
但是实际上,如果想要一个对象可被 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()
变为 getasyncenumerator()
和 movenextasync()
。
其中 movenextasync()
返回的东西应该是一个 awaitable<bool>
,至于这个 awaitable
到底是什么,它可以是 task
/valuetask
,也可以是其他的或者你自己实现的。
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
要怎么实现 idisposable
众所周知 ref struct
因为必须在栈上且不能被装箱,所以不能实现接口,但是如果你的 ref struct
中有一个 void dispose()
那么就可以用 using
语法实现对象的自动销毁。
ref struct mydisposable { public void dispose() => throw new notimplementedexception(); } class program { public static void main() { using var y = new mydisposable(); // ...... } }
不是只有 range
才能使用切片
c# 8 引入了 ranges,允许切片操作,但是其实并不是必须提供一个接收 range
类型参数的 indexer 才能使用该特性。
只要你的类可以被计数(拥有 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
索引倒数第一个元素,但是其实并不是必须提供一个接收 index
类型参数的 indexer 才能使用该特性。
只要你的类可以被计数(拥有 length
或 count
属性),并且可以被索引(拥有一个接收 int
参数的索引器),那么就可以用该特性。
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]; } }
给类型实现解构
如何给一个类型实现解构呢?其实只需要写一个名字为 deconstruct()
的方法,并且参数都是 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; } }