CLR-2-2-引用類型和值類型

引用類型和值類型,是一個老生常談的問題了。裝箱拆箱相信也是猿猿都知,可是仍是跟着CLR via C#加深下印象,看有沒有什麼更加根本和之前被忽略的知識點。數組

 

引用類型:ide

引用類型有哪些這裏不過多贅述,來關心一下它在計算機內部的實際操做,引用類型老是從託管堆分配,線程棧上存儲的是指向堆上數據的引用地址,首先確立一下四個事實:性能

 內存必須從託管堆分配spa

 堆上分配成員時,CLR要求你必須有一些額外成員(好比同步塊索引,類型對象指針)。這些成員必須初始化。線程

 對象中的其餘字節老是設爲零指針

 從託管堆上分配對象時,可能強制執行一次垃圾回收code

因此引用類型對性能是有顯著影響的。orm

 

值類型:對象

值類型是CLR提供的輕量級類型,它把實際的字段存儲在線程棧上blog

值類型不受垃圾回收器的限制,因此它的存在緩解了託管堆的壓力,也減小了垃圾回收的次數。

值類型都是派生自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)

相關文章
相關標籤/搜索