Modifying list of ValueTuples vs list of integers

95 Views Asked by At

Having trouble wrapping my head around the concept of modifying these two different lists. So when using a regular for loop to iterate over each one, we can directly modify the list of integers, but cannot modify a specific item in the list of ValueTuples.

Consider this example

var test = new List<int>() { 1, 2, 3, 4 };

for (int i = 0; i < test.Count; i++)
{
    test[i] += 1;
}

var test2 = new List<(int, int)>() { (1, 2), (2, 3), (3, 4) };

for(int i = 0; i < test2.Count; i++)
{
    test2[i].Item1 += 1;
}

So in this, we can successfully add 1 to each integer value in the first list. However, with the second list we actually get a compiler error of CS1612 which states "Cannot modify the return value of 'List<(int, int)>.this[int]' because it is not a variable."

I read into the error on the official docs, and it makes sense that in the second example we are returning a copy of the ValueTuple, therefore we are not modifying the actual one in the list. But then why does the integer example work?

Feel like I might just be overcomplicating this, but wanted to ask here and see where I could be going wrong.

3

There are 3 best solutions below

5
Michael Liu On BEST ANSWER

To understand why test[i] += 1 compiles but test2[i].Item1 += 1 doesn't, let's examine how the C# compiler transforms them into simpler statements.

test[i] += 1 is transformed as follows:

var x = test.get_Item(i); // Make a copy of test[i].
var y = x + 1;
test.set_Item(i, y); // Replace test[i] with y.

// Or more concisely:
test.set_Item(i, test.get_Item(i) + 1);

The get_Item and set_Item methods refer to the get and set accessors of List<T>'s indexer. I'm using get_Item and set_Item here instead of test[i] to clarify whether test[i] refers to a get or a set.

test2[i].Item1 += 1 is transformed as follows:

var tuple = test2.get_Item(i); // Make a copy of test2[i].
var x = tuple.Item1;
var y = x + 1;
tuple.Item1 = y; // Mutate the copy. NOT ALLOWED (CS1612)

// Or more concisely:
var tuple = test2.get_Item(i);
tuple.Item1 = tuple.Item1 + 1;

Notice two important points:

  1. With integers, there's a call to set_Item. With ValueTuple, there isn't.
  2. With ValueTuple, the assignment to Item1 occurs on a temporary copy of test2[i], so it ultimately has no observable effect. This is why the compiler reports error CS1612: to prevent you from writing code that doesn't do what you think it does.

You can get your code to compile in a couple ways:

  1. Use an array instead of List. If test2 were an array, then test2[i] would be a mutable reference to the element at index i, not a copy. Array indexers are special in this regard.

  2. Replace the entire element test2[i]:

    var tuple = test2[i]; // test2.get_Item(i)
    tuple.Item1 += 1;
    test2[i] = tuple; // test2.set_Item(i, tuple)
    

    The last statement invokes set_Item because the left-hand side of the assignment is an indexer expression and nothing else. (That's just the way the C# language works.)

0
Ken On

The Microsoft docs explain this behavior here:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs1612

This code works:

var test2 = new List<(int, int)>() { (1, 2), (2, 3), (3, 4) };
    
for(int i = 0; i < test2.Count; i++)
{
   var x = test2[i];

   x.Item1 += 1;

   test2[i] = x;

}    
0
Guru Stron On

Basically because the specification says so. From Structs: 16.4.4 Assignment section:

Assignment to a variable of a struct type creates a copy of the value being assigned. This differs from assignment to a variable of a class type, which copies the reference but not the object identified by the reference.

Similar to an assignment, when a struct is passed as a value parameter or returned as the result of a function member, a copy of the struct is created. A struct may be passed by reference to a function member using a ref or out parameter.

When a property or indexer of a struct is the target of an assignment, the instance expression associated with the property or indexer access shall be classified as a variable. If the instance expression is classified as a value, a compile-time error occurs. This is described in further detail in §12.21.2.

Basically the instance expression associated with the indexer access - test[i] (of test[i] += 1) is classified as a variable , i.e. something like:

var foo = test[i];
foo += 1;
test[i] = foo;

While the second one due to property access is classified by compiler as value due to the field access (Item1).

Other than that I could only guess why it was designed this way. My guess would be that some corner-cases and overcomplication of the compiler to handle this case could be a factor.