Stackoverflow Adventures: Why can I not return a reference to a dictionary value?

Akos Nagy
Apr 11, 2018

A while back I found a question on SO about returning a reference to a dictionary value. Here's the code sample from the question that also explains the question itself:

public class PropertyManager
{
  private Dictionary<ElementPropertyKey, string> _values = new Dictionary<ElementPropertyKey, string>();
  private string[] _values2 = new string[1];
  private List<string> _values3 = new List<string>();
  public PropertyManager()
  {
    _values[new ElementPropertyKey(5, 10, "Property1")] = "Value1";
    _values2[0] = "Value2";
    _values3.Add("Value3");
  }
  
  public ref string GetPropertyValue(ElementPropertyKey key)
  {
  // Does not compile
  // An expression cannot be used in this context because it may not be returned by reference.         
   return ref _values[key]; 
  }

  public ref string GetPropertyValue2(ElementPropertyKey key)
  {
    return ref _values2[0]; //Compiles
  }

  public ref string GetPropertyValue3(ElementPropertyKey key)
  {
   // Does not compile. 
   // An expression cannot be used in this context because it may not be returned by reference.
    return ref _values3[0];
  }
}

In my last post I touched on the subject of ref returns, so I thought I would add my answer here as a blog post for the returning readers (and to make it seem like that I publish content very often).

So, why doesn't that work for lists and dictionaries? Well, if you have a piece of code like this:

static string Test()
{
  Dictionary<int, string> s = new Dictionary<int, string>();
  return s[0];
}

This (in debug mode) translates to this IL code:

IL_0000: nop
IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<int32, string>::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.0
IL_0009: callvirt instance !1 class [mscorlib]System.Collections.Generic.Dictionary`2<int32, string>::get_Item(!0)
IL_000e: stloc.1
IL_000f: br.s IL_0011

IL_0011: ldloc.1
IL_0012: ret

This, in turn means that what you do with one line of code (return s[0]) is actually a three-step process: calling the method, storing the return value in a local variable and then returning the value that is stored in that local variable. And returning a local variable by reference is not possible (unless the local variable is a ref local variable, but since Dicitionary<TKey,TValue> and List<T> does not have a by-reference return API, this is not possible either).

And now, why does it work for the array? If you look at how array-indexing is handled more closely (i.e. on IL-code level), you can see that there is no method call for array indexing. Instead, a special opcode is added to the code called ldelem (or some variant of that). A code like this:

static string Test()
{
  string[] s = new string[2];
  return s[0];
}

translates to this in IL:

IL_0000: nop
IL_0001: ldc.i4.2
IL_0002: newarr [mscorlib]System.String
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: ldc.i4.0
IL_000a: ldelem.ref
IL_000b: stloc.1
IL_000c: br.s IL_000e

IL_000e: ldloc.1
IL_000f: ret

Of course this looks like the same as it was for the dictionary, but I think the key difference is that the indexer here generates an IL-native call, not a property (i.e. method) call. And if you look at all the possible ldelem variants on MSDN here, you can see that there is one called ldelema which can load the address of the element directly to the heap. And indeed, if you write a piece of code like this:

static ref string Test()
{
  string[] s = new string[2];
  return ref s[0];
}

This translates to the following IL code, utilizing the direct-reference loading ldelema opcode:

IL_0000: nop
IL_0001: ldc.i4.2
IL_0002: newarr [mscorlib]System.String
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: ldc.i4.0
IL_000a: ldelema [mscorlib]System.String
IL_000f: stloc.1
IL_0010: br.s IL_0012

IL_0012: ldloc.1
IL_0013: ret

So basically, array indexers are different and under the hood, arrays have support for loading an element by reference to the evaluation stack via native IL calls. Since the Dictionary<TKey,TValue> and other collections implement indexers as properties, which result in method calls, they can only do this if the method called explicitly specifies ref returns.

Akos Nagy
Posted in C# Stack Overflow