如果我将对象传递给方法,为什么要使用 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”,这意味着该对象是作为引用传递的。
如果要更改对象是什么,请传递 ref
:
TestRef t = new TestRef();
t.Something = "Foo";
DoSomething(ref t);
void DoSomething(ref TestRef t)
{
t = new TestRef();
t.Something = "Not just a changed t, but a completely different TestRef object";
}
调用 DoSomething 后,t
并没有引用原来的 new TestRef
,而是引用了一个完全不同的对象。
如果您想更改不可变对象(例如 string
)的值,这也可能很有用。 string
创建后就不能更改它的值。但是通过使用 ref
,您可以创建一个函数,将字符串更改为另一个具有不同值的字符串。
除非需要,否则使用 ref
不是一个好主意。使用 ref
使方法可以自由地更改其他参数,方法的调用者需要进行编码以确保他们能够处理这种可能性。
此外,当参数类型是对象时,对象变量始终充当对对象的引用。这意味着当使用 ref
关键字时,您将获得对引用的引用。这使您可以按照上面给出的示例中的描述进行操作。但是,当参数类型是原始值(例如int
)时,如果在方法内部分配了这个参数,则在方法返回后传入的参数的值将改变:
int x = 1;
Change(ref x);
Debug.Assert(x == 5);
WillNotChange(x);
Debug.Assert(x == 5); // Note: x doesn't become 10
void Change(ref int x)
{
x = 5;
}
void WillNotChange(int x)
{
x = 10;
}
在 .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; }
}
}
由于 TestRef 是一个类(它们是引用对象),因此您可以更改 t 内部的内容,而无需将其作为 ref 传递。但是,如果您将 t 作为 ref 传递,则 TestRef 可以更改原始 t 所指的内容。即使它指向不同的对象。
使用 ref
,您可以编写:
static public void DoSomething(ref TestRef t)
{
t = new TestRef();
}
并且 t 将在方法完成后更改。
将引用类型(例如 List<T>
)的变量(例如 foo
)视为持有“Object #24601”形式的对象标识符。假设语句 foo = new List<int> {1,5,7,9};
导致 foo
保存“Object #24601”(包含四个项目的列表)。然后调用 foo.Length
将询问 Object #24601 的长度,它会响应 4,因此 foo.Length
将等于 4。
如果将 foo
传递给方法而不使用 ref
,则该方法可能会更改 Object #24601。由于此类更改,foo.Length
可能不再等于 4。但是,方法本身将无法更改 foo
,它将继续保留“Object #24601”。
将 foo
作为 ref
参数传递将允许被调用的方法不仅可以更改 Object #24601,还可以更改 foo
本身。该方法可能会创建一个新对象 #8675309 并将对该对象的引用存储在 foo
中。如果这样做,foo
将不再持有“Object #24601”,而是“Object #8675309”。
实际上,引用类型变量不包含“Object #8675309”形式的字符串;他们甚至没有任何可以有意义地转换为数字的东西。尽管每个引用类型变量都将保存一些位模式,但存储在这些变量中的位模式与它们所标识的对象之间并没有固定的关系。代码无法从对象或对它的引用中提取信息,然后确定另一个引用是否标识了同一对象,除非代码持有或知道标识原始对象的引用。
IntPtr
吗?你不能做 IntPtr.ToString()
来获取内存地址吗?
ref
参数传递,系统将跟踪堆栈帧的哪些部分包含 ref
参数,以及对以这种方式访问其字段的对象的引用;至少在 .NET gc 的某些版本中。系统可以重新定位其字段由 byref
标识的对象,并适当地更新 byref
。
IntPtr
,只要对象被固定,它就会保持有效,但在某个时间点知道对象的地址仅当对象自观察到地址后一直被固定时,时间才有意义。
这就像在 C 中传递一个指向指针的指针。在 .NET 中,这将允许您更改原始 T 所指的内容,尽管我个人认为如果您在 .NET 中这样做,您可能遇到了设计问题!
通过将 ref
关键字与引用类型一起使用,您实际上是在传递对引用的引用。在许多方面,它与使用 out
关键字相同,但细微的差别在于,不能保证该方法实际上会将任何内容分配给 ref
的参数。
ref
模仿(或表现)为仅针对两个范围的全局区域:
呼叫者
被调用者。
但是,如果您要传递一个值,则情况会有所不同。您可以强制通过引用传递值。例如,这允许您将整数传递给方法,并让该方法代表您修改整数。
ref
...您还希望在哪里区分它?语义也相当清晰,但需要仔细表达(而不是“对象通过引用传递”,这是常见的过度简化)。