554 字
3 分钟
鸭子类型与 await
2024-08-12

鸭子类型与 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 拥有 IsCompletedGetResult()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

上面介绍的是 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 使用。

鸭子类型与 await
https://www.tyblog.site/posts/csharp/duck-typingandawait/
作者
37°C
发布于
2024-08-12
许可协议
CC BY-NC-SA 4.0