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,
biggerDict
dict.
- This gives us a valid, but empty,
var mammalId = 0;
- Self-explanatory.
biggerDict.TryGetValue(mammalId, out var encarta)
- This evaluates to
false
. Theout
param is also an inlineout
declaration, and when you use inlineout
declarations withDictionary
'sTryGetValue
then the new variable will benull
(ordefault
) when it returnsfalse
. - ...and it will return
false
becausebiggerDict
is empty, as established earlier. - ...therefore
encarta
isnull
. - (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 ofbiggerDict
).
- This evaluates to
- Because the
TryGetValue
call is inside anif( !TryGetValue(...) )
statement it means thatbiggerDict[mammalId] = encarta;
will be evaluated.- and
encarta
is stillnull
. - ...therefore
biggerDict[mammalId]
(akabiggerDict[0]
) isnull
.
- and
Subscribe(ref encarta);
- This passes a reference to the local variable
encarta
toSubscribe
. - Crucially,
encarta
is 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-allocatedencarta
local is deferenced and assigned to thatnew Dictionary<Guid, int> { { Guid.NewGuid(), 10 } };
. - ...which means
encarta
is now notnull
. - Execution then returns to the previous function.
- Inside
- The
encarta
local is now a reference to that valid dictionary object on the GC heap. But nothing ever invoked thebiggerDict[int].set_Item
property setter to makebiggerDict[0]
a non-null
reference to the same object thatencarta
points 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.