说我有以下代码:
void Main()
{
int a = 5;
f1(ref a);
}
public void f1(ref int a)
{
if(a > 7) return;
a++;
f1(ref a);
Console.WriteLine(a);
}
输出为:
8 8 8
即。当堆栈展开时,保持ref参数的值。
这是否意味着将ref keyword
添加到int parameter
会导致它被装箱?
在递归调用期间,实际堆栈的外观如何?
答案 0 :(得分:25)
通过引用传递值类型会导致堆栈上的位置传递而不是值本身。它与拳击和取消装箱无关。这使得思考堆栈在递归调用期间的外观相当容易,因为每个调用都指的是"相同的"在堆栈上的位置。
我认为很多混淆来自MSDN's paragraph on boxing and unboxing:
拳击是给予进程的名称,其中值类型被转换为引用类型。当您装入变量时,您将创建一个指向堆上的新副本的引用变量。引用变量是一个对象,......
可能会让您在两个不同的事物之间感到困惑: 1)"转换"如您所愿,值类型表示一个对象,根据定义它是一个引用类型:
int a = 5;
object b = a; // boxed into a reference type
和 2),通过引用传递值类型参数:
main(){
int a = 5;
doWork(ref a);
}
void doWork(ref int a)
{
a++;
}
这是两件不同的事情。
答案 1 :(得分:9)
根据ref int
是否装箱,可以轻松创建可能产生不同结果的程序:
static void Main()
{
int a = 5;
f(ref a, ref a);
}
static void f(ref int a, ref int b)
{
a = 3;
Console.WriteLine(b);
}
你得到了什么?我看到3
已打印出来。
拳击涉及创建副本,因此如果装箱ref a
,则输出将为5
。相反,a
和b
都是对a
中原始Main
变量的引用。如果它有帮助,你可以大多数(不完全)将它们视为指针。
答案 2 :(得分:3)
您的Console.WriteLine(a);
将在递归完成后执行。当int的值变为8时,递归完成。为了使其为8,它递归3次。因此,在最后它将打印8然后将控制传递给上面的递归,其将再次打印8作为变量,其值已变为8.
同时检查ILDASM输出
.method public hidebysig static void f1(int32& a) cil managed
{
// Code size 26 (0x1a)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldind.i4
IL_0002: ldc.i4.7
IL_0003: ble.s IL_0006
IL_0005: ret
IL_0006: ldarg.0
**IL_0007: dup**
IL_0008: ldind.i4
IL_0009: ldc.i4.1
IL_000a: add
IL_000b: stind.i4
IL_000c: ldarg.0
IL_000d: call void ConsoleApplication1.Program::f1(int32&)
IL_0012: ldarg.0
IL_0013: ldind.i4
IL_0014: call void [mscorlib]System.Console::WriteLine(int32)
IL_0019: ret
} // end of method Program::f1
答案 3 :(得分:3)
添加现有答案如何实施:
CLR支持所谓的托管指针。 ref
将托管指针传递给堆栈上的变量。您还可以传递堆位置:
var array = new int[1];
F(ref array[0]);
您也可以将引用传递给字段。
这不会导致钉扎。运行时(尤其是GC)可以理解托管指针。它们是可重新定位的。它们是安全可靠的。
答案 4 :(得分:2)
我认为你错误地说int
参数被装箱了。来自MSDN,
拳击是将值类型转换为类型对象或的过程 到此值类型
实现的任何接口类型
这里有一个int
参数通过引用传递,特别是它是一个通过引用传递的“值类型”。
您可以参考Jon Skeet关于参数传递的优秀explanation以获取详细信息。
答案 5 :(得分:2)
这不是拳击。
MSDN ref keyword documentation中有明确的解释:
不要将引用传递的概念与引用类型的概念混淆。这两个概念不尽相同。无论是值类型还是引用类型,都可以通过ref修改方法参数。通过引用传递时,没有值类型的装箱。