当传递一个对象时,为什么要使用'ref'关键字?

345

如果我将一个对象传递给一个方法,为什么要使用ref关键字?这不是默认行为吗?

例如:

class Program
{
    static void Main(string[] args)
    {
        TestRef t = new TestRef();
        t.Something = "Foo";

        DoSomething(t);
        Console.WriteLine(t.Something);
    }

    static public void DoSomething(TestRef t)
    {
        t.Something = "Bar";
    }
}


public class TestRef
{
    public string Something { get; set; }
}

输出结果为“Bar”,这意味着对象被作为引用传递。
11个回答

349
传递一个 ref 如果你想改变对象的内容:
TestRef t = new TestRef();
t.Something = "Foo";
DoSomething(ref t);

void DoSomething(ref TestRef x)
{
  x = new TestRef();
  x.Something = "Not just a changed TestRef, but a completely different TestRef object";
}

调用DoSomething后,变量t不再引用原始的new TestRef,而是引用一个完全不同的对象。
如果您想更改不可变对象(例如字符串)的值,则这也可能很有用。一旦创建了字符串,就无法更改其值。但是,通过使用ref,您可以创建一个函数,将字符串更改为具有不同值的另一个字符串。
除非必要,否则不建议使用ref。使用ref使方法可以自由更改参数为其他内容,因此调用该方法的调用方需要编写代码以确保处理此可能性。
此外,当参数类型为对象时,对象变量始终充当对对象的引用。这意味着当使用ref关键字时,您拥有对引用的引用。这使您可以执行上面给出的示例中描述的操作。但是,当参数类型为基元值(例如int)时,如果在方法内分配了此参数,则传递的参数值将在方法返回后更改:
int v = 1;
Change(ref v);
Debug.Assert(v == 5);
WillNotChange(v);
Debug.Assert(v == 5); // Note: v doesn't become 10

void Change(ref int x)
{
  x = 5;
}

void WillNotChange(int x)
{
  x = 10;
}

如果我之前不了解这个机制,这个答案会让人非常困惑。在文本中,你提到了“t”,但实际上方法参数和私有字段都被命名为“t”。既然答案实际上是关于改变参数会改变字段引用本身的,我强烈建议在此处使用不同的名称! - somedotnetguy
@somedotnetguy 希望现在好一些了。 - Scott Langham

97
你需要区分“按值传递引用”和“按引用传递参数/参数”的区别。
我已经写了一篇相当长的文章,以避免每次在新闻组中出现这种情况时都要仔细书写。

1
在将VB6升级为.Net C#代码时,我遇到了问题。有些函数/方法签名需要使用ref、out和普通参数。那么我们如何更好地区分普通参数和引用参数? - bonCodigo
2
@bonCodigo:不确定你所说的“更好区分”是什么意思——它是签名的一部分,您还必须在调用站点指定“ref”…您还想在哪里进行区分?语义也相当清晰,但需要小心表达(而不是“对象通过引用传递”,这是常见的过度简化)。 - Jon Skeet

65

.NET中,当你将任何参数传递给一个方法时,都会创建一个副本。在值类型中,这意味着你对该值所做的任何修改都只在方法范围内有效,并且在退出方法时丢失。

当传递引用类型时,也会创建一个副本,但它是指向同一对象的引用的副本。因此,如果你使用引用来修改对象,则对象被修改。但如果你修改引用本身 - 我们必须记住它是一个副本 - 那么任何更改也将在退出方法时丢失。

正如之前所说,赋值是引用的修改,因此会丢失:

public void Method1(object obj) {   
 obj = new Object(); 
}

public void Method2(object obj) {  
 obj = _privateObject; 
}

上述方法不会修改原始对象。
稍作修改,您的示例如下:
 using System;

    class Program
        {
            static void Main(string[] args)
            {
                TestRef t = new TestRef();
                t.Something = "Foo";

                DoSomething(t);
                Console.WriteLine(t.Something);

            }

            static public void DoSomething(TestRef t)
            {
                t = new TestRef();
                t.Something = "Bar";
            }
        }



    public class TestRef
    {
    private string s;
        public string Something 
        { 
            get {return s;} 
            set { s = value; }
        }
    }

但是在“t = new TestRef();”中,对象的属性丢失了,这并没有正确回答问题。 - Heitor Giacomini

