Why the tuple-type list element's value cannot be modified?

Viewed 870

In C# 8.0, I can modify the value inside a tuple directly by accessing the field name:

(string name, int score) student = ("Tom", 100);
student.name = "Jack";
Console.WriteLine(student);

And I can modify the list element's property as follow:

var list = new List<Student>();  // assume I have a Student class which has a Name property
list.Add(new Student { Name = "Tom" });
list[0].Name = "Jack";
Console.WriteLine(list[0]);

But why can't I modify the tuple-type element's value like this?

var list = new List<(string name, int score)>();
list.Add(("Tom", 100));
list[0].name = "Jack"; // Error!
Console.WriteLine(list[0]);
2 Answers

A tuple (ValueTuple) is a struct. Rather than returning a reference to the value as is the case with your Student example, you would actually recieve a copy of the tuple.

Changes to that copy wouldn't be reflected in the list and would be discarded. The compiler is smart enough to recognize this and stops you from doing it.

If it did compile, it would be to something similar to the following:

var list = new List<(string name, int score)>(); list.Add(("Tom", 100));
var copy = list[0];
copy.name = "Jack"; 
Console.WriteLine(copy.name);    // Jack
Console.WriteLine(list[0].name); // still Tom

Mutable structs can be dangerous if you don't use them properly. The compiler is simply doing its job.

You can work around this with the following:

var list = new List<(string name, int score)>(); list.Add(("Tom", 100));
var copy = list[0];
copy.name = "Jack"; 
list[0] = copy;                  // put it back
Console.WriteLine(copy.name);    // Jack
Console.WriteLine(list[0].name); // Jack

Try It Online

If you use an array (string, int)[] instead of a List<(string, int)>, this isn't a problem due to the way array element access works:

var arr = new (string name, int score) [] { ( "Tom", 10 ) };
arr[0].name = "Jack";
Console.WriteLine(arr[0].name); // Jack

Try It Online

This behavior is not unique to List or your tuple type. You'll experience this issue with any collection where the element is a Value Type (unless of course they offer a ref element accessor).

Note that there are similar issues when having a readonly field of a mutable value type that mutates via method calls. This can be much more insidious as no error or warning is emitted:

struct MutableStruct { 
   public int Val; 
   public void Mutate(int newVal) {
     Val = newVal;
   } 
} 
class Test {
    private readonly MutableStruct msReadonly;
    private MutableStruct msNormal;
    public Test() {
      msNormal = msReadonly = new MutableStruct(){ Val=5 };
    } 
    public void MutateReadonly() {
         Console.WriteLine(msReadonly.Val); // 5
         msReadonly.Mutate(66);             // defensive copy! 
         Console.WriteLine(msReadonly.Val); // still 5!!! 
    } 
    public void MutateNormal() {
         Console.WriteLine(msNormal.Val);   // 5
         msNormal.Mutate(66);
         Console.WriteLine(msNormal.Val);   // 66
    } 
}
new Test().MutateReadonly();
new Test().MutateNormal();

Try It Online

ValueTuple is a great addition to the framework and language. But there's a reason you'll often hear that [Mutable] structs are evil. In the majority of cases you shouldn't hit these restrictions. If you find yourself falling into this pattern a lot, I suggest moving over to a record, which is a reference type (thus not suffering these issues) and can be reduced to a tuple-like syntax.

Mutable value types are evil, it's hard to see why this prints "Tom" not "Jack":

(string name, int score) student = ("Tom", 100);
(string name, int score) student2 = student;
student.name = "Jack";
Console.WriteLine(student2);

The reason is that you always create a copy. Because it's not obvious you should avoid mutable value types. To avoid that people will fall into that trap the compiler just allows to modify the object directly via properties(like above). But if you try to do it via a method call you get a compiler error "Cannot modify the return value of ... because it is not a variable".

So this is not allowed:

list[0].name = "Jack";

It would create a new copy of the ValueTuple, assigns a value but doesn't use or store it anywhere.

This compiles because you assign it to a new variable and modify it via property:

(string name, int score) x = list[0];
x.name = "Jack"; // Compiles 

So it compiles but gives you again a suprising result:

Console.WriteLine(list[0]);  // Tom

Read more about it here: Do Not Define Mutable Value Types

Related