等待异步谓词列表,但在第一个false时退出

本文关键字:第一个 false 退出 异步 谓词 列表 等待 | 更新日期: 2023-09-27 18:10:31

想象下面的类:

public class Checker
{
   public async Task<bool> Check() { ... }
}

现在,想象一个该类的实例列表:

IEnumerable<Checker> checkers = ...

现在我想控制每个实例将返回true:

checkers.All(c => c.Check());

现在,这将无法编译,因为Check()返回Task<bool>而不是bool

所以我的问题是:我怎样才能最好地枚举检查器列表?我怎么能在检查器返回false时快捷枚举?(我认为All( )已经做到了)

等待异步谓词列表,但在第一个false时退出

"异步序列"总是会引起一些混淆。例如,不清楚您想要的语义是否为:

  1. 同时启动所有检查,并在完成后对其进行评估。
  2. 每次启动一个检查,按顺序评估它们。

还有第三种可能(同时开始所有检查,并按顺序对它们进行评估),但在这种情况下,这将是愚蠢的。

我建议对异步序列使用Rx。它给了你很多选择,有点难学,但它也迫使你思考你到底想要什么。

以下代码将同时启动所有检查,并在检查完成时对其进行评估:

IObservable<bool> result = checkers.ToObservable()
    .SelectMany(c => c.Check()).All(b => b);

它首先将检查器序列转换为一个可观察对象,对它们全部调用Check,并检查它们是否都是true。第一个以false值结束的Check将导致result产生false值。

相反,下面的代码将一次启动一个检查,按顺序计算它们:

IObservable<bool> result = checkers.Select(c => c.Check().ToObservable())
    .Concat().All(b => b);

它首先将检查器序列转换为可观察对象序列,然后将这些序列连接起来(每次启动一个)。

如果你不想过多地使用可观察对象,也不想扰乱订阅,你可以直接await它们。例如,在所有检查器上调用Check并在它们完成时评估结果:

bool all = await checkers.ToObservable().SelectMany(c => c.Check()).All(b => b);

我如何在检查器返回false时快捷枚举?

这将按完成顺序检查任务的结果。因此,如果任务#5是第一个完成的任务,并返回false,则该方法立即返回false,而不考虑其他任务。较慢的任务(#1,#2等)永远不会被检查。

public static async Task<bool> AllAsync(this IEnumerable<Task<bool>> source)
{
    var tasks = source.ToList();
    while(tasks.Count != 0)
    {
        var finishedTask = await Task.WhenAny(tasks);
        if(! finishedTask.Result)
            return false;
        tasks.Remove(finishedTask);
    }
    return true;
}

用法:

bool result = await checkers.Select(c => c.Check())
                            .AllAsync();

All并没有考虑到async(像所有LINQ一样),所以你需要自己实现:

async Task<bool> CheckAll()
{
    foreach(var checker in checkers)
    {
        if (!await checker.Check())
        {
            return false;
        }
    }
    return true;
}

你可以用一个通用的扩展方法使它更易于重用:

public static async Task<bool> AllAsync<TSource>(this IEnumerable<TSource> source, Func<TSource, Task<bool>> predicate)
{
    foreach (var item in source)
    {
        if (!await predicate(item))
        {
            return false;
        }
    }
    return true;
}

并像这样使用:

var result = await checkers.AllAsync(c => c.Check());

你可以做

checkers.All(c => c.Check().Result);

,但这会同步运行任务,这可能非常慢,取决于Check()的实现。

下面是一个功能齐全的测试程序,遵循dcastro的步骤:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AsyncCheckerTest
{
    public class Checker
    {
        public int Seconds { get; private set; }
        public Checker(int seconds)
        {
            Seconds = seconds;
        }
        public async Task<bool> CheckAsync()
        {
            await Task.Delay(Seconds * 1000);
            return Seconds != 3;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var task = RunAsync();
            task.Wait();
            Console.WriteLine("Overall result: " + task.Result);
            Console.ReadLine();
        }
        public static async Task<bool> RunAsync()
        {
            var checkers = new List<Checker>();
            checkers
                .AddRange(Enumerable.Range(1, 5)
                .Select(i => new Checker(i)));
            return await checkers
                            .Select(c => c.CheckAsync())
                            .AllAsync();
        }
    }
    public static class ExtensionMethods
    {
        public static async Task<bool> AllAsync(this IEnumerable<Task<bool>> source)
        {
            var tasks = source.ToList();
            while (tasks.Count != 0)
            {
                Task<bool> finishedTask = await Task.WhenAny(tasks);
                bool checkResult = finishedTask.Result;
                if (!checkResult)
                {
                    Console.WriteLine("Completed at " + DateTimeOffset.Now + "...false");
                    return false;
                }
                Console.WriteLine("Working... " + DateTimeOffset.Now);
                tasks.Remove(finishedTask);
            }
            return true;
        }
    }
}

下面是示例输出:

Working... 6/27/2014 1:47:35 AM -05:00
Working... 6/27/2014 1:47:36 AM -05:00
Completed at 6/27/2014 1:47:37 AM -05:00...false
Overall result: False

请注意,当达到退出条件时,整个eval结束,而不会等待其余部分完成。

作为一种更开箱即用的替代方案,这似乎可以并行运行任务,并在第一次失败后不久返回:

var allResult = checkers
    .Select(c => Task.Factory.StartNew(() => c.Check().Result))
    .AsParallel()
    .All(t => t.Result);

我对TPL和PLINQ不太感兴趣,所以请随时告诉我这有什么问题