5

I have a list of tuples that I want to modify.

    var myList = new List<(int head, int[] tail)> {
        (0, new[] { 1, 2, 3 }),
        (7, new[] { 8, 9 }   ), };
    var myArray = new (int head, int[] tail)[] {
        (0, new[] { 1, 2, 3 }),
        (7, new[] { 8, 9 }   ), };

    // ...

    // ref var firstElement = ref myList[0];
    ref var firstElement = ref myArray[0];
    firstElement.head = 99;

This works perfectly for arrays, but not for lists. I understand that this is because the indexer doesn't do a ref return; but is there some other way that lists can return their elements by reference, so that I can modify the returned tuple?

This will cause a compiler error "A property or indexer may not be passed as an out or ref parameter":

ref var firstElementList = ref myList[0];
Tim Schmelter
  • 450,073
  • 74
  • 686
  • 939
Steinbitglis
  • 2,482
  • 2
  • 27
  • 40
  • 2
    I recommend to use a reference type for storing your data in a list. I understand that ValueTuples are comfortable to use here, but I don't see a way to achieve your goal by using ValueTuples. – Erik T. Jul 22 '19 at 11:35
  • 2
    Lists are inherently unable to do this (safely) because they allow dynamic sizing, and hence effectively invalidating your reference. If a list decided to implement this, you could end up with highly surprising results. – Jeroen Mostert Jul 22 '19 at 11:36
  • 1
    I don't plan to keep the reference around... I just want to modify the element. – Steinbitglis Jul 22 '19 at 11:38
  • Then you'll have to modify it by index, and pass that around (or an unsafe pointer, if you *really* insisted). I understand you *intend* to use it only in a safe manner, but `List` still can't give you what you want even if it wanted to -- if only because C# has no notion of specialization by value type. You could write a custom type to achieve this (and promise to only use it safely) but it would rarely be worthwhile to do so. – Jeroen Mostert Jul 22 '19 at 11:46
  • I definitely don't expect the reference to be an index. And if the internal array changes, I don't expect my element to refer to the new array... but I want my element to linger until it goes out of scope. The element could be a normal instance of a class, then it doesn't matter what list it was or wasn't stored in. – Steinbitglis Jul 22 '19 at 11:50
  • 1
    But it's not a class; `ValueTuple` is a `struct`, and so to the runtime it matters very much where it was stored. `ref` is not a way to produce generic references to anything, it's a way to safely emit code that takes inner pointers. Very interesting in certain performance scenarios; not something you use in general, and certainly not with a `List`. – Jeroen Mostert Jul 22 '19 at 11:55
  • Well, tuples are very convenient for a lot of things. So are lists. I might be abusing tuples a bit, but they really do a lot of good things. I have to be a bit mindful so I don't end up copying large structs around everywhere. If I could do this operation, I wouldn't have to do an additional copy operation. – Steinbitglis Jul 22 '19 at 12:03
  • 1
    If you really want to, you can implement your own `ValueList where T : struct` that has a `ref T this[int index]`. Of course this type would be "unsafe" in that dynamic resizing invalidates all current references, but you could promise to only use it safely. You could also write a wrapper that grabs the inner array of `List` with reflection, though that is very, very ugly indeed. – Jeroen Mostert Jul 22 '19 at 12:04

2 Answers2

3

As I understand it happens because compiler is aware of array and doesn't call indexer for it. While for list it calls indexer and for indexer you cannot use ref(without ref in indexer signature), according to MSDN.

For this

 var firstElement = myArray[0];

 firstElement.head = 99;

Ildasm shows this

ldelem     valuetype [System.Runtime]System.ValueTuple`2<int32,int32[]>

MSDN

I.e. arrays are supported on IL level.

While for list it calls indexer.

 callvirt   instance !0 class [System.Collections]System.Collections.Generic.List`1<valuetype [System.Runtime]System.ValueTuple`2<int32,int32[]>>::get_Item(int32)

And for indexer it works if you put ref to the signature.

E.g.(it is only for demonstration purposes; yep, there should be array instead of single variable, etc, but just to get it compilable)

class Program
{
    static void Main(string[] args)
    {
        var myList = new MyList<(int head, int[] tail)> {
    (0, new[] { 1, 2, 3 }),
    (7, new[] { 8, 9 }   ), };

        ref var firstElement = ref myList[0];
        firstElement.head = 99;
        Console.WriteLine("Hello World!");
    }
}

public class MyList<T> : IEnumerable
{
    private T value;
    public ref T this[int index]
    {
        get
        {
             return ref value;
        }
    }

    public void Add(T i)
    {
        value = i;
    }

    public IEnumerator GetEnumerator() => throw new NotImplementedException();
}

P.S. But when you start implementing your own list implementation(as array list) you will probable notice that it is not worth having ref indexer - imagine you resized the array - created the new one and copied all the data; it means someone might hold non actual reference.

P.P.S Going further, say we creating linked list - nothing wrong will happen on just resize, but imagine we removed an element someone is holding a reference to - it is not possible to understand it doesn't belong to the list any longer.

So, yes, I think they intentionally made List indexer non ref since it doesn't seem to be a good idea having ref return for something that can change.

Viktor Arsanov
  • 1,285
  • 1
  • 9
  • 17
1

You can do this using WeakReference and changing ValueTuple to class:

List<MyClass> myList = new List<MyClass> {
    new MyClass { Head = 0, Tail = new[] { 1, 2, 3 } },
    new MyClass { Head = 7, Tail = new[] { 8, 9 } } };
var firstElement = new WeakReference(myList[0]);
MyClass reference = firstElement.Target as MyClass;
reference.Head = 99;
firstElement.Target = new MyClass { Head = 99, Tail = reference.Tail};
Console.WriteLine(myList[0].Head);

You can try this code here.

Josh Gallagher
  • 5,211
  • 2
  • 33
  • 60
Lemm
  • 196
  • 1
  • 9