【问题标题】:Why can I not return a reference to a dictionary value?为什么我不能返回对字典值的引用?
【发布时间】:2018-06-13 00:43:15
【问题描述】:
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 不编译。 从字典或列表返回一个值作为参考有什么问题,而它确实适用于数组?

【问题讨论】:

  • 在 7.2 中为值类型引入了引用语义。在返回引用类型的情况下,为什么需要 ref
  • @MichaWiedenmann,虽然您是正确的,但您的评论肯定与问题正交?在 OP 的示例代码中将 string 更改为 int 仍然会产生相同的错误。
  • @Lander 你应该问为什么当你不能将它与其他容器一起使用时你可以将它与数组一起使用。 Jon Skeet answered 今天早些时候。
  • @Lander 索引器在具有值类型的容器上返回一个在堆栈中分配的值的副本,一旦您退出该方法,它将消失。你不能用return ref 安全地返回它。数组索引器由 CLR 本身提供,并返回对值 as shown in this answer 的引用
  • @DavidArno 在我链接到的第二个问题Indexers in List vs Arrays 中进行了解释。列表/字典索引器返回堆栈分配的副本,因此不安全。数组索引器是由 CLR 本身实现的特殊野兽,它返回对数据本身的引用,而不是数据的副本。重要的是这些引用延长了数组的生命周期,就好像它们是字段一样

标签: ref c#-7.0


【解决方案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&lt;TKey,TValue&gt;List&lt;T&gt;没有按引用返回 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&lt;TKey,TValue&gt; 和其他集合将索引器实现为属性,这会导致方法调用,因此只有在调用的方法显式指定 ref 返回时它们才能这样做。

【讨论】:

  • 谢谢,这向我解释了。一句话:我想第二个代码块应该显示一个数组。目前它与第一个代码块的代码相同。
  • 感谢您的评论,您说的完全正确。用正确的sn-p编辑代码,
  • 我想知道未来版本的 List/Dictionary 是否会包含通过引用返回值的方法,因为该语言现在支持这种方法。 ref string = ref dictionary.GetValueByRef(1);会很好。
猜你喜欢
  • 1970-01-01
  • 2020-02-11
  • 2022-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-06
  • 2011-06-28
  • 1970-01-01
相关资源
最近更新 更多