简体   繁体   中英

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

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?

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.

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.

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

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.

使MyStruct成为一个类(而不是struct),你就能做到这一点。

I think the you can find the anwser below.

Foreach struct weird compile error in C#

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM