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

5

我最近听说了C# 7.2中的新功能,现在我们可以返回值类型的引用(例如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”,因为它不是 ref local(11,24)

为什么会出现这个错误?难道是因为该变量无法移到堆上所以无法工作吗?这是问题所在吗?我们只能返回不在堆上的值类型的引用吗?这两个问题是一个问题吗。

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

其次:为什么不能通过引用返回在堆栈上创建的值类型?

因此,以下代码可以编译:

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?
}

如果我理解你的评论正确,那是因为现在_myInt不再存在于GetX方法中,因此没有在堆栈中创建对吗?


5
堆和栈只是实现细节,与问题无关。 - TheGeneral
据我所知,值类型并不是存储在堆栈中的。 - H H
6
返回值的生命周期必须超过方法的执行时间,也就是说,它不能是返回它的方法中的局部变量。它可以是类的实例或静态字段,也可以是传递给方法的参数。试图返回一个局部变量会生成编译器错误 CS8168,“无法按引用返回本地变量 'obj',因为它不是 ref 本地变量”。 - Thangadurai
6
新的 ref 语法使运行时能够使用指针。指针可以生成更高效的代码,避免复制值,但它们也很危险。使用指向不再有效的内存位置的指针是一个非常经典的错误。C# 编译器会检查这些错误,它能够判断方法返回后局部变量已不存在,因此标记为糟糕的代码。https://en.wikipedia.org/wiki/Dangling_pointer - Hans Passant
1
栈是一种实现细节,作者为Eric Lippert,来源于https://blogs.msdn.microsoft.com/ericlippert/2009/04/27/the-stack-is-an-implementation-detail-part-one/。 - satnhak
显示剩余12条评论
2个回答

4
据我所知,值类型存储在堆栈中,这是你困惑的基础;这种简化是极其不准确的。结构体可以存在于堆栈上,但它们也可以存在于以下位置:
- 作为堆上对象的字段 - 作为其他结构体的字段,该结构体是堆上对象的字段 - 包装在堆上(直接或通过上述任何一种方式) - 在非托管内存中
你是正确的:如果你将一个ref返回值从方法传递到方法内部的本地变量,则会破坏堆栈完整性。这正是为什么不允许这种情况的原因。
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;
}

需要注意的是,本地变量的生命周期继承了传递到后续调用中的任何参数的生命周期;如果后续调用涉及具有受限生命周期的ref本地变量,则结果将继承该受限生命周期,并且您将无法返回它。

例如,这个后续调用可以是调用存储在非托管内存中的结构体:

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

那么:有很多非常有效和有用的场景,并不涉及对当前堆栈帧的引用。

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

3
我觉得你已经理解为什么它不能工作了。除非是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返回值类型还是引用类型都没有任何区别。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接