Changing the value of an element in a list of structs
Asked Answered
P

8

84

I have a list of structs and I want to change one element. For example :

MyList.Add(new MyStruct("john");
MyList.Add(new MyStruct("peter");

Now I want to change one element:

MyList[1].Name = "bob"

However, whenever I try and do this I get the following error:

Cannot modify the return value of System.Collections.Generic.List.this[int]‘ because it is not a variable

If I use a list of classes, the problem doesn't occur.

I guess the answer has to do with structs being a value type.

So, if I have a list of structs should I treat them as read-only? If I need to change elements in a list then I should use classes and not structs?

Poltroonery answered 9/9, 2008 at 10:23 Comment(0)
F
44
MyList[1] = new MyStruct("bob");

structs in C# should almost always be designed to be immutable (that is, have no way to change their internal state once they have been created).

In your case, what you want to do is to replace the entire struct in specified array index, not to try to change just a single property or field.

Funky answered 9/9, 2008 at 10:27 Comment(5)
This is not the full answer, Gishu's answer is much more complete.Insistency
What Jolson said -- It's not so much that structs are "immutable." is correct. -1 cos It is really wrong to say that structs are immutable.Erminna
To be fair to Andrew - I don't interpret that he is saying structs are "immutable" he is saying that they should be used as if they are immutable; and certainly you can make them immutable if all fields are readonly.Entertainer
You really saved my day with this structs in C# should almost always be designed to be immutable (that is, have no way to change their internal state once they have been created).. I have some nested structs in my class and changing it's values made no changes. Changing struct to class solved my problem. Thank you, bro.Artefact
Just wanted to throw out the "structs should be seen as immutable" design concept is archaic and doesn't consider plenty of valid use cases. A common use of structs is for efficiency. Copying entire structs in large struct arrays (again stored this way for efficiency) in tight loops is counter to a common purpose of structs. It is valid to treat structs as mutable. That said, the answer to this question involves using "unsafe" contexts and retrieving a pointer to the desired struct data. One could say this violates the c# "way" and one would be mistaken.Guyton
N
55

Not quite. Designing a type as class or struct shouldn't be driven by your need to store it in collections :) You should look at the 'semantics' needed

The problem you're seeing is due to value type semantics. Each value type variable/reference is a new instance. When you say

Struct obItem = MyList[1];

what happens is that a new instance of the struct is created and all members are copied one by one. So that you have a clone of MyList[1] i.e. 2 instances. Now if you modify obItem, it doesn't affect the original.

obItem.Name = "Gishu";  // MyList[1].Name still remains "peter"

Now bear with me for 2 mins here (This takes a while to gulp down.. it did for me :) If you really need structs to be stored in a collection and modified like you indicated in your question, you'll have to make your struct expose an interface (However this will result in boxing). You can then modify the actual struct via an interface reference, which refers to the boxed object.

The following code snippet illustrates what I just said above

public interface IMyStructModifier
{
    String Name { set; }
}
public struct MyStruct : IMyStructModifier ...

List<Object> obList = new List<object>();
obList.Add(new MyStruct("ABC"));
obList.Add(new MyStruct("DEF"));

MyStruct temp = (MyStruct)obList[1];
temp.Name = "Gishu";
foreach (MyStruct s in obList) // => "ABC", "DEF"
{
    Console.WriteLine(s.Name);
}

IMyStructModifier temp2 = obList[1] as IMyStructModifier;
temp2.Name = "Now Gishu";
foreach (MyStruct s in obList) // => "ABC", "Now Gishu"
{
    Console.WriteLine(s.Name);
}

HTH. Good Question.
Update: @Hath - you had me running to check if I overlooked something that simple. (It would be inconsistent if setter properties dont and methods did - the .Net universe is still balanced :)
Setter method doesn't work
obList2[1] returns a copy whose state would be modified. Original struct in list stays unmodified. So Set-via-Interface seems to be only way to do it.

List<MyStruct> obList2 = new List<MyStruct>();
obList2.Add(new MyStruct("ABC"));
obList2.Add(new MyStruct("DEF"));
obList2[1].SetName("WTH");
foreach (MyStruct s in obList2) // => "ABC", "DEF"
{
    Console.WriteLine(s.Name);
}
Navada answered 9/9, 2008 at 10:56 Comment(3)
Still no good. The list would have to be declared as the interface type, in which case all the items in it would be boxed. For every value type, there is a boxed equivalent which has class-type semantics. If you want to use a class, use a class, but then be aware of other nasty caveats thereof.Aluin
@Supercat - Like I have mentioned above... it will cause Boxing. I'm not recommending modify via interface reference - just saying that it will work if you must have a collection structs + want to modify them in-place. It's a hack.. basically you're making ref-type wrappers for value-types.Navada
Rather than using List<Object> or having the struct implement a setter interface (whoswe semantics are horrible, as noted above), an alternative is to define class SimpleHolder<T> { public T Value; } and then use a List<SimpleHolder<MyStruct>>. If Value is a field rather than a struct, one will then a statement like obList2[1].Value.Name="George"; will work just fine.Aluin
F
44
MyList[1] = new MyStruct("bob");

structs in C# should almost always be designed to be immutable (that is, have no way to change their internal state once they have been created).

In your case, what you want to do is to replace the entire struct in specified array index, not to try to change just a single property or field.

Funky answered 9/9, 2008 at 10:27 Comment(5)
This is not the full answer, Gishu's answer is much more complete.Insistency
What Jolson said -- It's not so much that structs are "immutable." is correct. -1 cos It is really wrong to say that structs are immutable.Erminna
To be fair to Andrew - I don't interpret that he is saying structs are "immutable" he is saying that they should be used as if they are immutable; and certainly you can make them immutable if all fields are readonly.Entertainer
You really saved my day with this structs in C# should almost always be designed to be immutable (that is, have no way to change their internal state once they have been created).. I have some nested structs in my class and changing it's values made no changes. Changing struct to class solved my problem. Thank you, bro.Artefact
Just wanted to throw out the "structs should be seen as immutable" design concept is archaic and doesn't consider plenty of valid use cases. A common use of structs is for efficiency. Copying entire structs in large struct arrays (again stored this way for efficiency) in tight loops is counter to a common purpose of structs. It is valid to treat structs as mutable. That said, the answer to this question involves using "unsafe" contexts and retrieving a pointer to the desired struct data. One could say this violates the c# "way" and one would be mistaken.Guyton
J
16

It's not so much that structs are "immutable."

The real underlying issue is that structs are a Value type, not a Reference type. So when you pull out a "reference" to the struct from the list, it is creating a new copy of the entire struct. So any changes you make on it are changing the copy, not the original version in the list.

Like Andrew states, you have to replace the entire struct. As that point though I think you have to ask yourself why you are using a struct in the first place (instead of a class). Make sure you aren't doing it around premature optimization concerns.

Jeaninejeanlouis answered 9/9, 2008 at 22:22 Comment(1)
Hm. Why I can modify a field of a struct allocated on the stack, also when struct is part of an array MyStruct[]? Why this works?Jair
B
13

In .Net 5.0, you can use CollectionsMarshal.AsSpan() (source, GitHub issue) to get the underlying array of a List<T> as a Span<T>. Note that items should not be added or removed from the List<T> while the Span<T> is in use.

var listOfStructs = new List<MyStruct> { new MyStruct() };
Span<MyStruct> spanOfStructs = CollectionsMarshal.AsSpan(listOfStructs);
spanOfStructs[0].Value = 42;
Assert.Equal(42, spanOfStructs[0].Value);

struct MyStruct { public int Value { get; set; } }

This works because the Span<T> indexer uses a C# 7.0 feature called ref returns. The indexer is declared with a ref T return type, which provides semantics like that of indexing into arrays, returning a reference to the actual storage location.

In comparison the List<T> indexer is not ref returning instead returning a copy of what lives at that location.

Keep in mind that this is still unsafe: if the List<T> reallocates the array, the Span<T> previously returned by CollectionsMarshal.AsSpan won't reflect any further changes to the List<T>. (Which is why the method is hidden in the System.Runtime.InteropServices.CollectionsMarshal class.)

Source

Blucher answered 5/6, 2022 at 14:1 Comment(1)
Came here to see if there was a Span solution. Awesome!Doorsill
A
7

There is nothing wrong with structs that have exposed fields, or that allow mutation via property setters. Structs which mutate themselves in response to methods or property getters, however, are dangerous because the system will allow methods or property getters to be called on temporary struct instances; if the methods or getters make changes to the struct, those changes will end up getting discarded.

Unfortunately, as you note, the collections built into .net are really feeble at exposing value-type objects contained therein. Your best bet is usually to do something like:

  MyStruct temp = myList[1];
  temp.Name = "Albert";
  myList[1] = temp;

Somewhat annoying, and not at all threadsafe. Still an improvement over a List of a class type, where doing the same thing might require:

  myList[1].Name = "Albert";

but it might also require:

  myList[1] = myList[1].Withname("Albert");

or maybe

  myClass temp = (myClass)myList[1].Clone();
  temp.Name = "Albert";
  myList[1] = temp;

or maybe some other variation. One really wouldn't be able to know unless one examined myClass as well as the other code that put things in the list. It's entirely possible that one might not be able to know whether the first form is safe without examining code in assemblies to which one does not have access. By contrast, if Name is an exposed field of MyStruct, the method I gave for updating it will work, regardless of what else MyStruct contains, or regardless of what other things may have done with myList before the code executes or what they may expect to do with it after.

Aluin answered 19/12, 2011 at 7:59 Comment(6)
You say, "…if Name is an exposed field of MyStruct, the method I gave for updating it will work…" Not exactly. Since you raised the specter of thread-safety for the reference class case, it's only fair to judge the ValueType code on the same basis, and the index positions in the list can change during your operation such that myList[1] no longer corresponds to the struct instance you fetched. To fix this you'd require some kind of locking or synchronization that's aware of the collection instance as a whole. And the class version does still suffer these same problems as well.Unmoor
@GlennSlayden: A collection that exposes items as byrefs for in-place editing could easily allow items to be edited in thread-safe fashion if all additions and deletions that will ever be done, are done before any byrefs are exposed. If necessary, a collection could be constructed to allow items to be added at the end without affecting any byrefs, but that would require that any expansion be done solely by adding new objects or arrays--certainly possible, but how most collections are implemented.Aluin
I didn't say it wasn't possible and I'd certainly know numerous ways to fix it, I just wanted to point out the race condition in your example as it stands. Maybe it's because my curious micro-expertise in this obscure area causes races to stick out like meteorites on the Antarctic glacier. As for "a collection that exposes items as byrefs for in-place editing:" Yes, exactly; I couldn't have said it better myself. Been there, done that, works great. Oh, and I almost forgot, lock-free concurrency too.Unmoor
As an aside, I originally misread your comment and thought you were hinting at a way to actually persist byrefs internally in a managed collection. A few minutes poking around convinced me that you couldn't possibly be suggesting it, so I re-read more carefully... But now I can't stop wondering: surely (ref T)[] is fundamentally impossible in .NET, right? Even C++/CLI won't allow cli::array of interior_ptr<T>--which is what it would have to be--because the latter is a non-primitive native type. So... no way, right?Unmoor
And to clarify supercat's original comment for the benefit of those who came in late, we're talking about a type of .NET collection that synthesizes managed pointers on-the-fly in order to expose its elements in-situ through a specially-designed ref return API.Unmoor
Sorry, one more note. There seem to be few (if any) use cases for the (ref T)[] idea since they're so close to (far-superior) normal GC reference handles or even boxing scenarios on the (putative) value type. It would have to a case dealing with interior_ptr relationships within arrays of jumbo and/or non-trivial value-types that shouldn't be boxed or otherwise lifted out of in-situ access. But these always seem to reduce to just storing an integer index into some TValueType[] instead, with no benefit for caching the result of the trivial—and inevitable—CLR array-indexing operation.Unmoor
F
3

In addition to the other answers, I thought it could be helpful to explain why the compiler complains.

When you call MyList[1].Name, unlike an array, the MyList[1] actually calls the indexer method behind the scenes.

Any time a method returns an instance of a struct, you're getting a copy of that struct (unless you use ref/out).

So you're getting a copy and setting the Name property on a copy, which is about to be discarded since the copy wasn't stored in a variable anywhere.

This tutorial describes what's going on in more detail (including the generated CIL code).

Freitag answered 5/7, 2019 at 2:39 Comment(0)
N
2

Since C# 10 you can use with with any struct so you can set a new value with following one-liner:

List<MyStruct> myList = new();
myList.Add(new MyStruct { Name = "john" });

myList[0] = myList[0] with { Name = "john1" }; // one-liner =)

Console.WriteLine(myList[0].Name); // prints "john1"

struct MyStruct
{
    public string Name { get; set; }
    public int Age { get; set; }
}
Newell answered 2/5, 2023 at 11:43 Comment(0)
M
1

As of C#9, I am not aware of any way to pull a struct by reference out of a generic container, including List<T>. As Jason Olson's answer said:

The real underlying issue is that structs are a Value type, not a Reference type. So when you pull out a "reference" to the struct from the list, it is creating a new copy of the entire struct. So any changes you make on it are changing the copy, not the original version in the list.

So, this can be pretty inefficient. SuperCat's answer, even though it is correct, compounds that inefficiency by copying the updated struct back into the list.

If you are interested in maximizing the performance of structs, then use an array instead of List<T>. The indexer in an array returns a reference to the struct and does not copy the entire struct out like the List<T> indexer. Also, an array is more efficient than List<T>.

If you need to grow the array over time, then create a generic class that works like List<T>, but uses arrays underneath.

There is an alternative solution. Create a class that incorporates the structure and create public methods to call the methods of that structure for the required functionality. Use a List<T> and specify the class for T. The structure may also be returned via a ref returns method or ref property that returns a reference to the structure.

The advantage of this approach is that it can be used with any generic data structure, like Dictionary<TKey, TValue>. When pulling a struct out of a Dictionary<TKey, TValue>, it also copies the struct to a new instance, just like List<T>. I suspect that this is true for all C# generic containers.

Code example:

public struct Mutable
{
   private int _x;

   public Mutable(int x)
   {
      _x = x;
   }

   public int X => _x; // Property

   public void IncrementX() { _x++; }
}

public class MutClass
{
   public Mutable Mut;
   //
   public MutClass()
   {
      Mut = new Mutable(2);
   }

   public MutClass(int x)
   {
      Mut = new Mutable(x);
   }

   public ref Mutable MutRef => ref Mut; // Property

   public ref Mutable GetMutStruct()
   {
      return ref Mut;
   }
}

private static void TestClassList()
{
   // This test method shows that a list of a class that holds a struct
   // may be used to efficiently obtain the struct by reference.
   //
   var mcList = new List<MutClass>();
   var mClass = new MutClass(1);
   mcList.Add(mClass);
   ref Mutable mutRef = ref mcList[0].MutRef;
   // Increment the x value defined in the struct.
   mutRef.IncrementX();
   // Now verify that the X values match.
   if (mutRef.X != mClass.Mut.X)
      Console.Error.WriteLine("TestClassList: Error - the X values do not match.");
   else
      Console.Error.WriteLine("TestClassList: Success - the X values match!");
}

Output on console window:

TestClassList: Success - the X values match!

For the following line:

ref Mutable mutRef = ref mcList[0].MutRef;

I initially and inadvertently left out the ref after the equal sign. The compiler didn't complain, but it did produce a copy of the struct and the test failed when it ran. After adding the ref, it ran correctly.

Mcniel answered 27/7, 2021 at 2:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.