【问题标题】:Break from function composition on condition using LINQ使用 LINQ 在条件下打破函数组合
【发布时间】:2019-08-07 10:17:37
【问题描述】:

是否有与以下等效的 LINQ?

static T Compose<T>(T source,
    IEnumerable<Func<T, T>> funcs,
    Func<T, bool> cond)
{
    foreach (var func in funcs)
    {
        source = func(source);

        if (cond(source)) break;
    }

    return source;
}

// This ends the composition as soon as a null is returned
var result = Compose(x, funcs, x => x == null);

本质上,我希望在彼此之上组合函数,如果当前值匹配特定条件则中断。

result = f(g(h(x)))  // if condition is met somewhere within this chain,
                     // it will immediately break and return

编辑:我想到了一些类似的东西

funcs.Aggregate(seed, (x, f) => cond(x) ? x : f(x));

但这看起来令人困惑,并且最终会枚举整个集合,而不是在满足条件时中断。

【问题讨论】:

  • 要在不满足条件后停止计算,您可以聚合函数本身,而不是其执行结果:funcs.Aggregate((cur, next) =&gt; (i =&gt; { var val = cur(i); return cond(val) ? next(val) : val; }))

标签: c# linq


【解决方案1】:

这样做的一种方法是Aggregate 直到满足条件。所以,我定义了一个扩展方法:

public static TAccumulate AggregateUntil<TSource, TAccumulate>(
    this IEnumerable<TSource> source,
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> func,
    Func<TAccumulate, bool> predicate)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    if (func == null)
        throw new ArgumentNullException(nameof(func));

    if (predicate == null)
        throw new ArgumentNullException(nameof(predicate));

    var accumulate = seed;
    foreach (var item in source)
    {
        accumulate = func(accumulate, item);
        if (predicate(accumulate)) break;
    }
    return accumulate;
}

我们现在可以将ComposeUntil 写成:

public static T ComposeUntil<T>(
    this IEnumerable<Func<T, T>> source,
    T seed,
    Func<T, bool> predicate)
{
    return source.AggregateUntil(seed, (x, f) => f(x), predicate);
}

并按以下方式使用:

var funcs = new List<Func<int, int>>
{
    x => x + 1,
    x => x + 2,
    x => x + 3,
    x => x + 4
};

var result = funcs.ComposeUntil(0, x => x >= 6);
// result == 6

