由 ref 返回的值类型变量在哪里存在?堆栈还是堆?

Posted

技术标签:

【中文标题】由 ref 返回的值类型变量在哪里存在?堆栈还是堆?【英文标题】:Where does a value type-variable - which is returned by ref - live? Stack or heap? 【发布时间】:2018-08-27 17:45:37 【问题描述】:

我最近听说了 7.2 中的新 C# 功能,因此我们现在可以返回值类型的引用(例如 int),甚至可以返回值类型的只读引用。据我所知,值类型存储在堆栈中。当方法离开时,它们会从堆栈中删除。那么当 GetX 方法退出时 int 会发生什么?

private ref int GetX()

    // myInt is living on the stack now right?
    int myInt = 5;

    return ref myInt;


private void CallGetX()

    ref int returnedReference = ref GetX();
    // where does the target of 'returnedReference' live now? 
    // Is it somehow moved to the heap, because the stack of 'GetX' was removed right?

我收到了错误

错误 CS8168:无法通过引用返回本地“myInt”,因为它不是本地引用 (11, 24)

那么为什么它不起作用?难道仅仅因为变量不能移动到堆中就不起作用吗?这是问题吗?如果它们不在堆栈中,我们是否只能通过引用返回值类型?我知道这是两个问题。

首先:ref 返回的值类型变量在哪里?堆栈还是堆? (我猜在堆上,但为什么)?

第二:为什么栈上创建的值类型不能通过引用返回?

所以这是可以编译的:

private int _myInt;

private ref int GetX()

    // myInt is living on the stack now right?
    _myInt = 5;

    return ref _myInt;


private void CallGetX()

    ref int returnedReference = ref GetX();
    // where does the target of 'returnedReference' live now? 
    // Is it somehow moved to the heap? becase the stack of 'GetX' was removed right?

如果我理解你的 cmets 是因为现在 _myInt 不在方法 GetX 中,因此没有在堆栈中创建,对吗?

【问题讨论】:

堆与栈只是实现细节,与问题无关 “据我所知,值类型存储在堆栈中。” - 错误。 来自microsoft docs:返回值的生命周期必须超出方法的执行范围。换句话说,它不能是返回它的方法中的局部变量。它可以是类的实例或静态字段,也可以是传递给方法的参数。尝试返回局部变量会生成编译器错误 CS8168,“无法通过引用返回本地 'obj',因为它不是本地引用。” 新的ref 语法允许运行时使用指针。指针可以生成更高效的代码,它们可以避免复制值,但它们很危险。使用指向不再有效的内存位置的指针是一个非常传统的错误。 C# 编译器检查这些错误,它可以告诉方法返回后局部变量不再存在。因此将其标记为错误代码。 en.wikipedia.org/wiki/Dangling_pointer 堆栈是一个实现细节,作者 Eric Lippert,blogs.msdn.microsoft.com/ericlippert/2009/04/27/… 【参考方案1】:

据我所知,值类型存储在堆栈中。

因此是您困惑的基础;这是一个非常不准确的简化。结构可以存在于堆栈中,但它们也可以存在:

作为堆上对象的字段 作为 other 结构上的字段,这些结构是(等)堆上对象上的字段 在堆上装箱(直接,或通过上述任一方式) 在非托管内存中

不过,您是对的:如果您将方法的ref return out 传递给方法的本地inside,您将违反堆栈完整性。这正是该场景不允许的原因:

ref int RefLocal()

    int i = 42;
    return ref i;
    // Error CS8168  Cannot return local 'i' by reference because it is not a ref local

一些场景,编译器可以证明即使它是作为本地存储的,生命周期的范围也仅限于此方法;它有助于您不能重新分配 ref 本地(老实说,此检查是此限制的关键原因);这允许:

ref int RefParamViaLoval(ref int arg)

    ref int local = ref arg;
    return ref local;

由于ref int arg 的生命周期不在方法的范围内,我们的ref int local 可以在赋值中继承此生命周期。


那么可以我们有用地返回什么?

它可能是对数组内部的引用

ref int RefArray(int[] values)

    return ref values[42];

它可能是对象上的一个字段(不是属性):

ref int ObjFieldRef(MyClass obj)

    return ref obj.SomeField;

它可能是结构上的一个字段(不是属性)通过引用传入

ref int StructFieldRef(ref MyStruct obj)

    return ref obj.SomeField;

它可能是从后续调用中获得的只要调用不涉及任何已知指向本地的 ref 本地(这将无法证明有效性):

ref int OnwardCallRef()

    ref MyStruct obj = ref GetMyStructRef();
    return ref obj.SomeField;

这里再次注意,local 的生命周期继承了传入 onward 调用的任何参数的生命周期;如果后续调用涉及 ref-local 具有受限生命周期,则 result 将继承该受限生命周期,您将无法返回它。 p>

例如,后续调用可能是调用非托管内存中保存的结构:

ref int UnmanagedRef(int offset)

    return ref Unsafe.AsRef<int>(ptr + offset);

所以:许多非常有效和有用的场景不涉及对当前堆栈帧的引用。

【讨论】:

感谢您的出色回答。我学到了很多关于这个话题的知识。但是由于@Evk 提到了我显然理解错误的堆栈/堆问题,所以我选择了他的问题作为正确答案。还剩一件事。为什么我不能通过引用返回属性(int)?是不是因为它实际上是一个get_method,你无法知道是返回一个字段还是一个局部变量? @Daniel 一个“get”方法不是ref intget 返回堆栈上值的副本,因此:它不能是对基础价值的引用。可能实际存在也可能不存在 - 它可能是一个计算属性:public int Area =&gt; Width * Height; 感谢您的解释。这是有道理的!【参考方案2】:

我觉得您已经了解自己为什么它不起作用。您不能通过方法引用返回 局部变量(除非它是 ref local),因为在大多数情况下,局部变量的生命周期就是方法,因此它在方法之外的引用没有任何意义(在方法之外这个变量是死的,它之前的位置可能包含任何东西)。正如文档所述:

返回值的生命周期必须超出 方法的执行。换句话说,它不能是局部变量 在返回它的方法中

在实践中,一些局部变量的寿命可能比它们声明的方法的执行时间更长。例如,闭包捕获的变量:

int myLocal = 5;
SomeMethodWhichAcceptsDelegate(() => DoStuff(myLocal));
return ref myLocal;

但是,这会引入额外的复杂性而没有任何好处,因此这也是被禁止的,即使 myLocal 的生命周期可能比包含方法长得多。

最好不要从堆栈和堆的角度考虑它。例如,您可能认为您无法通过ref return 从方法返回对堆栈上分配的内容的引用。这不是真的,例如:

private void Test() 
    int myLocal = 4;
    GetX(ref myLocal);       


private ref int GetX(ref int i)             
    return ref i;

这里myLocal 显然在堆栈上,我们通过引用GetX 来传递它,然后用return ref 返回这个(堆栈分配的)变量。

所以只需考虑变量生命周期而不是堆栈\堆。

在您的第二个示例中,_myInt 字段的生命周期显然比 GetX 的执行时间长,因此通过引用返回它没有问题。

另请注意,无论您使用return ref 返回值类型还是引用类型,在此问题的上下文中都没有任何区别。

【讨论】:

以上是关于由 ref 返回的值类型变量在哪里存在?堆栈还是堆?的主要内容,如果未能解决你的问题,请参考以下文章

JVM的堆内存和栈内存中存储的数值或类型的区别在哪里

Java 基本数据类型作为局部变量存储在哪

js中的简单数据类型和复杂数据类型的存储

java堆栈的理解

js堆栈的理解

Java堆,栈,堆栈