前言
最近在逛 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();
}
}
// 自定义 Awaitable
public 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.dll
Build succeeded in 1.9s
Result: 123
纯鸭子类型示例
public class DuckType
{
public DuckEnumerator GetEnumerator()
{
return new DuckEnumerator();
}
public class DuckEnumerator
{
private int _index = -1;
private static readonly string[] _items = { "A", "B", "C" };
public object Current => _items[_index];
public bool MoveNext()
{
_index++;
return _index < _items.Length;
}
}
}
class Program
{
static void Main()
{
foreach (var item in new DuckType())
{
Console.WriteLine(item); // 输出 A, B, C
}
}
}
可以看到,FakeEnumerable 并没有实现 IEnumerable 接口,只要有 GetEnumerator() 方法,就能被 foreach 使用。