【讨论】:

    【解决方案2】:

    这很好用:

    var funcs = new Func<string, string>[]
    {
        t => t + "1",
        t => t + "2",
        t => t + "3",
    };
    
    Func<string, bool> cond = t => t.Length == 3;
    
    Func<string, string> func =
        t => funcs.Aggregate(t, (a, f) => !cond(a) ? f(a) : a);
    
    Console.WriteLine(func("A"));
    

    它产生"A12"一个长度为3的字符串。

    为避免迭代集合,您需要引入一个新运算符 .Scan(...),这是我从 Reactive Extensions 团队的 Interactive Extensions (NuGet "Ix-Main") 获得的。

    .Scan(...) 的工作方式类似于 .Aggregate(...),但它在每一步都会产生一个值。

    Func<string, string> func =
        t => funcs
            .Scan(t, (a, f) => f(a))
            .SkipWhile(x => !cond(x))
            .FirstOrDefault();
    

    这会产生与上面相同的结果,但不会迭代整个集合。

    以下是Compose 方法中插入的两个选项:

    (1)

    static T Compose<T>(T source,
        IEnumerable<Func<T, T>> funcs,
        Func<T, bool> cond)
    {
        return funcs.Aggregate(source, (a, f) => !cond(a) ? f(a) : a);
    }
    

    (2)

    static T Compose<T>(T source,
        IEnumerable<Func<T, T>> funcs,
        Func<T, bool> cond)
    {
        return
            funcs
                .Scan(source, (a, f) => f(a))
                .SkipWhile(t => !cond(t))
                .FirstOrDefault();
    }
    

    【讨论】:

    • 它仍然会枚举整个funcs集合,即使在满足条件之后。
    • @MarcinJuraszek - 完成。
    • 如果条件始终为假,(2) 似乎会跳过所有内容。
    • @MateenUlhaq - 这不是应该做的吗?
    • 不,它应该返回最终值。换句话说,(1) 不等于 (2)
    【解决方案3】:

    @Mateen Ulhaq - 我不确定你是否能看到我更新的答案(以及下面的 cmets),因为它们变灰了,所以我重新发布了我的答案。要查看此代码的运行情况,并在您更改 cond 变量的 int 值时查看它在做什么:
    1) 打开LinqPad4。
    2)将语言设置为“C#程序”。
    3) 删除您在void main().. 等处看到的所有代码。
    4) 在下面复制我的代码并将其粘贴到 LinqPad 中。
    5) 按 F5。

    static T Compose<T>(T source,
            IEnumerable<Func<T, T>> funcs,
            Func<T, bool> cond)
        {
            var func = funcs.Select(x => 
            {
                var ret = x;
                source = x(source);
                return ret;
            })
            .FirstOrDefault(y => cond(source));
    
            Console.WriteLine("Here is the Func that the condition succeeded on(null if none):");
            Console.WriteLine(func);
    
            return source;
        }
    
    void Main()
    {
        var funcs = new Func<int, int>[]
        {
            a => a + 1,
            a => a + 2,
            a => a + 3,
        };
    
        // Set this "cond = b => b == ?" value to want your  
        // func will succeed on. Since we are continuing to add 
        // to the source, the three Func's above will succeed on 
        // any of the following values: 1, 3, 6 and immediately exit 
        // ending any further enumeration via the ".FirstOrDefault"
    
        Func<int, bool> cond = b => b == 3;
    
        Console.WriteLine("Here are the list of Funcs:");
        Console.WriteLine(funcs);
    
        var result = Compose(0, funcs, cond);
    
        Console.WriteLine();
        Console.WriteLine("Below is the actual result of the source after being ");
        Console.WriteLine("acted on by the Funcs. If no Func matched the condition,");
        Console.WriteLine("all Funcs acted on the source so the source will return 6.");
        Console.WriteLine("That means;");
        Console.WriteLine("If the first Func matched (aka cond = b => b == 1), the source will return 1.");
        Console.WriteLine("If the second Func matched (aka cond = b => b == 3), the source will return 3.");
        Console.WriteLine("If the third Func matched  (aka cond = b => b == 6), the source will return 6. Same ");
        Console.WriteLine("as matching no Func because all Funcs were applied. However, see above that 'null'");
        Console.WriteLine("was displayed for the 'Here is the Func that the condition succeeded on' when ");
        Console.WriteLine("you set the condition value to an int other than 1, 3, or 6.");
        Console.WriteLine();
        Console.WriteLine("Actual result of the source after being acted on by the Funcs retuned:");
        Console.WriteLine(result);
    }
    

    【讨论】:

    • 感谢您的宝贵时间。不幸的是,我想在彼此之上组合函数。对于你给的funcs,Compose(4, funcs, x =&gt; false) 应该产生(((4+1)+2)+3) = 10。
    • 嗨@Mateen Ulhaq。是的,它确实。在上面的代码中,如果我将 var result = Compose(0, funcs, cond); 更改为 var result = Compose(4, funcs, cond); 并在 LinqPad 中运行此代码,它会返回 10。除非我不理解某些内容,否则请选择此作为正确答案。
    • @Mateen Ulhaq。我喜欢这里使用的FirstOrDefault( 语法比.Take(1) 更好,因为它更简洁。但是,我最初对.Take(1) 的想法也有效。要查看.Take(1) 是否正常工作,请将.FirstOrDefault(y =&gt; cond(source)); 代码替换为.Where(y =&gt; cond(source)).Take(1);。这是我在 SO 上的第一个答案,尽管它包含与答案相同的方法,但下面的答案被否决了 3 次。我只是重新发布它,因为我不知道您是否可以看到它,因为它已经变灰了,所以请选择我的两个答案作为接受的答案,这样我的声誉就会得到修复。
    【解决方案4】:
    var funcs = new Func<int, int>[] {i => i + 1, i => i * 2, i => i - 3};
    Func<int, bool> cond = i => i < 20;
    var combined =
        funcs.Aggregate(
            (cur, next) => 
                i =>
                {
                    var val = cur(i);
                    return cond(val) ? next(val) : val;
                });
    Console.WriteLine(combined(10)); // 22
    

    注意,该循环被链式调用取代,因此函数的长度受堆栈大小的限制。

    【讨论】:

    • 条件使用错误。如果你通过 OP 的代码运行它,你会得到 11 作为答案,而不是 22。
    【解决方案5】:

    如果你真的经常想要这样的辅助函数,那么我建议你看this question将它创建为扩展方法,并在你想要的任何地方使用它(类似于你建议的提示,但聚合有限.)

    但是,如果您很少需要它,那么我会选择类似的东西:

    string value = "Tes";
    
    var funcs = new Func<string, string>[]
    {
        x => x + "t",
        x => x + "s",
        x => x + "s"
    };
    
    funcs.TakeWhile(f =>
    {   
        if (value == "Tests") // your condition
        {
            return false;
        }
    
        value = f(value); // reassignment of value
        return true;
    }).ToList();
    
    Console.WriteLine(value);
    

    编辑:更简洁的版本:

    string value = "Tes";
    
    var funcs = new Func<string, string>[]
    {
        x => x + "t",
        x => x + "s",
        x => x + "s"
    };
    
    funcs.TakeWhile(f => value != "Tests" && (value = f(value)) is string).ToList();
    
    Console.WriteLine(value);
    

    【讨论】:

    • 为什么这被否决了?你愿意解释吗?
    【解决方案6】:

    是的,使用 .Take(1)
    例如:

    var strList = new List<string> {"a","b","c","d"};
    var str = strList.Where(x => x.Equals("c")).Take(1);
    

    [编辑]
    同样,“在使用 LINQ 的条件下打破函数组合”问题的关键是使用 .FirstOrDefault 或 .Take(1)。

    这是每个@Enigmativity 请求/答案的完整代码。但是,请注意我的代码不需要像 @Enigmativity 那样的任何 3rd 方包:

    static T Compose<T>(T source,
            IEnumerable<Func<T, T>> funcs,
            Func<T, bool> cond)
        {
            var func = funcs.Select(x => 
            {
                var ret = x;
                source = x(source);
                return ret;
            })
            .FirstOrDefault(y => cond(source));
    
            Console.WriteLine("Here is the Func that the condition succeeded on(null if none):");
            Console.WriteLine(func);
    
            return source;
        }
    
    void Main()
    {
        var funcs = new Func<int, int>[]
        {
            a => a + 1,
            a => a + 2,
            a => a + 3,
        };
    
        // Set this "cond = b => b == ?" value to want your  
        // func will succeed on. Since we are continuing to add 
        // to the source, the three Func's above will succeed on 
        // any of the following values: 1, 3, 6 and immediately exit 
        // ending any further enumeration via the ".FirstOrDefault"
    
        Func<int, bool> cond = b => b == 3;
    
        Console.WriteLine("Here are the list of Funcs:");
        Console.WriteLine(funcs);
    
        var result = Compose(0, funcs, cond);
    
        Console.WriteLine();
        Console.WriteLine("Below is the actual result of the source after being ");
        Console.WriteLine("acted on by the Funcs. If no Func matched the condition,");
        Console.WriteLine("all Funcs acted on the source so the source will return 6.");
        Console.WriteLine("That means;");
        Console.WriteLine("If the first Func matched (aka cond = b => b == 1), the source will return 1.");
        Console.WriteLine("If the second Func matched (aka cond = b => b == 3), the source will return 3.");
        Console.WriteLine("If the third Func matched  (aka cond = b => b == 6), the source will return 6. Same ");
        Console.WriteLine("as matching no Func because all Funcs were applied. However, see above that 'null'");
        Console.WriteLine("was displayed for the 'Here is the Func that the condition succeeded on' when ");
        Console.WriteLine("you set the condition value to an int other than 1, 3, or 6.");
        Console.WriteLine();
        Console.WriteLine("Actual result of the source after being acted on by the Funcs retuned:");
        Console.WriteLine(result);
    }
    

    【讨论】:

    • 为什么投反对票?如果当前值匹配特定条件而不枚举整个集合,则此示例说明如何使用 linq 跳出 foreach 循环。这基本上是他的问题。
    • var str = strList.FirstOrDefault(x =&gt; x.Equals("c")); 在当前值匹配特定条件而不枚举整个集合时也可以中断。见https://msdn.microsoft.com/en-us/library/bb503062(v=vs.110).aspx和https://msdn.microsoft.com/en-us/library/bb340482(v=vs.110).aspx
    • 您能否展示您的代码如何替换 OP 的 Compose 方法的主体?
    • @Enigmativity - 完成并感谢您的回复。很遗憾,其他人在没有解释他们无法理解为什么我的回答是 OP 问题的关键的情况下投了反对票。最好先让人们了解他们的核心问题,就像我的回答一样。如果 OP 在解决他的问题的关键之外需要更多帮助,他本可以问的。其次,最好保留“开箱即用”C# 功能的答案,而不是依赖于引入 3rd 方代码。我的编辑现在是 OP 可以复制和粘贴的位置 :( 我希望这个社区能够尊重“......教人们如何钓鱼,而不仅仅是分发鱼。”
    • @Mateen Ulhaq 查看我的代码运行并查看它在您更改条件的 int 值时正在做什么:1)打开LinqPad4,2)将语言设置为“C#程序” , 3) 删除你看到的所有代码 'void main()..' etc , 4) 粘贴到我的代码中, 5) 按 F5。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-18
    • 1970-01-01
    • 1970-01-01
    • 2014-05-03
    • 2014-03-11
    相关资源
    最近更新 更多