寻找特定的Enumerable操作符序列:TakeWhile(!) + Concat Single。

6

给定一个 Enumerable,我想要使用 Take() 方法获取到包括终止符在内的所有元素(如果没有找到终止符则抛出异常)。类似于下面的代码:

list.TakeWhile(v => !condition(v)).Concat(list.Single(condition))

除了糟糕的,我只想走一遍它。

使用.NET 4和Rx中当前的操作符是否能够简洁地实现此操作?还是需要编写新的操作符?

编写操作符所需时间比编写这个问题更少(尽管我认为其中一半的时间将用于想出如何命名此函数),但我不想重复已经存在的内容。

更新

好的,这里是操作符。非常激动人心,我知道。无论如何,有可能从内置操作符中构建它吗?

    public static IEnumerable<T> TakeThroughTerminator<T>([NotNull] this IEnumerable<T> @this, Func<T, bool> isTerminatorTester)
    {
        foreach (var item in @this)
        {
            yield return item;
            if (isTerminatorTester(item))
            {
                yield break;
            }
        }

        throw new InvalidOperationException("Terminator not found in list");
    }

3
我只需要编写一个操作符来完成这项工作... - Marc Gravell
我同意 - 你可能可以使用巧妙的 Zip() 进行前瞻,但这仍然需要遍历枚举两次 - 使用运算符是正确的方法。 - BrokenGlass
顺便问一下,你对这个事实有何看法呢?在我们知道是否被允许使用这些项之前(即未抛出异常),就会产生大量的yielded项。 - skarmats
1
嗯,我想在大多数情况下,即使使用流式可枚举对象,这仍然是最好的解决方案。但是,如果它是流式的,你不能真正对项目进行操作,直到确认选择成功为止。有些不尽如人意。一个完全安全的解决方案总是需要在产出之前检查是否成功。(流式指从某个源中进行昂贵的检索) - skarmats
3个回答

2

如果您不想编写自己的操作符,则可以使用以下高级技巧:

var input = Enumerable.Range(1, 10);

var condition = new Func<int, bool>(i => i < 5);

bool terminatorPassed = false;
var condition2 = new Func<int, bool>(i =>
        {
            try { return !terminatorPassed; }
            finally { terminatorPassed = !condition(i); }
        });

var result = input.TakeWhile(condition2).ToArray();
if (!terminatorPassed) throw new FutureException("John Connor survived");

1
这真的很酷,但希望我在工作中永远不会遇到这种情况。 - Jimmy
1
我也喜欢它,不常有机会滥用finally语句。但是,就像我的解决方案一样,如果在检查之前不执行可枚举对象,它将始终抛出异常。 - skarmats
很遗憾,我们在解决方案中不能使用TakeWhile(),因为它会太早终止。我们需要验证整个序列是否包含单个失败项(根据原始查询的逻辑)。 - Jeff Mercado
在我看来,首先经过所有事情的筛选才是唯一安全的解决方案。如果枚举器可能在最后抛出异常,那么你无法使用已经产生的结果进行实际工作。 - skarmats
@skarmats,感谢您提到这一点,我已经修复了代码以避免由Linq惰性引起的异常。 - Snowbear

2

没有内置方法可以高效地执行这样的操作。很少有人需要获取满足条件的项目以及不满足条件的项目。您需要自己编写代码实现。

但是,您可以使用现有的方法来构建此功能,只是效率不会很高,因为您需要以某种方式保持状态,从而使代码变得更加复杂。我不建议这种查询方式,因为它违背了LINQ的哲学,我会自己编写代码。但既然您提出了要求:

var list = Enumerable.Range(0, 10);
Func<int, bool> condition = i => i != 5;
int needed = 1;
var query = list.Where(item => condition(item)
                                   ? needed > 0
                                   : needed-- > 0)
                .ToList(); // this might cause problems
if (needed != 0)
    throw new InvalidOperationException("Sequence is not properly terminated");

然而,这也存在自己的问题,无法很好地解决。正确处理此问题的方法是手动编写代码(不使用LINQ)。这将给您完全相同的行为。

public static IEnumerable<TSource> TakeWhileSingleTerminated<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    var hasTerminator = false;
    var terminator = default(TSource);
    foreach (var item in source)
    {
        if (!hasFailed)
        {
            if (predicate(item))
                yield return item;
            else
            {
                hasTerminator = true;
                terminator = item;
            }
        }
        else if (!predicate(item))
            throw new InvalidOperationException("Sequence contains more than one terminator");
    }
    if (!hasTerminator)
        throw new InvalidOperationException("Sequence is not terminated");
    yield return terminator;
}

经过深思熟虑,我认为很难得到最有效的原始查询实现,因为它具有冲突的要求。您正在混合TakeWhile()(提前终止)和不能终止的Single()。可以复制最终结果(我们都在这里尝试过),但是行为不能在不对代码进行重大更改的情况下实现。如果目标是仅获取第一个失败的项目,则完全可以实现和复制,但由于不是这样,您将不得不处理此查询存在的问题。
附言:我认为仅从我在这个答案上所做的编辑数量就可以看出这很不容易。希望这是我的最后一次编辑。

这使得如果未找到终止符,抛出异常变得困难。不要认为列表总是一个列表。请参阅我的答案。 - skarmats
@Jeff M:我对列表的喃喃自语源于这样一个想法,即可以通过比较.Count来判断是否需要抛出异常。沿着“未找到终止符”&&“计数等于源”的方向。我不喜欢运算符解决方案的原因是,它枚举了大量的项目,然后可能在异常上全部丢弃它们(当然,我们所有的解决方案都是这样做的...)。请参见我在问题帖子上的问题。 - skarmats
@skarmats:你让我看到了光明。起初,我认为这可以相对容易地完成,但现在你(正确地)提出了其他条件后,我得出的结论是,这个问题根本没有一个好的解决方案。 - Jeff Mercado
@Jeff M:是的,原始查询无法编译。如果您像这样编写.Concat(list.Where(cond).Take(1)),那么您是正确的——它将被延迟评估。但是为了保持接近原始伪查询,我尽可能地使用.Single()来接近原始语句。基本上通过 new T[]。如果你不把 .Single() 隐藏在一些 yield return 中,它总是会立即被评估。 - skarmats
@Jeff M:我知道你的意思,但这可能不是作者想要表达的...但我的意思是,任何一个第一/单一类方法都像ToList一样,如果没有被人为地隐藏起来,它们就会立即被评估。 - skarmats
显示剩余4条评论

0
int? j = null;
var result = list.TakeWhile((o, i) =>
                        {
                          if (j == null && cond(o)) { j = i + 1; }
                          return (j ?? -1) != i;
                        });
if (j == null) { throw new InvalidOperationException(); }

我会选择运算符,但是怎么能确定真的没有内置的方法呢?;-)

更新1:好吧,我的代码没用。如果在检查异常之前未执行 Enumerable 的执行,则我敢打赌它总是会抛出异常...


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