83

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?

Rohit Vipin Mathews
  • 11,629
  • 15
  • 57
  • 112
Darren
  • 933
  • 1
  • 7
  • 9

8 Answers8

53

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);
}
kruoli
  • 20
  • 1
  • 1
  • 5
Gishu
  • 134,492
  • 47
  • 225
  • 308
  • 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. – supercat Jan 05 '12 at 23:48
  • @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. – Gishu Jan 06 '12 at 05:53
  • 2
    Rather than using `List` or having the struct implement a setter interface (whoswe semantics are horrible, as noted above), an alternative is to define `class SimpleHolder { public T Value; }` and then use a `List>`. If `Value` is a field rather than a struct, one will then a statement like `obList2[1].Value.Name="George";` will work just fine. – supercat Nov 25 '13 at 20:27
43
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.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
Andrew
  • 2,810
  • 2
  • 27
  • 14
  • 3
    This is not the full answer, Gishu's answer is much more complete. – Motti Oct 06 '08 at 19:50
  • 3
    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. – Gurucharan Balakuntla Maheshku Mar 17 '11 at 09:44
  • 4
    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. – Montdidier May 03 '12 at 01:52
  • 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. – Vlad Nov 26 '15 at 22:03
  • 2
    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. – Slight Apr 10 '21 at 19:45
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.

Jason Olson
  • 3,616
  • 2
  • 21
  • 24
  • 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? – Kirill Kobelev Dec 31 '19 at 08:07
11

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

DaemonFire
  • 573
  • 6
  • 13
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.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • 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. – Glenn Slayden Nov 25 '17 at 04:38
  • @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. – supercat Nov 25 '17 at 14:53
  • 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. – Glenn Slayden Nov 25 '17 at 20:21
  • 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`--which is what it would have to be--because the latter is a non-primitive native type. So... no way, right? – Glenn Slayden Nov 25 '17 at 20:43
  • 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. – Glenn Slayden Nov 25 '17 at 20:49
  • 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. – Glenn Slayden Nov 25 '17 at 21:17
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).

David Klempfner
  • 8,700
  • 20
  • 73
  • 153
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; }
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
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.

Bob Bryan
  • 3,687
  • 1
  • 32
  • 45