問題
最近跟同事討論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的理解有誤,望解惑!
解惑
@鈞梓昊逑 方法內部的臨時變數是在進入方法時就在棧上分配的,通過棧頂指標的移動實現變數分配與回收,效率是極高的,對於你說的記憶體浪費,的確會有,這也是為什麼推薦寫小方法的原因。
我想著應該是最好的答案了~