問題
最近跟同事討論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的理解有誤,望解惑!
解惑
@鈞梓昊逑我想着應該是最好的答案了~