假设我有以下示例代码:
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对before
和after
:
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个调用-我需要它们一个接一个地执行,仅在前一个完全完成并返回值后才开始下一个。
答案 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中可用的IAsyncEnumerable
和System.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
将在内部调用GetEnumerator
和MoveNext
。因此,要使您的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;
}
}