tags:

views:

97

answers:

3

I cannot figure this out. The workflow of passing IEnumerable<T> (T is some my class, but it is not relevant here) basically looks like this:

var a = GetEntireCollection(); // so I get IEnumerable<T>
...
var b = a.Where(condition1);
...
var c = b.Where(condition2);
...

So I filter out more and more items from the collection, finally I call:

if (z.IsEmpty())
    throw new Exception();
Foo(z);

and Foo is

public void Foo(IEnumerable<T> p)
{
    pool = p.OrderByDescending(it => it.MyProperty).ToList();

    if (pool.IsEmpty())
        throw new Exception(pool.Count().ToString() + ", " + p.Count().ToString());
    ...

All I do, is order the collection.

Now, my program crashes with exception -- it says that p has Count = 1, and pool has Count = 0. What's more when I point out p and require the results (I run program using Visual Studio) it says, the collection yielded with no results (or somethig similar, not verbatim quote).

Questions:

  1. how can non-empty collection become empty just by reordering?
  2. how can collection Count can be > 0, when there are no items in it?

I am asking because I would like to know how to avoid this situation, but honestly, when I am looking at the code it seems 100% legit for me.

Technical background:

  • it is pure C# code, no asm inlines, or anything like this
  • no threads
  • no external libraries, except for Where (which comes from Linq) this is all my code

Edits

Edit 1

public static bool IsEmpty<T>(this IEnumerable<T> coll)
{
    var iter = coll.GetEnumerator();
    return !iter.MoveNext();
}

Edit 2

Just before I call Foo(z) I check if the z is empty, so the code looks like this:

if (z.IsEmpty())
    throw new Exception();
Foo(z);

SOLVED

As Jon suggested (C# sharpshooting I would say) one of the conditions was time dependent. So when the collection evaluation was forced the condition changed and I get in fact another collection.

+1  A: 

It's empty because of Deferred Execution, most query operators execute not when constructed, but when enumerated (in other words, when MoveNext is called on its enumerator).

So if you change it to :

var c = b.Where(condition2).ToList(); 

And then call Foo(c) it will work.

ace
You are assuming it is IQueryable, but that is incorrect.
leppie
I don't think so -- the `Count` call should cause `p` to be evaluated.
Dan Tao
While this *may* end up being explained by repeated evaluation (which is in turn due to deferred execution) you haven't really explained why `p.Count()` would be 1 but `pool.Count()` would be 0. We don't really have enough information at the moment.
Jon Skeet
@Dan: Count call is made in the Exception, before that I don't see any count call.
ace
@ace: So that forces it to be evaluated once, and prior to that it will be evaluated due to the `OrderByDescending(...).ToList()` call.
Jon Skeet
@ace: But the exception is thrown on `pool.IsEmpty()`, which shouldn't be subject to deferred execution since `pool` is a `List<T>`. It seems the main question is: why would `pool.Count()` and `p.Count()` return disparate values? Personally I don't think deferred execution can answer this.
Dan Tao
@Dan: It can, if the conditions effectively change between executions. For example, consider `x.Where(x => DateTime.Now.Seconds > 30)` - that clearly has the ability to return different results each time it's executed. So `pool` could end up with 0 items, but when evaluating `p.Count()` you could end up with 1. However, we need more information before we know if that's actually the problem.
Jon Skeet
@Jon: True, my statement was too broad. What I meant was that I don't think ace's explanation is accurate; he seems to be saying that `Count()` is bound to return 0 because the enumeration doesn't happen (which is false). At least that's how I interpreted this answer. But yeah, the ramifications of deferred execution could ultimately be to blame in a different sense, I concede.
Dan Tao
@Dan: Agreed on all counts - I suspect ace is a bit confused about what deferred execution really entails, but I also expect deferred execution to be *something* to do with it :)
Jon Skeet
Just a comment -- converting it to something solid like ToList would of course solve the problem, but I was interested why it happens (about the source of the problem).
macias
+3  A: 

One possibility is that evaluating the conditions twice is effectively changing the results.

Each time you iterate through p, it will re-evaluate the data, including the Where clauses. So you're doing that once to populate pool, and then again to print out p.Count().

Without any more information about what GetEntireCollection does or what the Where conditions are, it's hard to say what's going on... but that's one possible explanation. If you can post a short but complete program demonstrating the problem, that would help a lot.

Jon Skeet
Thank you, it could be a cause, when looking I don't see any time dependant conditions, but of course I can miss something. I will try to track it (or simplify the code) and will post update ASAP.
macias
I found out I already added check for being empty before I call Foo. Even with deferred exection should this condition hold true (once executed) in all subsequent calls?
macias
@macias: Nope - because unless `z` is something concrete and non-deferred (like a `List<T>`) its results can change every time you iterate over it. Note that it doesn't have to be time-dependent... if any of your conditions have side effects, that could cause the same kind of problem.
Jon Skeet
Found it :-( Sad face because it is embarrasing for me, however on the other side it was very valuable lesson for me -- despite I know about late execution I was bite so easily ;-). Many thanks (I cannot upvote +10 for the help though :-D)!!!
macias
A: 

I think the code you show will call GetEntireCollection multiple times.

So maybe this method is returning different results on successive invocations.

Joe
No, it's not going to call `GetEntireCollection` multiple times. The source code within `GetEntireCollection` will execute multiple times *if* it's an iterator block, but the method itself will only be called once.
Jon Skeet