Допустим, у меня есть следующий пример кода:
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 для этого асинхронного метода? Если я просто использую асинхронную лямбду 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 вызова параллельно - мне нужно, чтобы они выполнялись один за другим, начиная следующий, только когда предыдущий полностью закончил и вернул значение.
Похоже, что в настоящее время нет другого решения (до С# 8.0), кроме перечисления его вручную. Я сделал метод расширения для этого (возвращает список вместо массива, как это проще):
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;
}
В С# 8 (следующая основная версия на момент написания) будет поддерживаться IAsyncEnumrable<T>
где вы можете написать async для каждого цикла:
await foreach (var item in GetItemsAsync())
// Do something to each item in sequence
Я ожидаю, что будет метод расширения Select
для проецирования, но если нет, то написать свой собственный не сложно. Вы также сможете создавать блоки итератора IAsyncEnumrable<T>
с yield return
.
Я регулярно сталкиваюсь с этим требованием и не знаю ни одного встроенного решения для его решения, как в С# 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
стремится, а не ленив. С# 8 обещает ввести асинхронные потоки, которые позволят этому снова быть ленивым.
Вы должны знать, что такое объект 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>
Enumerable
, я ее не осознавал. Я пришел к очень похожему ToListAsync
расширения ToListAsync
, за исключением того, что я использовал foreach
вместо прямой работы с Enumerable (см. Ответ, который я добавил). Я понимаю, что foreach
основном делает то же самое под капотом. Это правильно, или я что-то пропустил?
Вы можете использовать примитивы синхронизации
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;
}
}
foreach
?foreach
, я просто надеялся, что уже есть решение в стандартной библиотеке.