引用類型和值類型,是一個老生常談的問題了。裝箱拆箱相信也是猿猿都知,但是還是跟著CLR via C#加深下印象,看有沒有什麼更加根本和以前被忽略的知識點。 引用類型: 引用類型有哪些這裡不過多贅述,來關心一下它在電腦內部的實際操作,引用類型總是從托管堆分配,線程棧上存儲的是指向堆上數據的引用地址, ...
引用類型和值類型,是一個老生常談的問題了。裝箱拆箱相信也是猿猿都知,但是還是跟著CLR via C#加深下印象,看有沒有什麼更加根本和以前被忽略的知識點。
引用類型:
引用類型有哪些這裡不過多贅述,來關心一下它在電腦內部的實際操作,引用類型總是從托管堆分配,線程棧上存儲的是指向堆上數據的引用地址,首先確立一下四個事實:
記憶體必須從托管堆分配
堆上分配成員時,CLR要求你必須有一些額外成員(比如同步塊索引,類型對象指針)。這些成員必須初始化。
對象中的其他位元組總是設為零
從托管堆上分配對象時,可能強制執行一次垃圾回收
所以引用類型對性能是有顯著影響的。
值類型:
值類型是CLR提供的輕量級類型,它把實際的欄位存儲線上程棧上
值類型不受垃圾回收器的限制,所以它的存在緩解了托管堆的壓力,也減少了垃圾回收的次數。
值類型都是派生自System.ValueType
所有值類型都是隱式密封的,目的是防止將值類型作為其他引用類型的基類
值類型初始化為空時,預設為0,它不像引用類型是指針,它不會拋出NullReferenceException異常,CLR還為值類型提供了可控類型。
誤區防範:根據我自己的經驗,要避免對引用類型值類型賦值的錯誤認識,我們先需要清楚,定義值類型,引用類型的底層實際操作,下麵先根據流程圖瞭解一下:
例子:
1 class SomeRef{public int x;}
2 struct SomeVal{public int x;}
3
4 staic void Test
5 {
6 SomeRef r1=new SomeRef();
7 SomeVal v1 =new SomeVal();
8
9 r1.x=5;
10 v1.x=5;
11
12 SomeRef r2=r1;
13 SomeVal v2 =v1;
14 r1.x=8;
15 v1.x=9;
16
17 string a="QWER";
18 string b=a;
19 a="TYUI";
20 }
這樣類似的例子,相信只要講到引用類型,值類型,就一定會見到,繼續複習一下。
首先揭曉幾輪複製後的結構:r1.x=8,r2.x=8 v1.x=9 v2.x=5 a="TYUI" b="QWER"
簡單分析一下:
r1 ,r2線上程棧上存儲的是同一個指向記憶體堆的地址,當r1值改變時,其實是直接改變記憶體堆里的內容,自然r1,r2全部變成了8。
而v1,v2是獨立存儲線上程棧上的,v1值改變時,只是單單改變v1線程棧里的值,自然v2=5,v1=9。
而a,b的值為什麼不像上面r1.x一樣變化呢,它們不是引用類型嗎,這就需要去看看上面的流程圖,因為你在給a改變賦值時,其實是在托管堆上開闢了一個新的空間,你傳給a的是一個新的地址,而b還指向原來的老地址。
結合上面的三個圖和示例,對於引用類型和值類型構建相信應該有一個清楚的理解了。
使用值類型的一些建議:
值類型相對於引用類型,性能上更有優勢,但是考慮在業務上的問題,值類型一般需要滿足下麵的全部條件,才是適合定義為值類型:
類型具有基元類型的行為。也就是說,是十分簡單的類型,沒有成員會修改類型的任何實例。如果類型沒有提供會更改其他欄位的成員,就稱為不可變類型(immutable)。事實上,對於許多值類型,我們都建議將全部欄位標記為readonly。
類型不需要從其他類型繼承
類型不派生出其他類(隱式密封)。
類型大小也應考慮:
因為實參預設以傳值方式傳遞,造成對值類型實例中的欄位進行複製,如果值類型過於大會對性能造成損害。
同樣,當頂一個值類型的方法返回時,實例中的欄位會複製到調用者分配的記憶體,也可能造成性能的損害。
所以,必須滿足以下任意條件:
類型實例較小(16位元組或更小)
類型實例較大(大於16位元組),但不作為方法實參傳遞,也不從方法傳遞
值類型的局限:
值類型有兩種形式:未裝箱和已裝箱,而引用類型一直是已裝箱。
值類型從System.ValueType派生,System.ValueType重寫了Equals和GetHashCode方法。生成哈希碼時,會將對象的實例欄位的值考慮在內。所以定義自己的值類型時,因重寫Equals和GetHashCode方法。
值類型不能被繼承,它自己的方法不能是抽象的,所有都是隱式密封的。
值類型不在記憶體堆中分配,所以一個實例的方法不再活動時,分配給值類型的記憶體空間會被釋放,而沒有垃圾回收機制來處理它。
值類型的裝箱拆箱:
例如,ArrayList不斷的添加值類型進入數組時,就會發生不斷的裝箱操作,因為它的Add方法參數是object類型,自然裝箱就不可避免,自然也會造成性能的損失(FCL現在提供了泛型集合類,System.Collection.Generic.List<T>,它不需要裝箱拆箱操作。使得性能提升不少)。
裝箱相關的含義相信不用過多解釋,我們來關心一下,記憶體中的變化,看看它是如何對性能造成影響的。
裝箱:
在托管堆中分配記憶體。記憶體大小時值類型各欄位所需的記憶體加上兩個額外成員(托管堆所有對象都有)類型對象指針和同步塊索引所需的記憶體量。
值類型的欄位值複製到堆記憶體的空間中。
返回堆上對應的地址
然後,一個值類型就變成了引用類型。
拆箱:
根據引用類型的地址找到堆記憶體上的值
將值複製給值類型
拆箱的代價比裝箱小得多
裝箱拆箱註意點:
下麵通過幾個示例,來熟悉一下裝箱拆箱的過程,並學會如何避免錯誤的判定裝箱拆箱,CLR via C#這兩個實例對裝箱拆箱的理解非常有幫助:
1 internal struct Point : IComparable 2 { 3 private Int32 m_x,m_y; 4 public Point(int x,int y) 5 { 6 m_x = x; 7 m_y = y; 8 } 9 10 public override string ToString() 11 { 12 return String.Format("({0},{1})", m_x.ToString(), m_y.ToString()); 13 } 14 15 16 public int CompareTo(Point p) 17 { 18 return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y) - Math.Sqrt(p.m_x * p.m_x + p.m_y * p.m_y)); 19 } 20 21 public int CompareTo(object obj) 22 { 23 if (GetType() != obj.GetType()) 24 { 25 throw new ArgumentException("o is not a point"); 26 } 27 return CompareTo((Point)obj); 28 } 29 }
1 static void Main(string[] args) 2 { 3 //在棧上創建兩個實例 4 Point p1 = new Point(10,10); 5 Point p2 = new Point(10,20); 6 7 //調用Tostring不裝箱 8 Console.WriteLine(p1.ToString()); 9 10 //調用非虛方法GetType裝箱 11 Console.WriteLine(p1.GetType()); 12 13 //調用CompareTo,不裝箱 14 Console.WriteLine(p1.CompareTo(p2)); 15 16 //p1裝箱 17 IComparable C = p1; 18 Console.WriteLine(C.GetType()); 19 20 //不裝箱,調用的CompareTo(object) 21 Console.WriteLine(p1.CompareTo(C)); 22 23 //不裝箱,調用的CompareTo(object) 24 Console.WriteLine(p1.CompareTo(p2)); 26 27 Console.ReadKey(); 28 }
1.調用ToString
不裝箱,因為ToString是從ValueType繼承的虛方法,中間沒有類型轉換的發生,不需要進行裝箱,另外註意的是:Equals,GetHashCode,ToString都是從ValueTye繼承的虛方法,由於值類型都是密封類,無法派生,所以只要你的值類型重寫了這些方法,並沒有去調用基類的實現,那麼是不會發生裝箱的,如果你去調用基類的實現,或者你沒有實現這些方法,那麼還是可能發生裝箱。
2.調用GetType
GetType是繼承自Object,並且不能被重寫,所以無論如何值類型對其調用都會發生裝箱,另外MemberwiseClone方法也是如此。
3.第一次調用CompareTo方法
因為Point裡面有了類型為Point的參數CompareTo方法,不會發生裝箱操作
4.p1轉換為ICompable
確認過眼神,這一定是一個裝箱。
5.第二次調用CompareTo方法
雖然這次調用的是參數為object的方法,但是註意的是:首先我們Point實現了這個重載,另外傳進去的是個ICompable,自然不會發生裝箱(另外,如果Point本身沒有這個方法呢?當然會裝箱,因為它不得不去調用父類的方法,而父類是一個引用類型,自然需要進行一次裝箱操作)
6.第三次調用CompareTo方法
c是ICompable,而ICompable在托管堆上也有對應的方法,也不會有裝箱發生。
5 internal struct point 6 { 7 private int m_x,m_y; 8 9 pulic point(int x,int y) 10 { 11 m_x=x; 12 m_y=y; 13 } 14 15 public void change(int x,int y) 16 { 17 m_x=x; 18 m_y=y; 19 } 20 21 public ovveride String ToString() 22 { 23 return String.Format("{0},{1}",m_x.ToString.m_y.ToString()); 24 } 25 26 }
1 public static void Main() 2 { 3 Point p = new Point(1,1); 4 Console.WriteLine(p); 5 6 p.Change(2,2); 7 Console.WriteLine(p); 8 9 Object o=p; 10 Console.WriteLine(o); 11 12 ((Point) o).Change(3,3); 13 Console.WriteLine(o); 14 }
結果:當然是 (1,1)(2,2) (2,2) (2,2) 前面三次的結果很好理解,第四次為什麼是(2,2),因為object沒有change方法,它等拆箱拆到線程棧新的地址上,於是後面的操作則是線上程棧上進行,對o堆上的內容沒有任何影響
1 internale interface IChangeBoxedPoint 2 { 3 void Change(int x,int y); 4 } 5 internal struct point 6 { 7 private int m_x,m_y; 8 9 pulic point(int x,int y) 10 { 11 m_x=x; 12 m_y=y; 13 } 14 15 public void change(int x,int y) 16 { 17 m_x=x; 18 m_y=y; 19 } 20 21 public ovveride String ToString() 22 { 23 return String.Format("{0},{1}",m_x.ToString.m_y.ToString()); 24 } 25 26 }
1 public static void Main() 2 { 3 Point p =new p(1,1); 4 Console.WriteLine(p); 5 6 p.Change(2,2); 7 Console.WriteLine(p); 8 9 Objec o =p; 10 Console.WriteLine(o); 11 12 ((Point) o).Change(3,3); 13 Console.WriteLine(o); 14 15 ((IChangeBoxedPoint) p).Change(4,4); 16 Console.WriteLine(p); 17 18 ((IChangeBoxedPoint) o).Change(5,5); 19 Console.WriteLine(o); 20 }
結果:前面四次的結果應該是顯而易見了,(1,1)(2,2) (2,2) (2,2),那麼第五次呢,來簡單分析一下p裝箱為IChangeBoxedPoint,然後把堆上對應的p的m_x,m_y改為4,4,但是對p輸出時堆上的內容不僅回收了,而且輸出的是原來p線程棧上的內筒,仍然還是剛剛的(2,2),第六步,o沒有任何裝箱拆箱操作,當然是預期的(5,5)