为什么我不能 return 对字典值的引用?

Why can I not return a reference to a dictionary value?

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 不能。 从字典或列表中返回一个值作为引用有什么问题,而它确实适用于数组?

我想将我的答案添加到 'pot',也许这会让事情更清楚一些。 那么,为什么这不适用于列表和字典呢?好吧,如果你有一段这样的代码:

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])所做的实际上是一个三步过程:调用方法,将return值存储在局部变量中然后 returning 存储在该局部变量中的值。并且,正如其他人提供的链接所指出的那样,return 引用局部变量是不可能的(除非局部变量是 ref 局部变量,但正如其他人再次指出的那样,因为 Diciotionary<TKey,TValue>List<T> 没有引用 return 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-native 调用,而不是 属性(即方法)调用。如果你在 MSDN here 上查看所有可能的 ldelem 变体,你会发现有一种叫做 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 returns.

时执行此操作