为什么在传递对象时使用“ref”关键字?

Why use the 'ref' keyword when passing an object?

提问人:Ryan 提问时间:10/9/2008 最后编辑:Jim FellRyan 更新时间:5/1/2023 访问量:195182

问:

如果我将对象传递给方法,为什么要使用 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”,这意味着对象是作为引用传递的。

C# .NET 按引用传递引用

评论


答:

2赞 Andrew 10/9/2008 #1

但是,如果您传递一个值,情况就不同了。您可以强制通过引用传递值。例如,这允许您将整数传递给方法,并让该方法代表您修改整数。

评论

5赞 Jon Skeet 10/9/2008
无论是传递引用还是值类型值,默认行为都是按值传递。您只需要了解,对于引用类型,您传递的值引用。这与引用传递不同。
17赞 Rinat Abdullin 10/9/2008 #2

你可以写:ref

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

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

评论

0赞 Mukus 5/26/2021
如果未指定 ref,则 t 是所有属性重置为初始值的同一对象。就调用方而言,传入的参数将始终重置属性。这样做有什么意义?
348赞 Scott Langham 10/9/2008 #3

如果要更改对象的内容,请传递 a: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 后,不引用原始对象,而是引用完全不同的对象。tnew TestRef

如果您想更改不可变对象的值,例如 .一旦创建,就无法更改 a 的值。但是通过使用 ,您可以创建一个函数来更改另一个具有不同值的字符串的字符串。stringstringref

除非需要,否则使用不是一个好主意。使用 使方法可以自由地更改其他内容的参数,需要对方法的调用者进行编码以确保他们处理这种可能性。refref

此外,当参数类型是对象时,对象变量始终充当对对象的引用。这意味着,当使用关键字时,您就得到了对引用的引用。这允许您按照上面给出的示例中所述执行操作。但是,当参数类型是基元值(例如 )时,如果在方法中将此参数赋值给,则在方法返回后,传入的参数的值将更改:refint

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

评论

0赞 somedotnetguy 4/20/2023
如果我还不知道机制,这个答案会非常令人困惑。在您引用的文本中,但实际上方法参数和私有字段都命名为 .由于答案实际上是关于更改参数实际上会改变字段的引用本身,因此我强烈建议在这里使用不同的名称!tt
0赞 Scott Langham 4/20/2023
@somedotnetguy希望现在情况会好转
18赞 Ferruccio 10/9/2008 #4

由于 TestRef 是一个类(它们是引用对象),因此您可以更改 t 中的内容,而无需将其作为 ref 传递。但是,如果将 t 作为 ref 传递,TestRef 可以更改原始 t 所指的内容。即让它指向不同的对象。

4赞 Isak Savo 10/9/2008 #5

通过将关键字与引用类型一起使用,可以有效地将引用传递给引用。在许多方面,它与使用关键字相同,但略有不同,即不能保证该方法实际上会为“ed”参数分配任何内容。refoutref

6赞 pezi_pink_squirrel 10/9/2008 #6

这就像在 C 中传递指向指针的指针一样,在 .NET 中,这将允许您更改原始 T 所指的内容,尽管我认为如果您在 .NET 中这样做,您可能会遇到设计问题!

97赞 Jon Skeet 10/9/2008 #7

您需要区分“按值传递引用”和“按引用传递参数/参数”。

我已经写了一篇关于这个主题的相当长的文章,以避免每次出现在新闻组上时都必须仔细写。

评论

1赞 bonCodigo 3/18/2015
好吧,我在将 VB6 升级到 .Net C# 代码时遇到了这个问题。有一些函数/方法签名可以接受 ref、out 和 plain 参数。那么,我们如何才能更好地区分普通参数与引用之间的区别呢?
2赞 Jon Skeet 3/18/2015
@bonCodigo:不确定你说的“更好地区分”是什么意思 - 这是签名的一部分,你也必须在呼叫现场指定......你还希望它在哪里被区分?语义也相当清晰,但需要谨慎表达(而不是“对象通过引用传递”,这是常见的过度简化)。ref
65赞 Ricardo Amores 10/9/2008 #8

