Search code examples
c#for-loopvaluetuple

Modifying list of ValueTuples vs list of integers


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.


Solution

  • 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.)