6

Consider the following example:

IEnumerable<Int32> groupsToAdd = new List<Int32>();

List<Int32> groups1 = new List<Int32>() { 1,2,3 };
List<Int32> groups2 = new List<Int32>() { 3,4,5 };

groupsToAdd = groups1.Where(g => false == groups2.Contains(g));

groups2.AddRange(groupsToAdd);

groupsToAdd.Dump();

When groupsToAdd.Dump() is called the list is now empty. I've looked up the AddRange reference and it doesn't mention that the elements are removed from list but when i test this code (in linqpad) it ends empty. Why is this?

Edit: To clarify, I mean that the elements are being removed from groupsToAdd because before groups2.AddRange(groupsToAdd) groupsToAdd is populated with two elements

Letseatlunch
  • 2,432
  • 7
  • 28
  • 33
  • 1
    There must be some code you're not showing us. The code you have above doesn't alter groups1 at all. – itsme86 Aug 20 '13 at 19:53
  • 2
    @itsme86 it doesn't need to – Servy Aug 20 '13 at 19:55
  • 1
    @itsme86 This is the complete code and this code by itself will 'Dump' a empty list in linqpad and in unit tests. – David Work Aug 20 '13 at 19:57
  • 1
    If you use `groupsToAdd = groups1.Where(g => false == groups2.Contains(g)).ToList();` instead you materialize the query to a `List`. Then it behaves as expected because there is no deferred execution anymore. – Tim Schmelter Aug 20 '13 at 20:00
  • I don't think you guys are reading the question. The OP says that the code is removing the elements from the first list (i.e. groups1). Not that groupsToAdd is empty. – itsme86 Aug 20 '13 at 20:02
  • 1
    @itsme86: _"When groupsToAdd.Dump() is called the list is now empty"_ He thinks that `groupsToAdd` is a list which is incorrect. – Tim Schmelter Aug 20 '13 at 20:04
  • @itsme86 To clarify, I mean that the elements are being removed from groupsToAdd because before "groups2.AddRange(groupsToAdd)" groupsToAdd is populated with two elements – Letseatlunch Aug 20 '13 at 20:04
  • @TimSchmelter Or he's saying that when `groupsToAdd.Dump()` is called, `groups1` is now empty, which is how I read that. – itsme86 Aug 20 '13 at 20:04
  • @Letseatlunch Oh okay, I was misinterpreting then. – itsme86 Aug 20 '13 at 20:05

2 Answers2

15

What's important to remember, when using LINQ, is that it results in a query, not the results of that query. groupsToAdd is not a list of items, it's just the definition of a query that is able to get some items when it needs to.

groupsToAdd doesn't actually iterate the source sequence (which is groups1) or perform the predicate checks (which is dependant on the state of groups2) until it is iterated.

You iterate groupsToAdd twice. Once with the call to AddRange, and again with the call to Dump. The second time you're iterating it group2 has changed, and thus the results of the query have changed as well.

If you want to avoid this deferred execution then you can materialize the query right away by modifying your code to be something like:

groupsToAdd = groups1.Where(g => false == groups2.Contains(g));
    .ToList();

This will evaluate the query at that moment in time so that groupsToAdd will represent the results of the query instead of the query itself.

Servy
  • 202,030
  • 26
  • 332
  • 449
11

It's because of the IEnumerable. When you set groupsToAdd to the result of groups1.Where(g => false == groups2.Contains(g)) there is deferred execution, which means that the query is not run until AddRange() and then again at Dump(). Because the list, groups2, now contains the elements they no longer are a result of the original query.

David Work
  • 269
  • 1
  • 4
  • 13