c#:内存中的变量会发生什么?

c#: What happens in a variable in memory?

如果我有这个变量:

string name;

会在内存中分配一个位置吗?或者它只会在我将其初始化为特定值时获得分配的内存?即,

string name = "Jack";

例如,考虑以下代码:

for (int i = 0; i < 20; i++) {
    Run();
}

private void Run() {
    int age = 20;
}

内存中的age值会发生什么变化?每次执行 运行 方法时,它会从内存中删除吗?还是会在代码执行后留在内存中,在使用它的程序关闭后移除?

string name;

如果这是你唯一的声明,编译器可能会优化并删除它。如果不进行优化,这将是对 null 的引用。

string name = "Jack";

这将在堆中创建一个内存分配来存储字符串本身。它还会在您的堆栈中生成一个指针,其中包含分配的堆内存的地址。退出方法并弹出堆栈后,堆中分配的内存将不再有引用,可以标记为垃圾回收。

您的 20 次迭代将生成 20 个堆栈分配,每个分配在堆栈中的值为 20,而在堆中不生成任何内容。退出该方法后,将弹出堆栈并丢失数据。

对于任何 .NET 值类型变量,如 intbooldouble 等;内存分配会在您声明它并为其赋值时立即发生,该值只会在内存中更新。

另一方面,对于包括string在内的引用类型,只在内存中分配一个地址,该地址创建对存储当前值的实际内存位置的引用(类似于[=27中的指针) =]++).

因此,在您的示例中,一旦 int age 为 运行,就会在内存中创建 age,然后它的值将设置为 20 age = 20 被执行。

每次执行Run()方法时都会分配一个新的内存位置。

在您的第一个示例(未初始化的变量)中,它不会分配任何内存,因为它不会生成任何 MSIL。这将与根本没有代码相同。 如果你初始化它,内存将在当前方法的堆栈中分配。

第二种情况,age变量会在每次方法调用时分配到栈中,并在每次方法调用退出时释放。

如果您有此代码:

void Main()
{
    string name;
}

然后,当使用编译器优化进行编译(在 LINQPad 中)时,您将获得以下 IL:

IL_0000:  ret

并且没有优化:

IL_0000:  nop         
IL_0001:  ret         

没有为此声明分配内存 - 只是一个 NOP 操作作为未优化代码在 IL 中的占位符。

当你的程序是这样的:

void Main()
{
    string name = "Jack";
}

那么你编译器优化后的代码是:

IL_0000:  ret

编译器会简单地忽略未使用的变量。

未经优化的代码生成此:

IL_0000:  nop         
IL_0001:  ldstr       "Jack"
IL_0006:  stloc.0     // name
IL_0007:  ret         

显然未优化的代码更易于解释,所以从现在开始我将只显示未优化的代码,除非我明确说明。

现在让我们让代码做一些更有趣的事情。

void Main()
{
    string name = "Jack";
    Console.WriteLine(name);
}

这会产生:

IL_0000:  nop         
IL_0001:  ldstr       "Jack"
IL_0006:  stloc.0     // name
IL_0007:  ldloc.0     // name
IL_0008:  call        System.Console.WriteLine
IL_000D:  nop         
IL_000E:  ret         

有趣的是,如果我们将此代码更改为:

void Main()
{
    int answer = 42;
    Console.WriteLine(answer);
}

我们得到这个:

IL_0000:  nop         
IL_0001:  ldc.i4.s    2A 
IL_0003:  stloc.0     // answer
IL_0004:  ldloc.0     // answer
IL_0005:  call        System.Console.WriteLine
IL_000A:  nop         
IL_000B:  ret         

代码与 string 示例几乎相同。

ldstr 调用正在获取对字符串文字的引用(它存储在大对象堆(不是普通堆,即小对象堆)的字符串池中)并将其推送到评估堆栈。

ldc.i4.s 正在将对数字 42 的引用推送到计算堆栈。

然后,在这两种情况下,stloc.0 将计算堆栈顶部的值存储到方法的第零个本地内存位置。

然后,在这两种情况下,ldloc.0 从第零个本地内存位置加载值并将其放入计算堆栈。

你大概可以想象编译器如果优化这段代码会做什么。

终于完成了 System.Console.WriteLine

现在让我们更详细地看看那个讨厌的 string 代码。

我说过它存储在实习生池中。让我们检查一下。

取此代码:

void Main()
{
    string name = "Jack";
    Console.WriteLine(String.IsInterned(name));
}

它产生:

IL_0000:  nop         
IL_0001:  ldstr       "Jack"
IL_0006:  stloc.0     // name
IL_0007:  ldloc.0     // name
IL_0008:  call        System.String.IsInterned
IL_000D:  call        System.Console.WriteLine
IL_0012:  nop         
IL_0013:  ret

并输出 Jack 到控制台。它只能在 System.String.IsInterned returns 一个 interned 字符串时这样做。

拿这个程序来展示相反的:

void Main()
{
    string name = String.Join("", new [] { "Ja", "ck" });
    Console.WriteLine(String.IsInterned(name));
}

它将 null 推送到控制台 - 这意味着字符串 name 未被保留,因此在这种情况下 name 存储在堆(小对象堆)中。

让我们看看您的第二段代码:

void Main()
{
    for (int i = 0; i < 20; i++)
    {
        Run();
    }
}

private void Run()
{
    int age = 20;
}

如果我们查看优化的 IL,那么 Run 方法如下所示:

Run:
IL_0000:  ret        

未优化的IL是这样的:

Run:
IL_0000:  nop         
IL_0001:  ldc.i4.s    14 
IL_0003:  stloc.0     // age
IL_0004:  ret 

并且,就像我之前使用 int 的示例一样,它将文字值 20(或十六进制的 14)加载到计算堆栈中,然后立即将其存储在该方法的本地内存,然后是 returns。因此它对局部变量 age.

重复使用相同的内存 20 次