554 字
3 分钟
鸭子类型与 await
鸭子类型与 await
最近在逛 Reddit 时,看到一个有趣的帖子:
What is the lowest effort, highest impact helper method you’ve ever written?
大家在讨论开发中常用的扩展方法,其中有一个非常亮眼的扩展如下:
public static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tasks) => WhenAllResult(tasks).GetAwaiter();
public static async Task<(T1, T2)> WhenAllResult<T1, T2>(this (Task<T1>, Task<T2>) tasks){ await Task.WhenAll(tasks.Item1, tasks.Item2).ConfigureAwait(false); return (tasks.Item1.Result, tasks.Item2.Result);}
var (result1, result2) = await ( GetDataAsync(), GetOtherDataAsync());
这样就可以通过元组优雅地获取并发结果。那么问题来了,这个是怎么实现的呢?
原理就在于 await
,它实际上采用了“鸭子类型”模型,所以我们才能 await 一个元组。那么什么是鸭子类型?
鸭子类型的定义是:只要一个对象“看起来像鸭子、叫起来像鸭子”,就可以当作鸭子用。也就是说,不要求类型继承某个接口或基类,只要有需要的方法和属性即可。
上述代码之所以能正常运行,是因为我们实现了 GetAwaiter
,并且返回的 Awaiter 拥有 IsCompleted
、GetResult()
和 OnCompleted()
方法。在 C# 中,Awaiter 还必须实现 INotifyCompletion
,否则编译器会报错。这也是鸭子类型的体现,只要有 GetAwaiter
就能被 await。
下面我们写一个简单的 demo 试试:
public class DemoAwaiter<TResult> : System.Runtime.CompilerServices.INotifyCompletion{ private TResult _num; public DemoAwaiter(TResult num) => _num = num; public bool IsCompleted => true; public TResult GetResult() => _num; public void OnCompleted(Action continuation) { Console.WriteLine("Continuation registered."); continuation?.Invoke(); }}
// 自定义 Awaitablepublic class DemoAwaitable<TResult>{ private TResult _num; public DemoAwaitable(TResult num) => _num = num; public DemoAwaiter<TResult> GetAwaiter() => new DemoAwaiter<TResult>(_num);}public class Program{ public static async Task Main(string[] args) { var result = await new DemoAwaitable<int>(123); Console.WriteLine($"Result: {result}"); }}
运行结果:
dotnet run:Restore complete (0.3s)You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy demo1 succeeded (1.4s) → bin/Debug/net10.0/demo1.dllBuild succeeded in 1.9sResult: 123
上面介绍的是 await 的鸭子类型体现,接下来我们再看一个纯粹的鸭子类型示例:
class FakeEnumerable{ public FakeEnumerator GetEnumerator() => new FakeEnumerator();}
class FakeEnumerator{ private int _current = 0; public bool MoveNext() => ++_current <= 3; public int Current => _current;}
class Program{ static void Main() { foreach (var item in new FakeEnumerable()) { Console.WriteLine(item); // 输出 1, 2, 3 } }}
FakeEnumerable
并没有实现 IEnumerable
接口,只要有 GetEnumerator()
方法,且返回的类型有 MoveNext()
和 Current
,就能被 foreach
使用。