18

由于TestRef是一个类(即引用对象),因此您可以在不将其作为ref传递的情况下更改t内部的内容。但是,如果将t作为ref传递,则TestRef可以更改原始t所引用的内容。也就是说,使其指向不同的对象。


17

使用 ref,您可以编写:

static public void DoSomething(ref TestRef t)
{
    t = new TestRef();
}

在方法完成后,t将被更改。


如果未指定ref,则t是相同的对象,所有属性都重置为初始值。对于调用者而言,传入的参数将始终具有重置的属性。这样做的目的是什么? - Mukus

7
将引用类型变量(如List<T>)中的变量(如foo)视为持有形式为“Object #24601”的对象标识符。假设语句foo = new List<int> {1,5,7,9};导致foo持有“Object #24601”(一个具有四个项目的列表)。然后调用foo.Length将会请求Object #24601的长度,并且回应为4,因此foo.Length将等于4。
如果未使用reffoo传递给方法,则该方法可能会更改Object #24601。由于这些更改,foo.Length可能不再等于4。但是,该方法本身将无法更改foo,它将继续持有“Object #24601”。
foo作为ref参数传递将允许被调用的方法不仅更改Object #24601,还更改foo本身。该方法可以创建一个新的Object #8675309并将对其的引用存储在foo中。如果它这样做,foo将不再持有“Object #24601”,而是持有“Object #8675309”。
在实践中,引用类型变量不会保存形式为“Object #8675309”的字符串;它们甚至不保存可以有意义地转换为数字的任何内容。尽管每个引用类型变量将保存某些位模式,但在这些变量中存储的位模式与它们标识的对象之间没有固定的关系。代码无法从对象或对其的引用中提取信息,并且稍后确定另一个引用是否标识了相同的对象,除非该代码保存或知道标识原始对象的引用。

不是应该使用引用类型变量来保存 IntPtr 吗?你不能使用 IntPtr.ToString() 来获取内存地址吗? - David Klempfner
@DavidKlempfner:.NET运行时需要在程序执行的每个点都知道至少一个固定对象的引用和每个未固定对象的引用。据我所知,如果将对象字段作为“ref”参数传递,则系统将跟踪哪些堆栈帧部分持有“ref”参数,以及访问此类方式中访问其字段的对象的引用;在至少某些版本的.NET gc中,系统可以重新定位由“byref”标识的对象,并相应地更新“byref”。 - supercat
@DavidKlempfner:我认为可以将一个由byref持有的对象固定,然后将这个byref转换为一个“IntPtr”,只要该对象被固定,该指针就会保持有效,但是只有在观察到地址时该对象一直被固定,才能够理解该地址的意义。 - supercat
@DavidKlempfner:我已经很久没有阅读有关这方面的内容并进行实验了。最重要的原则是要理解,如果一个对象被重新定位,那么可能用来访问该对象的每个引用都将更新存储的位模式以反映新位置。并发GC可能会设置访问控制位,以便尝试访问旧位置上的对象会触发总线故障,然后由总线故障处理程序更新地址以反映新位置,但旧存储区域不符合回收条件... - supercat
直到所有旧地址的副本都被新地址替换为止。这是一个看起来应该很复杂和低效的系统,但常见版本的JVM和.NET Runtime都能使用一些巧妙的技巧使事情出奇地顺利运行。 - supercat

6

这就像在C语言中传递指向指针的指针。在.NET中,这将允许您更改原始T所引用的内容,但个人认为,如果您在.NET中这样做,可能存在设计问题!


4
通过在引用类型中使用ref关键字,您实际上是将引用传递给引用。从许多方面来看,它与使用out关键字相同,但有一个小差异,即没有保证该方法实际上会将任何内容分配给ref参数。

3

ref 模拟(或行为)成一个全局区域,只适用于以下两个范围:

  • 调用者
  • 被调用者。

2

然而,如果您正在传递一个值,情况就不同了。您可以强制将一个值按引用传递。这使您可以将一个整数传递给一个方法,并代表您修改该整数。


5
无论是传递引用类型值还是值类型值,其默认行为都是按值传递。您只需要理解,对于引用类型,您传递的“值”实际上是一个引用。这不同于按引用传递。 - Jon Skeet

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