Почему я не могу вернуть ссылку на значение словаря?

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)
    {
        return ref _values[key]; //Does not compile. Error: An expression cannot be used in this context because it may not be returned by reference.         
    }

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

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

В приведенном выше примере GetPropertyValue2 компилируется, а GetPropertyValue и GetPropertyValue3 нет. Что плохого в возврате значения из словаря или списка в качестве ссылки, хотя оно работает для массива?

1 ответ

Решение

Я хотел бы добавить свой ответ в "банк", возможно, это делает вещи немного более ясными. Итак, почему это не работает для списков и словарей? Хорошо, если у вас есть такой код:

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

Это (в режиме отладки) переводится в этот код IL:

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

Это, в свою очередь, означает, что вы делаете с одной строкой кода (return s[0]) на самом деле представляет собой трехэтапный процесс: вызов метода, сохранение возвращаемого значения в локальной переменной, а затем возвращение значения, хранящегося в этой локальной переменной. И, как указано в ссылках, предоставленных другими, возврат локальной переменной по ссылке невозможен (если только локальная переменная не является локальной переменной ref, но, как еще раз указали другие, поскольку Diciotionary<TKey,TValue> а также List<T> не имеет API возврата по ссылке, это также невозможно).

А теперь, почему это работает для массива? Если вы посмотрите на то, как индексирование массивов обрабатывается более тщательно (т. Е. На уровне IL-кода), вы увидите, что для индексации массивов нет вызова метода. Вместо этого к коду добавляется специальный код операции, называемый ldelem (или какой-то другой вариант). Код вроде этого:

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

переводит на это в 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

Конечно, это выглядит так же, как и для словаря, но я думаю, что ключевое отличие состоит в том, что индексатор здесь генерирует собственный вызов IL, а не вызов свойства (то есть метода). И если вы посмотрите здесь на все возможные варианты ldelem в MSDN, то увидите, что есть один, называемый ldelema, который может загружать адрес элемента непосредственно в кучу. И действительно, если вы напишите кусок кода, как это:

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

Это приводит к следующему IL-коду, использующему код операции ldelema загрузки с прямой ссылкой:

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

Таким образом, в основном, индексаторы массивов различны, и под капотом массивы поддерживают загрузку элемента по ссылке на стек оценки с помощью собственных вызовов IL. Так как Dictionary<TKey,TValue> и другие коллекции реализуют индексаторы как свойства, которые приводят к вызовам методов, они могут делать это только в том случае, если вызываемый метод явно указывает возврат ref.

Другие вопросы по тегам