0

I have this code:

   class Foo
    {
        public string A { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var strings = new List<string> { "foo", "foo" };

            var list = strings.Select(x => new Foo { A = x });

            foreach (var item in list)
            {
                item.A = "bar";
            }

            foreach (var item in list)
            {
                Console.WriteLine(item.A);
            }
        }
    }

Which prints:

foo
foo

What exactly happens when you set item.A = "bar" ?

After the first foreach loop finishes, does the list var really contain the same two Foo objects with "bar" as the new string?

If so, how could you access these new values?

I understand that when the 2nd foreach loop runs, it is enumerating the collection of strings which is why you get two print outs of "foo", but I'm just confused as to what happens when item.A = "bar" is run, and if you are never able to access that new value, why does the compiler allow you to modify it?

David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • 4
    the fundamental problem here is that `Select` is a lazily evaluated sequence projection; if you enumerate it twice, it is *evaluated* twice; the `Foo`s that you create in the first iteration are **completely unrelated** to the `Foo`s that you create in the second iteration. So: it isn't that the change is or isn't preserved: it is that they're *completely different projections* – Marc Gravell Oct 24 '19 at 11:23
  • 1
    `why does the compiler allow you to modify it?` Why wouldn't it allow you to do it? Your code doesn't do anything useful - but it is valid. – mjwills Oct 24 '19 at 11:27
  • 1
    `What exactly happens when you set item.A = "bar" ?` It sets the value of `item.A` to `bar`. You could verify this by adding `Console.WriteLine(item.A);` on the next line. And then, since there are no further references to `item`, it will be GCed (sometime in the future). Thus **effectively** the code does nothing useful. – mjwills Oct 24 '19 at 11:37
  • @mjwills you can't update the reference for a foreach iteration variable. If you could, you'd just be updating the iteration variable and not the underlying collection. This would also not do anything useful but would be valid, but the compiler prevents this. – David Klempfner Oct 25 '19 at 00:10
  • @mjwills I assume the compiler cannot tell if you are updating a pure IEnumerable, or something that implements IEnumerable. However the runtime should be able to detect this and throw an Exception. The Exception would make it obvious that what you're trying to do is not doing what you think it should be doing. – David Klempfner Oct 25 '19 at 00:21
  • 1
    `However the runtime should be able to detect this and throw an Exception.` **How** would it detect this? Think about what this implementation would require. The runtime would have to keep track of the fact that this object was sourced from an enumerable. It would have to keep track of the type of enumerable (since it should allow it if it was List based etc). And it would have to track whether the variable was assigned somewhere else (since, if it was, the code could _potentially_ be legitimate). – mjwills Oct 25 '19 at 00:28
  • 1
    And how far does it go? What if the logic to set the property was extracted into a separate function? Should it still detect that? If you sit back and realise the **runtime overhead** required to implement such detection (overhead that will be born **all of the time**, not just when there is a problem) then the decision they made (to not try and detect it) seems reasonable. And that is ignoring the development and documentation costs, and opportunity costs (https://blogs.msdn.microsoft.com/ericgu/2004/01/12/minus-100-points/). – mjwills Oct 25 '19 at 00:30
  • 1
    Plus, maybe the setter has some logic in it that writes to the `Console` - and that is what the programmer wanted to occur (even though the object was essentially lost the `Console` write will still occur). So, you have a detection system that is costly to build, costly to maintain, costly to run, rarely useful, and likely to have either a high false positive or false negative detection rate. Plus it has a simple workaround that most people know (`ToList`). I can understand why it wasn't built. :) – mjwills Oct 25 '19 at 00:33
  • 1
    Now, I am not denying this is annoying - it is painful to run into these kinds of issues for sure. But you tend to make the mistake once and learn pretty quickly not to do it again (just like you do with integer division, and a large set of weird things in programming). Crappy yes, but liveable. – mjwills Oct 25 '19 at 00:36
  • 1
    If you'd like better detection of this, I'd suggest investing in Resharper. It would have warned you - https://stackoverflow.com/a/58540346/34092 . – mjwills Oct 25 '19 at 00:46

3 Answers3

1

The problem is that you haven't called ToList method to materialize your LINQ query. When you call ToList as below:

var list = strings.Select(x => new Foo { A = x })
                  .ToList();

an in-memory collection of Foo objects would be created, whose property value A would have the value x. Essentially two new objects of type Foo would be created with the value of A to be "foo". Then you can loop through this list and modify the property value.

Please look at this fiddle

Christos
  • 53,228
  • 8
  • 76
  • 108
1

What's happening here is that you are creating an enumerable list which you are enumerating multiple times.

Each time you enumerate list, the enumeration processes the elements of the strings list calling new Foo { A = x } for each element to create the elements of the resulting sequence.

That means the the Foo objects created by the first foreach enumeration are NOT the same as the ones created by the second foreach enumeration. New ones are created for each enumeration.

This is the reason that Resharper warns about "possible multiple enumeration".

To avoid this, you would use var list = strings.Select(x => new Foo { A = x }).ToList(); to enumerate the sequence just once and store the results in an actual List<T>.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
0

You are right that if will not going to be chnaged, then why compiler allow. but if you want to print without updating actual item in this scenario above code will helpful.

One thing you should know that, you can not modified the item of IEnumerable object. you have to use List();

 var strings = new List<string> { "foo", "foo" };
 var list = strings.Select(x => new Foo { A = x }).ToList();

    foreach (var item in list)
    {
            item.A = "bar";
    }

    foreach (var item in list)
    {
           Console.WriteLine(item.A);
    }
Vishal modi
  • 1,571
  • 2
  • 12
  • 20
  • I understand that the references (or values if ValueTypes are used) inside an IEnumerable are readonly, however the objects they refer to are not necessarily readonly. This is why the compiler allows you to modify a property on the object, but not change the object's reference. – David Klempfner Oct 24 '19 at 11:25