在 C# 中,方法的参数可以是引用类型或值类型。传递引用类型时,传递引用的副本。这样,如果在方法内部我们尝试将传递的引用重新分配给另一个对象实例,则在方法外部重新分配是不可见的。
为了使其工作,C# 具有 ref 修饰符。使用 ref 传递引用类型实际上使用原始引用而不是副本。(如我错了请纠正我)。
在这种情况下,由于我们没有创建引用的副本,我们是否节省了任何内存?如果一个方法被广泛调用,这是否会提高应用程序的整体性能?
谢谢!
在 C# 中,方法的参数可以是引用类型或值类型。传递引用类型时,传递引用的副本。这样,如果在方法内部我们尝试将传递的引用重新分配给另一个对象实例,则在方法外部重新分配是不可见的。
为了使其工作,C# 具有 ref 修饰符。使用 ref 传递引用类型实际上使用原始引用而不是副本。(如我错了请纠正我)。
在这种情况下,由于我们没有创建引用的副本,我们是否节省了任何内存?如果一个方法被广泛调用,这是否会提高应用程序的整体性能?
谢谢!
不,它没有。如果有的话,它会因为额外的查找而变慢。
没有理由通过引用传递引用类型,除非您以后特别打算分配给它。
由于有些人似乎认为编译器传递的是“变量本身”,所以看看这段代码的反汇编:
using System;
static class Program
{
static void Test(ref object o) { GC.KeepAlive(o); }
static void Main(string[] args)
{
object temp = args;
Test(ref temp);
}
}
这是(在 x86 上,为简单起见):
// Main():
// Set up the stack
00000000 push ebp // Save the base pointer
00000001 mov ebp,esp // Set up stack pointer
00000003 sub esp,8 // Reserve space for local variables
00000006 xor eax,eax // Zero out the EAX register
// Copy the object reference to the local variable `temp` (I /think/)
00000008 mov dword ptr [ebp-4],eax // Copy its content to memory (temp)
0000000b mov dword ptr [ebp-8],ecx // Copy ECX (where'd it come from??)
0000000e cmp dword ptr ds:[00318D5Ch],0 // Compare this against zero
00000015 je 0000001C // Jump if it was null (?)
00000017 call 6F910029 // (Calls some internal method, idk)
// THIS is where our code finally starts running
0000001c mov eax,dword ptr [ebp-8] // Copy the reference to register
0000001f mov dword ptr [ebp-4],eax // ** COPY it AGAIN to memory
00000022 lea ecx,[ebp-4] // ** Take the ADDRESS of the copy
00000025 call dword ptr ds:[00319734h] // Call the method
// We're done with the call
0000002b nop // Do nothing (breakpoint helper)
0000002c mov esp,ebp // Restore stack
0000002e pop ebp // Epilogue
0000002f ret // Return
这是来自代码的优化编译。显然,传递的是变量的地址,而不是“变量本身”。
Mehrdad 示例的分解器视图(两个版本)
对于像我这样不善于阅读汇编代码的人,我将尝试更深入地研究 Mehrdad 的好证明。当我们调试时,可以在 Visual Studio 中捕获此代码,单击 Debug -> Windows -> Dissasembly。
使用 REF 的版本
源代码:
namespace RefTest
{
class Program
{
static void Test(ref object o) { GC.KeepAlive(o); }
static void Main(string[] args)
{
object temp = args;
Test(ref temp);
}
}
}
汇编语言(x86)(仅显示不同的部分):
object temp = args;
00000030 mov eax,dword ptr [ebp-3Ch]
00000033 mov dword ptr [ebp-40h],eax
Test(ref temp);
00000036 lea ecx,[ebp-40h] //loads temp address's address on ecx?
00000039 call FD30B000
0000003e nop
}
没有参考的版本
源代码:
namespace RefTest
{
class Program
{
static void Test(object o) { GC.KeepAlive(o); }
static void Main(string[] args)
{
object temp = args;
Test(temp);
}
}
}
汇编语言(x86)(仅显示不同的部分):
object temp = args;
00000035 mov eax,dword ptr [ebp-3Ch]
00000038 mov dword ptr [ebp-40h],eax
Test(temp);
0000003b mov ecx,dword ptr [ebp-40h] //move temp address to ecx?
0000003e call FD30B000
00000043 nop
}
除了注释行之外,两个版本的代码是相同的:使用 ref,对函数的调用前面有一条 LEA 指令,没有 ref,我们有一个更简单的 MOV 指令。执行此行后,LEA 已将指向对象指针的指针加载到 ecx 寄存器中,而 MOV 已将指向对象的指针加载到 ecx 寄存器中。这意味着 FD30B000 子例程(指向我们的测试函数)在第一种情况下必须额外访问内存才能访问对象。如果我们检查这个函数的每个生成版本的汇编代码,我们可以看到在某些时候(实际上是两个版本之间唯一不同的行)进行了额外的访问:
static void Test(ref object o) { GC.KeepAlive(o); }
...
00000025 mov eax,dword ptr [ebp-3Ch]
00000028 mov ecx,dword ptr [eax]
...
而没有 ref 的函数可以直接进入对象:
static void Test(object o) { GC.KeepAlive(o); }
...
00000025 mov ecx,dword ptr [ebp-3Ch]
...
希望它有所帮助。
是的,有一个原因:如果要重新分配值。在这方面,值类型和引用类型没有区别。
请参见以下示例:
class A
{
public int B {get;set;}
}
void ReassignA(A a)
{
Console.WriteLine(a.B);
a = new A {B = 2};
Console.WriteLine(a.B);
}
// ...
A a = new A { B = 1 };
ReassignA(a);
Console.WriteLine(a.B);
这将输出:
1
2
1
然而,性能与它无关。这将是真正的微优化。
按值传递引用类型不会复制对象。它只创建对现有对象的新引用。所以你不应该通过引用传递它,除非你真的需要。