Linq to Objects: TakeWhileOrFirst

4

如何使用linq将以下方法应用于序列,使其更易读:

TakeWhile elements are valid but always at least the first element

编辑:我已更新标题,以更加精确。对于任何混淆的地方,我很抱歉,下面的答案肯定教会了我一些东西!

期望的行为是这样的:只要元素有效,就取出来。如果结果为空序列,那么仍然取第一个元素。


我已经更新了问题。请查看编辑。抱歉造成困惑。 - asgerhallas
4个回答

5
我认为这很清楚地表明了意图:
things.TakeWhile(x => x.Whatever).DefaultIfEmpty(things.First());

我之前的解决方案比较啰嗦:

var query = things.TakeWhile(x => x.Whatever);
if (!query.Any()) { query = things.Take(1); }

我认为这并不会使代码难以阅读。这只是使用函数式编程范式的方法(LINQ允许我们这样做)。如果你没有接触过函数式代码,而只有过程式代码的经验,那么是的,它可能被认为是“难以阅读”,但原因完全错误。 - Jeff Mercado
1
我所说的“难以阅读”,是指“无法清晰表达原意”。如果编写得当,功能代码可以非常易读。但我必须承认,在某些情况下,确实有一行代码可以清晰地表达意图。 - Botz3000
啊,这是一个好的更新。当我不用写外连接时,我似乎总是忘记那些操作。 - Jeff Mercado
1
这并未考虑到只能枚举一次的查询。我认为你的解决方案是可以的,但是我不确定索引重载是否适用于如ObjectResult<T>的情况。 - Botz3000
非常感谢您的回答。"只能枚举一次"确实很重要。好观点! - asgerhallas

4
以下内容*,在我的看来读起来相当顺畅:

以下内容*,在我的看来读起来相当顺畅:

seq.Take(1).Concat(seq.TakeWhile(condition).Skip(1));

也许有更好的方法,不确定。*感谢@Jeff M 的纠正

3
你需要一个IEnumerable<>才能使用Concat()。我会用Take(1)来代替First() - Jeff Mercado
@Rune:请注意,我跳过了取出集合中的第一个值,而不是跳过集合中的第一个值,然后在满足条件时开始取值。如果第一个值不符合条件,而后续的值符合条件,则只有第一个值会被返回。 - Anon.

1

据我所知,为了确保不进行不必要的枚举,最有效的实现方式是手动实现。

public static IEnumerable<TSource> TakeWhileOrFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
            yield break;
        TSource current = enumerator.Current;
        yield return current;
        if (predicate(current))
        {
            while (enumerator.MoveNext() && predicate(current = enumerator.Current))
                yield return current;
        }
    }
}

为了完整起见,包括索引的重载:
public static IEnumerable<TSource> TakeWhileOrFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
{
    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
            yield break;
        TSource current = enumerator.Current;
        int index = 0;
        yield return current;
        if (predicate(current, index++))
        {
            while (enumerator.MoveNext() && predicate(current = enumerator.Current, index++))
                yield return current;
        }
    }
}

我刚刚删除了一个类似的答案:它是错误的。如果第一项不匹配谓词,我们应该将其排除并停止。但这段代码没有做到这一点。 - Ani
@Ani:我不确定哪种解释是正确的。但我认为“始终至少第一个元素”可能意味着“始终至少是头元素”,并且不一定要匹配条件。如果是另一种方式,最好用“至少有一个符合条件的元素”来表达。除非OP澄清,否则我们应该没问题。如果有人没有理解这种解释,我会尝试想出一种方法来解决,并在我的答案中包含它。 - Jeff Mercado
我猜测。我的推理完全基于我如何解释上下文中没有给出的“TakeWhileButAtLeastOne”的含义。 :) - Ani
我真的无法理解这个问题的意思是始终取第一个元素和任何后续匹配的元素。如果第一个不匹配但后面的匹配,这种方法将失败。 - Rune FS
Botz3000的答案做得很好,但如果源序列只能迭代一次,Jeff M的答案具有很大的优势。干得好! - asgerhallas
显示剩余4条评论

1

免责声明: 这是Jeff Ms不错答案的变体,因此仅旨在展示使用do-while的代码。它只作为Jeffs答案的扩展提供。

public static IEnumerable<TSource> TakeWhileOrFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
            yield break;
        var current = enumerator.Current; 
        do{
            yield return current
        } while (predicate(current) && 
                 enumerator.MoveNext() && 
                 predicate(current = enumerator.Current) );
    }
}

当然这是一种风格问题,我个人喜欢尽可能地降低我的条件逻辑嵌套级别,但谓词的双重使用可能很难理解,并且可能会稍微影响性能(取决于优化和分支预测)。

如果低嵌套级别是目标,我认为最干净的方法是使用我已经有的(使用while循环),但是相反,如果第一个检查失败,则使用yield break; - Jeff Mercado
@Jeff 我认为,你拥有的退出点越多(无论是称之为 continue、return、goto 还是语言规范所需的任何内容),代码就越难以阅读和理解,同样的情况也适用于额外的分支点。这两者都被证明会增加错误的可能性,考虑到这一点,我认为额外的 if 和额外的退出点会产生负面影响。 - Rune FS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接