C#中参数按引用/值传递的工作原理是什么?

3

我有这段样例代码:

    public class MyClass
    {
        public int Value { get; set; }
    }


    class Program
    {
        public static void Foo(MyClass v)
        {
            v.Value = 2;
            v = new MyClass();
            v.Value = 3;
        }

        static void Main(string[] args)
        {
            var m = new MyClass();
            m.Value = 1;
            Foo(m);
            Console.Write(m.Value);
            Console.ReadLine();
        }
    }

我想了解为什么输出是2而不是3,您能否给我一些清晰的解释呢?谢谢。
5个回答

3

我会通过调试器与您一步步进行,我们将看到它是什么2。

Step1

我们看到我们进入了Foo,通过引用传递了MyClass类的实例v(在C#中,默认情况下通过引用传递类实例)
在内存中,我们会看到类似于这样的东西:
v = 0x01; //0x01 - is a simple representation of a pointer that we passed
v.Value = 1;

接下来,我们跨越一步,然后我们看到我们改变了我们的引用中的值Value。 步骤2
v = 0x01;
v.Value = 2; // our new value

然后我们将new分配给我们的v,因此在内存中我们有

v* = 0x01 // this is our "old" object
v*.Value = 2;
v = 0x02 // this is our "new" object
v.Value = 3;

如您所见,我们在内存中有两个对象!新的对象为v,旧的对象标记为v*

当我们退出该方法时,我们并没有替换内存地址0x01的内容,而是为函数作用域创建了v的本地副本,并创建了一个新对象,其内存地址为0x02,但在我们的主方法中没有引用它。

step3

我们的主要方法是使用来自地址0x01的实例,而不是在Foo方法中创建的新实例0x02!为了确保我们传递正确的对象,我们需要告诉C#我们想要使用ref编辑输出或者我们想要使用out覆盖输出。在底层,它们的实现方式相同!我们不是将0x01传递给Foo方法,而是传递0x03!它具有指向我们类的指针0x01。因此,当我们使用refout分配v = new MyClass()时,我们实际上修改了0x03的值,然后在我们的Main方法中提取并“替换”为包含正确值的值!

我们通过引用传递了v - 我猜v更像是一个引用而不是实例,并且这是按值传递的(这就是为什么问题有意义),但是类的实例由v引用传递(即由您在括号中正确得出的方式)。 - E. Shcherbo
@E.Shcherbo 你说得对。v是一个引用——它是指向类实例的值,但是看到你的句子有点复杂!我花了一分钟才理解它! - Tomasz Juszczak
我同意并且对不起,我并不是想说你写错了什么,我只是写出了一个结论,它曾经帮助我完全理解传递引用和值,所以可能对其他人也有用 :) - E. Shcherbo

1

Foo 函数中初始化 v 对象会创建一个新的 MyClass 实例,但是它的引用没有被设置为 Main 函数中的 m 对象。因为对象的引用是按值传递的。

如果你想在 Foo 中引用它,你应该像这样使用 ref

public static void Foo(ref MyClass v)

并像这样调用它:

Foo(ref m)

如果需要方法初始化参数,则可以使用 out 代替 ref


1
当您将引用传递给方法时,该引用会被复制到堆栈上的另一个变量中。这两个变量(引用)可能仍然引用相同的对象,但变量本身是不同的。这就像这样:
var m = new MyClass();
m.Value = 1;
var s = m;
s.Value = 2; // m.Value is also 2
s = new MyClass();
s.Value = 3; // m.Value is still 2

你不会预料到在这里 m.Value 等于 3,因为你有两个不同的变量引用了堆上的同一个对象,但是后来你改变了 s 以便它引用一个全新的对象。当你将引用传递给方法时,它只是被复制到另一个变量中。

主要思想 是,类的实例默认情况下是通过引用传递的(因为实际上是传递引用),但是引用本身是按值传递的,这意味着它们被复制到另一个变量中。


1
因为当你调用 Foo(m) 时,vm 都是指向同一个对象的不同引用。
将值重新分配给 v 并不会重新分配给 m
与下面的示例形成对比:
public static void Foo(ref MyClass v)
{
    v.Value = 2;
    v = new MyClass();
    v.Value = 3;
}

通过使用ref,如果现在调用Foo(m)vm成为指向同一对象的相同引用,因此对v的重新分配也会重新分配到m:使输出为3:
static void Main(string[] args)
{
    var m = new MyClass();
    m.Value = 1;
    Foo(m);
    Console.Write(m.Value);
    Console.ReadLine();
}

0

当在Foo中调用v = new MyClass();时,引用停止指向传递的对象,而是现在指向新创建的对象。

这不会影响调用者,因为新对象不是在为旧对象分配的内存中创建的,而是变量v现在指向新对象,而不是它曾经指向的对象。

这就是为什么Foo会影响值为2的原始对象,但在重新分配v之后,原始对象不会受到影响的原因。

public class MyClass
    {
        public int Value { get; set; }
    }


    class Program
    {
        public static void Foo(MyClass v)
        {
            v.Value = 2;
            v = new MyClass(); // this will make v point to an other object
            v.Value = 3;
        }

        static void Main(string[] args)
        {
            var m = new MyClass();
            m.Value = 1;
            Foo(m);
            Console.Write(m.Value);
            Console.ReadLine();
        }
    }

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