问题
最近跟同事讨论for循环中变量定义在哪里的问题。先看一段代码:
private void ForInner()
{
for (int i = 0; i < 5; i++)
{
var obj = new MyClass();
Console.WriteLine(obj.name);
}
}
这是我们正常习惯写的代码。同事的意思是说如果照上面那样写因为每循环一次,obj的变量就要在堆栈上分配一段空间,造成浪费。应该把obj的定义拿到for代码块的外面这样可以少分配一些内存提高效率,代码如下:
private void ForOuter()
{
MyClass obj;
for (int i = 0; i < 5; i++)
{
obj = new MyClass();
Console.WriteLine(obj.name);
}
}
从正常的角度上来看这样写变量obj确实比上面要少分配内存,因为obj只是定义了一次,只在堆栈上分配了一次内存,用来保存指向MyClass的实例的地址。理解这个问题首先得对.net的内存分配有个了解。简单科普一下:
一个引用类型的对象被创建分为以下几步
1. MyClass obj ; 在线程堆栈上创建一个obj的变量,用来保存实例对象的地址。
2. new MyClass();在托管堆上创建 MyClass的实例对象。
3. “=”操作符号 obj存储实例对象的地址。
参考资料:
Anytao的大作:http://www.cnblogs.com/anytao/archive/2007/12/07/must_net_19.html
好了,有了这个背景知识,就不难理解同事为什么说第二种写法会少分配内存了。对于第一种写法会创建多次变量obj,第二种只有一次。那么事实上是不是如此呢?
答案
要查明这个问题我们只需要借助IL,看一下这2段代码的IL就一清二楚了。
看2段IL的代码,我们很容易就发现,其实不管是哪种写法,生成的IL几乎是一样的,不同的只是locals init初始化变量的顺序先后的差异。对于第一种写法IL并没有在循环体内去每次都声明obj变量。所以这两种写法在本质上是一样的。但是本人还是推荐第一种写法,在循环体里直接定义变量。因为循环体里实例化的对象,一般都是循环完成就不在使用了可以被回收,或者被其他业务对象引用,如放入某个List里面去。但是第二种写法的obj变量必定还保持着最后一次循环所创建的对象。这个对象的释放会被限制,且后面的新人接手你的代码时容易误操作了这个变量,造成不必要的bug。
疑惑
经过这次对IL的查看,还发现一个问题,难道在IL中方法的局部变量都是在方法体最上部全部初始化好了吗,于是我又做了测试:
private void ForMany()
{
int z = 1;
var a = new MyClass();
var b = new MyClass();
var c = new MyClass();
var d = new MyClass();
var e = new MyClass();
var f = new MyClass();
var g = new MyClass();
if( z==1)
return;
var h = new MyClass();
var i = new MyClass();
var j = new MyClass();
var k = new MyClass();
var l = new MyClass();
var n = new MyClass();
return;
}
我在方法里定义了很多的变量。看看IL是否全部一次初始化好。结果如下:
不出所料,IL在一开始就把所有的变量都初始化好了。这样我就想不通了,如果代码的中间就有条件语句控制return呢,后面的变量不一定都会用到,完全可以不去初始化,这样难得不会浪费内存空间吗?还是说我对.locas init的理解有误,望解惑!
解惑
@钧梓昊逑我想着应该是最好的答案了~