在 .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; }
        }
    }

评论

0赞 Heitor Giacomini 5/27/2022
但是对象的属性在 “t = new TestRef();” 中丢失了。这不能正确回答问题。
3赞 guneysus 5/11/2015 #9

ref仅针对两个范围模拟(或表现)为全局区域:

  • 访客
  • 卡莉。
7赞 supercat 7/18/2015 #10

将引用类型(例如)的变量(例如)视为保存“对象 #24601”形式的对象标识符。假设语句导致保存“对象 #24601”(包含四个项目的列表)。然后调用将询问对象 #24601 的长度,它将响应 4,因此等于 4。fooList<T>foo = new List<int> {1,5,7,9};foofoo.Lengthfoo.Length

如果在不使用 的情况下传递给某个方法,则该方法可能会对对象 #24601 进行更改。由于这些变化,可能不再等于 4。但是,方法本身将无法更改,它将继续保存“对象 #24601”。fooreffoo.Lengthfoo

作为参数传递将允许被调用的方法不仅对对象 #24601 进行更改,而且还对自身进行更改。该方法可能会创建一个新的对象 #8675309 并将对该对象的引用存储在 中。如果这样做,将不再保留“对象 #24601”,而是“对象 #8675309”。fooreffoofoofoo

在实践中,引用类型变量不包含“对象 #8675309”形式的字符串;他们甚至没有任何东西可以有意义地转换为数字。尽管每个引用类型变量都将包含一些位模式,但存储在这些变量中的位模式与它们标识的对象之间没有固定的关系。代码无法从对象或对它的引用中提取信息,然后确定另一个引用是否标识了同一对象,除非代码持有或知道标识原始对象的引用。

评论

0赞 David Klempfner 4/18/2021
引用类型变量不包含 s?你不能做来获取内存地址吗?IntPtrIntPtr.ToString()
0赞 supercat 4/19/2021
@DavidKlempfner:.NET 运行时需要知道在程序执行过程中的每一点,至少对每个固定对象的引用,以及对每个未固定对象的每个引用。根据我的理解,如果将对象字段作为参数传递,系统将跟踪堆栈帧的哪些部分包含参数,以及对以这种方式访问其字段的对象的引用;至少在某些版本的 .NET gc 中。系统可以重新定位其字段由 标识的对象,并相应地更新。refrefbyrefbyref
0赞 supercat 4/19/2021
@DavidKlempfner:我认为可以固定一个持有 byref 的对象,然后将 byref 转换为只要该对象被固定,它就会保持有效的对象,但是只有在观察到该地址后该对象一直被固定时,在某个时间点了解该对象的地址才有意义。IntPtr
0赞 supercat 4/21/2021
@DavidKlempfner:我已经很久没有读到这些事情是如何完成的,也做过一些实验。要理解的最重要的原则是,如果对象被重新定位,则可能用于访问该对象的每个引用都将更新存储的位模式以反映新位置。并发 GC 可能会设置访问控制位,以便尝试访问旧位置的对象将触发总线故障,然后让总线故障处理程序更新地址以反映新位置,但旧存储不符合回收条件......
0赞 supercat 4/21/2021
...直到旧地址的所有副本都替换为新地址。这是一个看起来应该很复杂且效率低下的系统,但 JVM 和 .NET 运行时的普通版本都能够使用一些聪明的技术来使事情运行得非常好。
0赞 somedotnetguy 4/20/2023 #11

我相信我在某处读到了关于相关行为的美丽描述。它是这样的:

在 C# 中没有“通过引用传递”,因为没有指针(不安全的代码除外),实际上一切都是“按值复制”。只有复杂类型的实例实际上应该被视为将引用作为值的变量。因此,它们在方法调用中复制了“值”(指向真实数据的引用)。因此,在方法内部,参数引用相同的对象,但保存引用的变量是原始输入的副本。 关键字将其更改为按引用调用。ref

官方独库

官方 Doku 继续