开发者

Looking for a particular Enumerable operator sequence: TakeWhile(!) + Concat Single

开发者 https://www.devze.com 2023-02-20 11:26 出处:网络
Given an Enumerable, I want to Take() all of the elements up to and including a terminator (throwing an exception if the terminator is not found). Something like:

Given an Enumerable, I want to Take() all of the elements up to and including a terminator (throwing an exception if the terminator is not found). Something like:

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

..except not crappy. Only want to walk it once.

Is this concisely possible with current operators in .NET 4 and Rx or do I need to write a new operator?

Writing the operator would take me less time than it did to write this question (though I think half that time would be figuring out what to name this function), but I just don't want to duplicate something that's already there.

Update

Ok, here's the operator. Very exciting, I know. Anyway, possible to build it from built-in operators?

    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开发者_StackOverflow(item))
            {
                yield break;
            }
        }

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


There isn't a builtin to do such an operation efficiently. It's not very often that people would need to get items that satisfy a condition and one more that doesn't. You'd have to write it yourself.

However you can build this up using existing methods, it just won't be as efficient since you'd need to keep the state somehow only complicating your code. I wouldn't condone this sort of query as it goes against the philosophy of LINQ and would write it myself. But since you asked:

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");

However this has its own problems which can't really be resolved nicely. The right way to deal with this is to code this all by hand (without LINQ). This will give you the exact same behavior.

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;
}

After much thinking about this, I would say it would be difficult to get the most efficient implementation of the original query since it has conflicting requirements. You're mixing TakeWhile() which terminates early with Single() which cannot. It would be possible to replicate the end result (as we all have attempted here) but the behavior cannot without making nontrivial changes to the code. If the goal was to take only the first failing item, then this would be totally possible and replicable, however since it isn't, you'll just have to deal with the problems that this query has.

p.s., I think it's evident how non-trivial this is to do just by how many edits I have made on this answer alone. Hopefully this is my last edit.


Here is some hardcore if you don't want to write your own operator:

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");


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(); }

I'd go for operator, but how can one ever be sure there really isn't a built-in way? ;-)

UPDATE1: Ok, my code is useless. I'd bet it always throws an exception, if execution of the Enumerable has not taken place before the check for the exception...

0

精彩评论

暂无评论...
验证码 换一张
取 消