Ref - 参数 - 栈还是堆

8
只是一个理论性的问题,但我找不到一个好的答案:
如果我通过ref传递一个参数,则传递的是对象本身而不是副本。
这就是让我感到困惑的地方:据我所知,每个方法都有自己的堆栈帧 - 内存,它们不能离开。那么,这是否意味着一个 ref - Object 被打包在堆上并且有一个对此参数的引用,或者该方法进入调用方法的堆栈并在那里工作?
如果我的问题令人困惑,我很抱歉,我基本上想知道 ref 类型如何保存以及它对其产生的影响。
注: 我认为我没有表达清楚。我了解值类型和引用类型的概念。为了简单起见,我尝试仅通过一个值类型来解释它,比如 Int:
过程 1 通过 ByVal 调用 Prodecure 2 传递一个 Int。这个 int 在 Prodecure 2 的堆栈上有自己的内存,这意味着在 P2 中更改此值并不会更改 P1 中的值,因为这两个值各自保存在堆栈中一次。
现在同样是通过 ByRef:Prodecure 2 不保存 Int 的副本,而是直接访问此值。使其工作有(在我看来)两种可能性:
1. int 被打包在堆上,并且实际上有 2 个指向此 Int 的指针,但由于它现在在堆上,因此值更改在两个过程中都可以看到。
2. P2 在某种程度上可以访问 P1 的堆栈,这是我认为不可能的,因为这意味着堆栈生命周期没有固定。
这样是否能更清楚地表达我的意思?

1
这可能会对您有所帮助:http://www.yoda.arachsys.com/csharp/parameters.html - Jason Evans
1
传递的不是对象本身,而是对象的引用。按值传递,在堆栈或寄存器中或通过其他实现细节。 - Frédéric Hamidi
1
@Rahul 不是的。当你通过 ref 传递引用时,你传递的是对另一个引用的引用。这类似于指向指针的指针。你并没有传递相同引用的副本,那样会按值传递引用。 - Servy
2
@RoyiNamir 是的。正如我从一开始就说的那样,通过引用传递引用是传递到引用存储位置的引用,而不是复制引用的值。当您按值传递时,会复制引用的值。 - Servy
1
是的,确实如此。现在,@Servy所说的话很有道理。 - Rahul
显示剩余13条评论
3个回答

11

传递的参数是某个对象的地址。该引用与所有其他参数一起通过栈传递给方法。

实际对象本身存在于调用方法之前它所在的位置。它可以在堆栈中,也可以在堆中,这并不重要。通过引用传递对象不会导致它在内存中移动,例如从堆栈移动到堆中,或者从堆中移动到堆栈中。


1
我曾经认为对象可以在内存中移动(除非被固定),而引用将以某种方式跟随(直接更新或双重间接)。否则,一旦存在对对象的引用,垃圾回收器将很难完成其工作。 - Frédéric Hamidi
2
@FrédéricHamidi 这是在谈论一个有些不同的抽象层面。(从任何具有对堆中某个东西的引用的对象的角度来看,它根本没有移动,因为它们所有的引用总是指向同一个地方。GC命中不同的内存块以解析地址是在另一个黑盒子里。)无论如何,这不是因为对象通过引用传递而移动,而是因为GC正在进行收集。我在这里的主要观点是,与闭包不同,当通过ref传递时,变量不会被提升。 - Servy
1
@RoyiNamir 我从未说过它不会移动。我说的是通过引用传递它的行为不会移动它,而不是它不能被某些完全分离的效果移动,例如GC压缩。问题意味着通过引用传递栈上的对象将被移动到堆上。这不是事实,它会停留在原地。 - Servy
+1。另请参阅 MSDN,其中指出 ref 不会导致值类型的装箱/拆箱。这意味着,如果值类型最初被分配为堆栈变量,则通过 ref 传递的值类型 必须 保留在 一个 栈上,因此逻辑假设是它仍然保留在分配它的堆栈上。 - Adam Robinson
谢谢大家,我以为一个Stackframe不可能离开自己去连接另一个Stackframe,但事实证明我错了。 - Matthias Müller
显示剩余5条评论

1

这应该是一条注释 :) - Markus

0
尽管Servy已经正确回答了这个问题,但是似乎关于使用ref传递参数和通过值传递对象的引用之间的区别还存在很多混淆。因此,我认为提供一个简短的例子是值得的。
假设有以下简单的类:
class Player
{
    public Player(int health)
    {
        Health = health;
    }
    public int Health { get; set; }
}

我们现在可以测试更新对象的属性并改变引用本身:
static void Main(string[] args)
{
    Player player = new Player(100);
    Console.WriteLine(player.Health);

    ChangeHealth(player);
    Console.WriteLine(player.Health);
    ChangeHealthByRef(ref player);
    Console.WriteLine(player.Health);

    ChangePlayer(player);
    Console.WriteLine(player.Health);
    ChangePlayerByRef(ref player);
    Console.WriteLine(player.Health);
}

static void ChangeHealth(Player player)
{
    player.Health = 80;
}

static void ChangeHealthByRef(ref Player player)
{
    player.Health = 60;
}

static void ChangePlayer(Player player)
{
    player = new Player(40);
}

static void ChangePlayerByRef(ref Player player)
{
    player = new Player(20);
}

输出:

100
80
60
60
20
ChangeHealth成功地修改了player对象的Health属性。ChangeHealthByRef也成功地修改了player对象的Health属性。因此,你可以看到,在这两个调用中,player所引用的对象都可以被修改,尽管ChangeHealth使用了一个引用的副本。
现在,这里是我认为人们会感到困惑的部分: ChangePlayer创建了一个新的Player对象,它修改了传递进来的引用的副本。这意味着改变不会反映在调用代码中(即Health仍然= 60)。 ChangePlayerByRef也创建了一个新的Player对象,但这次它直接修改了引用,这意味着改变会反映在调用代码中(即Health=20)。

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