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

来源:https://www.cnblogs.com/franhome/archive/2018/04/21/8901008.html
-Advertisement-
Play Games

引用類型和值類型,是一個老生常談的問題了。裝箱拆箱相信也是猿猿都知,但是還是跟著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)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在做這個SDRAM控制器之前,博主有一個疑問,對於學生來說,是否有必要學慣用純Verilog寫一個SDRAM控制器?因為目前X家和A家都有了DDR IP Core,對於要實現一個應用可以直接調用IP Core,只需要對其介面操作即可。對於開發者來說,與其費時費力用Verilog去寫一個性能差而且老的 ...
  • 轉載請註明出處:http://www.cnblogs.com/qm-article/p/8903893.html 一、介紹 在介紹該源碼之前,先來瞭解一下鏈表,接觸過數據結構的都知道,有種結構叫鏈表,當然鏈表也分多種,如常見的單鏈表、雙鏈表等,單鏈表結構如下圖所示(圖來自百度) 有一個頭結點指著下一 ...
  • 內容:流的分類,文件寫入(位元組輸出流),異常處理,獲取一個文件夾下的特定文件集合 位元組流的抽象基類:InputStream,OutputStream字元流的抽象基類:Reader,Writer由這四個類派生出來的子類名稱都是以父類名作為子類名的尾碼。如:InputStream的子類FileInput ...
  • 第一個 Python 程式 目標 第一個 程式 與 版本簡介 執行 程式的三種方式 解釋器 —— / 互動式 —— 集成開發環境 —— 01. 第一個 程式 1.1 Python 源程式的基本概念 1. Python 源程式就是 一個特殊格式的文本文件 ,可以 使用任意文本編輯軟體 做 的開發 2. ...
  • C++多態 Polymorphism 本文為C++官網對多態闡述:http://www.cplusplus.com/doc/tutorial/polymorphism/ 在瞭解多態前需具備如下知識:類、結構體、友元和繼承。 指向基類的指針 先來看一個基礎例子 1 // pointers to bas ...
  • 作者: "zyl910" 一、緣由 NLog是一個很好用的日誌類庫。利用它,可以很方便的將日誌輸出到 調試器、文件 等目標,還支持輸出到窗體界面中的RichTextBox等目標。 而且它還支持在運行時修改配置,例如可用於實現這樣的需求——在界面上做個下拉框,可動態調整RichTextBox的日誌級別 ...
  • 問題描述:由於最近項目需要使用Mac地址與註冊碼進行加密處理,但是又因為Web程式的局限性不能獲取客戶端電腦系統信息,當然IE瀏覽器有一個activex控制項他是可以通過Js在前端代碼中直接獲取的,局限性太小放棄。我的實現方法是通過windows服務嵌套一個HttpService服務實現。本人初級菜鳥 ...
  • 一、本文產生原由: 之前文章《總結消息隊列RabbitMQ的基本用法》已對RabbitMQ的安裝、用法都做了詳細說明,而本文主要是針對在高併發且單次從RabbitMQ中消費消息時,出現了連接數不足、連接響應較慢、RabbitMQ伺服器崩潰等各種性能問題的解方案,之所以會出現我列舉的這些問題,究基根源 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...