In C#, why can't I modify the member of a value type instance in a foreach loop?
Asked Answered
L

8

88

I know that value types should be immutable, but that's just a suggestion, not a rule, right? So why can't I do something like this:

struct MyStruct
{
    public string Name { get; set; }
}

 public class Program
{
    static void Main(string[] args)
    {
        MyStruct[] array = new MyStruct[] { new MyStruct { Name = "1" }, new MyStruct { Name = "2" } };
        foreach (var item in array)
        {
            item.Name = "3";
        }
        //for (int i = 0; i < array.Length; i++)
        //{
        //    array[i].Name = "3";
        //}

        Console.ReadLine();
    }
}

The foreach loop in the code doesn't compile while the commented for loop works fine. The error message:

Cannot modify members of 'item' because it is a 'foreach iteration variable'

Why is that?

Lenard answered 13/4, 2011 at 14:15 Comment(1)
+1: Good question. I've known for a long time that a foreach loop variable could not be modified, but I never really learned why!Katushka
K
89

Because foreach uses an enumerator, and enumerators can't change the underlying collection, but can, however, change any objects referenced by an object in the collection. This is where Value and Reference-type semantics come into play.

On a reference type, that is, a class, all the collection is storing is a reference to an object. As such, it never actually touches any of the object's members, and couldn't care less about them. A change to the object won't touch the collection.

On the other hand, value types store their entire structure in the collection. You can't touch its members without changing the collection and invalidating the enumerator.

Moreover, the enumerator returns a copy of the value in the collection. In a ref-type, this means nothing. A copy of a reference will be the same reference, and you can change the referenced object in any way you want with the changes spreading out of scope. On a value-type, on the other hand, means all you get is a copy of the object, and thus any changes on said copy will never propagate.

Keciakeck answered 13/4, 2011 at 14:27 Comment(7)
excellent answer. and where can find more proof about "value types store their entire structure in the collection"?Wray
@CuiPengFei: Value types store their entire value in a variable. Arrays are just a block of variables.Pensive
Exactly. A value type will hold its entire structure in whatever container it's placed in, be it a collection, a local var, etc. Incidentally, this is why it's better to make structs immutable: You end up with tons of copies of the "same" object and it's hard to keep track of which one took the change and whether it'll propagate and where. Y'know, the typical problems that happen with strings.Keciakeck
very easy explanationPulsate
This explanation is comletely correct, but I'm still wondering why enumerators work in this way. Couldn't enumerators have been implemented so that they manipulate collections of value types by reference thus enabling foreach to function effectively, or foreach implemented with the option of specifying that the iteration variable be of reference type to achieve the same thing. Does the limitation of the existing implementation bring any actual benefit?Rusk
Would the benefit of such a feature outweight the cost of specifying, implementing and testing it?Keciakeck
@Rusk You actually can write an Enumerator where Current returns by ref, and it will allow you to mutate the collection contents in a foreach. It's not part of the IEnumerator<T> interface, but you're not required to implement IEnumerator<T> to use foreach, just that you implement the specified properties/methods.Corney
P
20

It's a suggestion in the sense that there's nothing stopping you from violating it, but it should really be afforded much more weight than "it's a suggestion". For instance, for the reasons you're seeing here.

Value types store the actual value in a variable, not a reference. That means that you have the value in your array, and you have a copy of that value in item, not a reference. If you were allowed to change the values in item, it would not be reflected on the value in the array since it's a completely new value. This is why it isn't allowed.

If you want to do this, you'll have to loop over the array by index and not use a temporary variable.

Pensive answered 13/4, 2011 at 14:20 Comment(5)
I didn't downvote, but I believe the reason you mention why foreach is readonly isn't entirely correct.Syrian
@Steven: The reason that I mentioned is correct, if not as thoroughly described as in the linked answer.Pensive
everything you explained feels right, except for this one sentence "This is why it isn't allowed". it doesn't feel like the reason, i don't know that for sure, but it just doesn't feel like it. and Kyte's answer is more like it.Wray
@CuiPengFei: It is why it's not allowed. You are not allowed to change the actual value of the iterator variable, which is what you're doing if you set a property on a value type.Pensive
yeah, i just read it again. kyte's answer and yours are practically the same. thanksWray
S
15

Structures are value types.
Classes are reference types.

ForEach construct uses IEnumerator of IEnumerable data type elements. When that happens, then variables are read-only in the sense, that you can not modify them, and as you have value type, then you can not modify any value contained, as it shares same memory.

C# lang spec section 8.8.4 :

The iteration variable corresponds to a read-only local variable with a scope that extends over the embedded statement

To fix this use class insted of structure;

class MyStruct {
    public string Name { get; set; }
}

Edit: @CuiPengFei

If you use var implicit type, it's harder for the compiler to help you. If you would use MyStruct it would tell you in case of structure, that it is readonly. In case of classes the reference to the item is readonly, so you can not write item = null; inside loop, but you can change it's properties, that are mutable.

You can also use (if you like to use struct) :

        MyStruct[] array = new MyStruct[] { new MyStruct { Name = "1" }, new MyStruct { Name = "2" } };
        for (int index=0; index < array.Length; index++)
        {
            var item = array[index];
            item.Name = "3";
        }
