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?
CodePudding user response:
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...
var biggerDict = new Dictionary<int, Dictionary<Guid, int>>();- This gives us a valid, but empty,
biggerDictdict.
- This gives us a valid, but empty,
var mammalId = 0;- Self-explanatory.
biggerDict.TryGetValue(mammalId, out var encarta)- This evaluates to
false. Theoutparam is also an inlineoutdeclaration, and when you use inlineoutdeclarations withDictionary'sTryGetValuethen the new variable will benull(ordefault) when it returnsfalse. - ...and it will return
falsebecausebiggerDictis empty, as established earlier. - ...therefore
encartaisnull. - (In case you blinked and missed it:
encartais a new GC reference-type variable on the stack, it is not an alias or "reference" to any part ofbiggerDict).
- This evaluates to
- Because the
TryGetValuecall is inside anif( !TryGetValue(...) )statement it means thatbiggerDict[mammalId] = encarta;will be evaluated.- and
encartais stillnull. - ...therefore
biggerDict[mammalId](akabiggerDict[0]) isnull.
- and
Subscribe(ref encarta);- This passes a reference to the local variable
encartatoSubscribe. - Crucially,
encartais not a reference to any slot or space withinbiggerDict: it's still just a stack-allocated (aka automatic) object-reference-sized slot that's stillnull.
- This passes a reference to the local variable
encarta = new Dictionary<Guid, int> { { Guid.NewGuid(), 10 } };- Inside
Subscribe, at the machine-language level, a pointer(-ish) to the stack-allocatedencartalocal is deferenced and assigned to thatnew Dictionary<Guid, int> { { Guid.NewGuid(), 10 } };. - ...which means
encartais now notnull. - Execution then returns to the previous function.
- Inside
- The
encartalocal is now a reference to that valid dictionary object on the GC heap. But nothing ever invoked thebiggerDict[int].set_Itemproperty setter to makebiggerDict[0]a non-nullreference to the same object thatencartapoints to. - 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 aref-returning property, whichDictionary<K,V>does not do.
