int ref参数会被装箱吗?

22

假设我有以下代码:

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 

i.e. 当堆栈展开时,引用参数的值将得到维护。

这是不是意味着将 ref关键字 添加到 int参数 会导致它被装箱?在递归调用过程中实际的堆栈是怎样的?


12
可能是因为我的知识有限,但让我们试一下:通过引用传递值类型会传递它在栈上的位置而不是值本身。这与装箱和拆箱无关,是这样吗? - Allmighty
5
来自 MSDN 的原文是:"There is no boxing of a value type when it is passed by reference." 翻译为中文是:"当值类型通过引用传递时,不会出现装箱操作。" - shree.pat18
6个回答

29
将值类型按引用传递,导致其在堆栈上的位置被传递,而不是传递该值本身。这与装箱和拆箱无关。这使得在递归调用期间考虑堆栈外观变得相当容易,因为每个调用都引用堆栈上的“相同”位置。
我认为很多混淆来自于MSDN在装箱和拆箱方面的段落
引用:

装箱指将值类型转换为引用类型的过程。当您装箱变量时,您正在创建一个引用变量,它指向堆上的一个新副本。引用变量是一个对象,...

可能会让你在两种不同的事情之间感到困惑: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++;
}

这是两件不同的事情。


3
可能是最好的解释是......请注意,“在堆栈上”是问题中提到的特定情况,值类型不必在堆栈上分配。也许说“绝对位置(即在堆栈上)”可能更通用——不确定对于没有C / C ++背景的人来说它读起来如何。 - Alexei Levenkov

11

很容易创建一个程序,它可以根据 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。相反,ab都是对Main中原始a变量的引用。如果有帮助的话,你可以大多数情况下(不完全)将它们视为指针。


6

在现有答案的基础上,关于这个实现的方法:

CLR支持所谓的托管指针。 ref 将托管指针传递到堆栈上的变量。您也可以传递堆位置:

var array = new int[1];
F(ref array[0]);

您也可以传递字段的引用。

这不会造成固定。托管指针被运行时(特别是垃圾回收器)理解。它们是可重定位的,安全且可验证的。


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

1
这一切都是正确的,但它没有回答 Pawan 的任何问题。 :) - gehho
据我所知,boxing并不会发生。它只是将最新的值推送到堆栈上。 - Amit

3

这不是拳击。

MSDN参考关键字文档中有明确的解释:

不要将按引用传递的概念与引用类型的概念混淆。这两个概念并不相同。无论方法参数是值类型还是引用类型,都可以通过ref进行修改。当按引用传递值类型时,不会发生值类型的装箱。


2
我认为你在说int参数被装箱时,是错的。根据MSDN的描述,

装箱是将值类型转换为对象类型或实现该值类型的任何接口类型的过程。

这里出现的是一个int参数按引用传递,特别地,它是按引用传递的“值类型”。
有关详细信息,您可以参考Jon Skeet的优秀解释

是的,但对于运行时来说,f1 的参数类型不是 System.Int32,而是所谓的 ref 类型 System.Int32&,它不会报告自己是值类型。 - Mike Zboray

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