前言

最近在逛 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

纯鸭子类型示例

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 使用。