Search code examples
c#dictionaryreferencerefout

How can I update a reference returned from TryGetValue after its been inserted into a collection?


I have a dictionary inside a dictionary. I'd like to set a reference to the inner dictionary to a value after I'd added it to the outer dictionary as such:

var mammalIdSubscribers = new Dictionary<int, Dictionary<Guid, int>>();
        
var mammalId = 0;
if (!mammalIdSubscribers.TryGetValue(mammalId, out var mammalSubscribers))
    mammalIdSubscribers[mammalId] = mammalSubscribers; // Add reference to inner dict to outer dict
        
Subscribe(ref mammalSubscribers);

/* 
mammalIdSubscribers[mammalId] is still null after the call 
to Subscribe despite mammalSubscribers being non-null. Why?
*/
        
static void Subscribe(ref Dictionary<Guid, int> subscribers)
{
    subscribers = new Dictionary<Guid, int> { { Guid.NewGuid(), 10 } };
}

Unfortunately, this doesn't work and I'm not sure why ( Console.WriteLine(mammalSubscribers.First().Value); throws a null reference exception).

Can someone please explain why this doesn't work? In other words, why is mammalIdSubscribers[0] still null after the call to Subscribe with the ref keyword?


Solution

  • Your variables, mammalIdSubscribers and mammalSubscribers, are very similarly named, so for the sake of clarity I'll rename mammalIdSubscribers to "outerDict" or maybe "biggerDict" while mammalSubscribers is renamed to "encarta", because I used that as a reference a lot as a sprog.

    Line-by-line...

    1. var biggerDict = new Dictionary<int, Dictionary<Guid, int>>();
      • This gives us a valid, but empty, biggerDict dict.
    2. var mammalId = 0;
      • Self-explanatory.
    3. biggerDict.TryGetValue(mammalId, out var encarta)
      • This evaluates to false. The out param is also an inline out declaration, and when you use inline out declarations with Dictionary's TryGetValue then the new variable will be null (or default) when it returns false.
      • ...and it will return false because biggerDict is empty, as established earlier.
      • ...therefore encarta is null.
      • (In case you blinked and missed it: encarta is a new GC reference-type variable on the stack, it is not an alias or "reference" to any part of biggerDict).
    4. Because the TryGetValue call is inside an if( !TryGetValue(...) ) statement it means that biggerDict[mammalId] = encarta; will be evaluated.
      • and encarta is still null.
      • ...therefore biggerDict[mammalId] (aka biggerDict[0]) is null .
    • Subscribe(ref encarta);
      • This passes a reference to the local variable encarta to Subscribe.
      • Crucially, encarta is not a reference to any slot or space within biggerDict: it's still just a stack-allocated (aka automatic) object-reference-sized slot that's still null.
    1. encarta = new Dictionary<Guid, int> { { Guid.NewGuid(), 10 } };
      • Inside Subscribe, at the machine-language level, a pointer(-ish) to the stack-allocated encarta local is deferenced and assigned to that new Dictionary<Guid, int> { { Guid.NewGuid(), 10 } };.
      • ...which means encarta is now not null.
      • Execution then returns to the previous function.
    2. The encarta local is now a reference to that valid dictionary object on the GC heap. But nothing ever invoked the biggerDict[int].set_Item property setter to make biggerDict[0] a non-null reference to the same object that encarta points to.
    3. Remember, excepting for real arrays (T[]), all other types with indexers are just sugar over property getter/setter methods, which means object references are passed by value, and not references-passed-by-reference - at least not without a ref-returning property, which Dictionary<K,V> does not do.