Sergo answered 13/4, 2011 at 14:46 Comment(1)
thank, but how do you explain the fact that if i add a ChangeName method to MyStruct and call it in the foreach loop, it'll work just fine?Wray
B
3

NOTE: As per Adam's comment, this isn't actually the correct answer/cause of the problem. It's still something worth bearing in mind though.

From MSDN. You can't modify the values when using the Enumerator, which is essentially what foreach is doing.

Enumerators can be used to read the data in the collection, but they cannot be used to modify the underlying collection.

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined.

Blockus answered 13/4, 2011 at 14:19 Comment(4)
While it might look like it, this isn't the issue in the question. You can certainly modify the property or field values of an instance obtained through foreach (that isn't what the MSDN article is talking about), because that's modifying the member of the collection, not the collection itself. The issue here is value-type vs. reference-type semantics.Pensive
Indeed, I've just read your answer and realised the mistake in my ways. I'll leave this up as it's still kinda useful though.Blockus
thanks, but i don't think this is the reason. because i am trying to change one of the collection's element's member, not the collection itself. and if i change the MyStruct from a struct to a class, then the foreach loop will just work.Wray
CuiPengFei - Value types vs reference types. Read about them from skeet's blog.Byrd
U
1

I think the you can find the anwser below.

Foreach struct weird compile error in C#

Unpeg answered 13/4, 2011 at 14:21 Comment(0)
S
1

Value types are called by value. In short, when you evaluate the variable, a copy of it is made. Even if this was allowed, you would be editing a copy, rather than the original instance of MyStruct.

foreach is readonly in C#. For reference types (class) this doesn't change much, as only the reference is readonly, so you are still allowed to do the following:

    MyClass[] array = new MyClass[] {
        new MyClass { Name = "1" },
        new MyClass { Name = "2" }
    };
    foreach ( var item in array )
    {
        item.Name = "3";
    }

For value types however (struct), the entire object is readonly, resulting in what you are experiencing. You can't adjust the object in the foreach.

Syrian answered 13/4, 2011 at 14:34 Comment(3)
thanks, but i don't think your explanation is 100% percent right. because if i add to ChangeName method to MyStruct and call it in the foreach loop, it'll work just fine.Wray
@CuiPengFei: It compiles, but if you verify the original instances afterwards, you'll notice they haven't changed. (Due to the 'call by value' behavior of value types.)Syrian
oh, yes, sorry for the incautious comment.Wray
A
0

Make MyStruct a class (instead of struct) and you'll be able to do that.

Afton answered 13/4, 2011 at 14:20 Comment(3)
This is right based on VT (value type) vs RT (ref type) but doesn't really answer the question.Byrd
thanks, i know that'll make it work. but i am just trying to figure out the reason.Wray
@CuiPengFei: My answer explains why this is happening.Pensive
P
0

The accepted answer is absoluelty right regarding how it works. But I think not allowing a programmer to make that change is a bit of an overreach. There may very well be a good reason to want to do this. Here is mine:

     class MYClass
     {
        private List<OffsetParameters> offsetList = new List<OffsetParameters>();

        internal List<OffsetParameters> OffsetParamList
        {
            get { return this.offsetList; }
        }

where OffsetParameters is a struct (value type)

    internal struct OffsetParameters
    {
    ...

All is well - the list does not have a setter and the contents of it are of value type so they are protected from the consumer. So now I want to add a method to MyClass to update something in that struct. Here is my first thought instantiate a new list, do a foreach on the old list where I update the iteration var which is already copy. Add that copy to the new list. Assign offsetList the new list after the for each.

        internal void UpdateColors(Dictionary<string, string> nameColorMap)
        {
            if (nameColorMap == null)
                return; 

            List<OffsetParameters> newOffsetParamList = new List<OffsetParameters>();
            foreach (OffsetParameters param in offsetList)
            {
                if (nameColorMap.ContainsKey(param.offsetName))
                    param.color = nameColorMap[param.offsetName];
                newOffsetParamList.Add(param);
            }
            offsetList = newOffsetParamList;
        }

Seems legit, but because of this restriction I have to make yet another copy of that copy (inefficient). I know this does not answer the question, but it does not fit in a comment to the official answer and it adds smthng important to the picture.

My solution was to use indexer on that list instead of an enumerator just an FYI in case you are in the same boat:

        internal void ChangeOffsetGroupColors(Dictionary<string, string> nameColorMap)
        {
            if (nameColorMap == null)
                return; 

            List<OffsetParameters> newOffsetParamList = new List<OffsetParameters>();
            for (int i = 0; i < offsetList.Count; i++)
            {
                OffsetParameters param = offsetList[i];
                if (nameColorMap.ContainsKey(offsetList[i].offsetName))
                    param.color = nameColorMap[offsetList[i].offsetName];
                newOffsetParamList.Add(param);
            }
            offsetList = newOffsetParamList;
        }

I bet you List.Count gets recalculated every loop iteration :) watch out for that if you want efficiency

Pretext answered 10/1, 2022 at 18:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.