LINQ为异步方法选择类似物

时间:2019-02-24 15:18:02

标签: c# .net linq async-await

假设我有以下示例代码:

private static async Task Main(string[] args)
{
    var result = Enumerable.Range(0, 3).Select(x => TestMethod(x)).ToArray();
    Console.ReadKey();
}

private static int TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    Thread.Sleep(50);
    Console.WriteLine($"{param} after");
    return param;
}

TestMethod将运行3次,所以我将看到3对beforeafter

0 before
0 after
1 before
1 after
2 before
2 after

现在,我需要使TestMethod异步:

private static async Task<int> TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    await Task.Delay(50);
    Console.WriteLine($"{param} after");
    return param;
}

如何为该异步方法编写类似的Select表达式?如果我只使用异步lambda Enumerable.Range(0, 3).Select(async x => await TestMethod(x)).ToArray();,那将无法正常工作,因为它不会等待完成,因此before部分将首先被调用:

0 before
1 before
2 before
2 after
0 after
1 after

请注意,我不想同时运行所有3个调用-我需要它们一个接一个地执行,仅在前一个完全完成并返回值后才开始下一个。

6 个答案:

答案 0 :(得分:3)

C# 8(截至撰写本文时的下一个主要版本)中,将支持IAsyncEnumrable<T>,您可以在其中为每个循环编写异步操作:

await foreach (var item in GetItemsAsync())
    // Do something to each item in sequence

我希望会有一个Select扩展方法来进行投影,但是如果没有,那么编写自己的投影并不困难。您还可以使用IAsyncEnumrable<T>创建yield return迭代器块。

答案 1 :(得分:2)

我经常遇到此要求,并且我不知道有任何内置的解决方案可以解决C#7.2中的要求。我通常只是退回到在await内的每个异步操作上使用foreach,但是您可以使用扩展方法:

public static class EnumerableExtensions
{
    public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, Task<TResult>> asyncSelector)
    {
        var results = new List<TResult>();
        foreach (var item in source)
            results.Add(await asyncSelector(item));
        return results;
    }
}

然后您将在await上致电SelectAsync

static async Task Main(string[] args)
{
    var result = (await Enumerable.Range(0, 3).SelectAsync(x => TestMethod(x))).ToArray();
    Console.ReadKey();
}

此方法的缺点是SelectAsync渴望而不是懒惰。 C#8承诺引入async streams,这将使它再次变得懒惰。

答案 2 :(得分:1)

我想这就是OP真正想要的,因为我花了整整一天的时间来解决OP想知道的相同问题。但我希望用流利的方法完成。

通过Martin Liversage的回答,我找到了正确的方法:我想要的是C#8.0中可用的IAsyncEnumerableSystem.Linq.Async pacakge。

示例:

private static async Task Main(string[] args)
{
    var result = await Enumerable.Range(0, 3)
        .ToAsyncEnumerable()
        .SelectAwait(x => TestMethod(x))
        .ToArrayAsync();
    Console.ReadKey();
}

private static async Task<int> TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    await Task.Delay(50);
    Console.WriteLine($"{param} after");
    return param;
}

答案 3 :(得分:0)

您必须知道什么是Enumerable对象。一个Enumerable对象,不是枚举本身。它使您可以获取Enumerator对象。如果您有一个Enumerator对象,则可以要求一个序列的第一个元素,一旦获得枚举元素,就可以要求下一个。

因此,创建一个使您能够枚举序列的对象与枚举本身无关。

您的Select函数仅返回一个Enumerable对象,它不启动枚举。枚举由ToArray开始。

在最低级别,枚举按以下方式进行:

IEnumerable<TSource> myEnumerable = ...
IEnumerator<TSource> enumerator = myEnumerable.GetEnumerator();
while (enumerator.MoveNext())
{
     // there is still an element in the enumeration
     TSource currentElement = enumerator.Current;
     Process(currentElement);
}

ToArray将在内部调用GetEnumeratorMoveNext。因此,要使您的LINQ语句异步,您将需要一个ToArrayAsync

代码非常简单。我将向您展示ToListAsync作为扩展方法,该方法更容易实现。

static class EnumerableAsyncExtensions
{
    public static async Task<List<TSource>> ToListAsync<TSource>(
       this IEnumerable<Task<TSource>> source)
    {
        List<TSource> result = new List<TSource>();
        var enumerator = source.GetEnumerator()
        while (enumerator.MoveNext())
        {
            // in baby steps. Feel free to do this in one step
            Task<TSource> current = enumerator.Current;
            TSource awaitedCurrent = await current;
            result.Add(awaitedCurrent);
        }
        return result;
    } 
}

您只需创建一次即可。您可以将其用于任何需要等待每个元素的ToListAsync:

 var result = Enumerable.Range(0, 3)
     .Select(i => TestMethod(i))
     .ToListAsync();

请注意,Select的返回值为IEnumerable<Task<int>>:一个对象,它可以枚举Task<int>对象的序列。在foreach中,每个周期都会得到一个Task<int>

答案 4 :(得分:-1)

看起来目前没有其他解决方案(直到C#8.0),除了手动枚举它。我为此做了一个扩展方法(返回一个List而不是Array,因为它更简单):

public static async Task<List<T>> ToListAsync<T>(this IEnumerable<Task<T>> source)
{
    var result = new List<T>();
    foreach (var item in source)
    {
        result.Add(await item);
    }
    return result;
}

答案 5 :(得分:-2)

您可以使用同步原语

    class Program
    {
        static async Task Main(string[] args)
        {
            var waitHandle = new AutoResetEvent(true);
            var result = Enumerable
                .Range(0, 3)
                .Select(async (int param) => {
                    waitHandle.WaitOne();
                    await TestMethod(param);
                    waitHandle.Set();
                }).ToArray();
            Console.ReadKey();
        }
        private static async Task<int> TestMethod(int param)
        {
            Console.WriteLine($"{param} before");
            await Task.Delay(50);
            Console.WriteLine($"{param} after");
            return param;
        }
    }