c#基礎系列1---深刻理解值類型和引用類型

「大菜」:源於本身剛踏入猿途混沌拾起,自我感受不是通常的菜,於是得名「大菜」,於自身共勉。面試

不知不覺已經踏入坑已10餘年之多,對於c#多多少少有一點本身的認識,寫出來渴求同類抨擊,對本身也算是個十年之癢的一個總結。據說有美女圖點讚的人多c#

基本概念

CLR支持兩種類型:值類型和引用類型。 面試過不少5年左右的同窗,有不少連值類型和引用類型的基本概念都回答不上來,難道如今的c#開發人員基礎這麼弱了嗎?仍是你們都不重視基礎呢?這個隨便找一篇博客均可以基礎入門的。安全

image

引用類型

哪些類型是引用類型呢?其實一個能夠稱爲」類「的類型都是引用類型。 引用類型老是從託管堆上分配的,經常使用的語法就是New XX(). C#的new 操做符會返回對象的指針 - 也就是指向對象數據的內存地址的一個引用。引用類型的傳遞其實傳遞的是對象的指針(string類型比較特殊),因此在特定的場景下性能是高於值類型的。一個引用類型在建立時默認爲null,也就是說當前變量不指向一個有效的對象,也就是咱們常遇到的異常「未將對象引用設置到對象的實例」。微信

值類型

由於引用類型變量都須要進行一次堆內存的分配,這會給GC形成很大的壓力,因此CLR提供了輕量級類型「值類型」。 值類型通常在線程棧上分配。(注意:值類型能夠嵌入一個引用對象中)一個值類型變量其實就包含了值類型實例的值,因此它沒有引用類型的指針(你們猜測值類型需不須要類型對象指針呢?markdown

相同點和不一樣點

相同點

  1. 值類型和引用類型都是System.Object的子類
  2. 值類型和引用類型均可以繼承接口。(不少人都認爲值類型不能繼承接口)網絡

    interface Itest
     {
         void test();
     }
     struct TestStruct : Itest
     {
         public void test()
         {
             throw new NotImplementedException();
         }
     }

不一樣點

  1. 值類型分配在堆棧上,引用類型是在託管堆上分配的。這裏須要指出一點:若是一個引用類型中的某個屬性是值類型,這個值類型的屬性是分配在託管堆上的
  2. 全部的值類型都是隱式密封的(sealed),例如 :你不可能繼承int 來構造本身的類型。
  3. 值類型的每一次賦值都會執行一次逐字段的複製,因此若是是頻繁賦值也會形成性能上的壓力,引用類型的賦值只是指針的傳遞,其實也是生成新的指針實例。
  4. 引用類型額外有類型對象指針和同步塊索引,值類型是沒有的。因此咱們平時使用lock 鎖的對象不多是值類型,由於值類型沒有同步塊索引

image

性能

有的同窗說值類型的性能高於引用類型,那爲何不都用值類型呢?引用類型也是如此。任何東西都有兩面性,只有合適的類型,沒有萬能的類型。函數

  1. 值類型:所謂的.net Framework中的「輕量類型」,爲何說是「輕量」呢,這和他的內存分配有直接關係,由於值類型是分配在棧上,因此在GC的控制以外,不會對GC形成壓力。那是否是能夠隨便用呢?固然不是,舉個例子:我自定義一個struct 類型做爲一個方法的參數會發生什麼呢?每次調用都會發生全字段的賦值,這是不可接受的,這也是典型的值類型勿用場景。
  2. 引用類型:引用類型分配在堆中,因此會影響GC,若是頻繁的初始化引用類型,對GC的壓力是很大的,由於每一次分配都有可能會強制執行一次垃圾收集操做。另外提一點,引用類型的所佔內存,並不是全部屬性/字段的和,堆上分配的每一個對象都有一些額外的成員,這些成員必須初始化。(類型對象指針和內存塊索引)。
  3. 裝箱拆箱:所謂裝箱就是將值類型轉化爲引用類型的過程。拆箱則相反(只是概念上相反,實際編譯器的操做不同)。有的同窗說裝箱拆箱影響性能,那究竟是裝箱影響呢仍是拆箱呢仍是都影響呢?
    1. 裝箱發生了什麼過程呢:
      1. 在託管堆中分配好內存,分配的內存量是值類型的各個字段須要的內存量加上託管堆上因此對象的兩個額外成員(類型對象指針,同步塊索引)須要的內存量
      2. 值類型的字段複製到新分配的堆內存中
      3. 返回對象的地址,這個地址就是這個對象的引用
    2. 拆箱發生了什麼過程呢:
      1. 獲取已經裝箱的值類型實例的指針
      2. 把獲取到的值複製到棧

因此裝箱是比較耗費性能的,還有可能引起一次GC操做,而拆箱只是一個獲取指針的過程耗費資源要比裝箱小的多。注意:一個對象拆箱以後只能還原爲原先未裝箱以前的類型,例如:你不能把int32類型裝箱後還原爲int16類型。 因此面試的時候能夠和麪試官裝B一下了~~性能

image

測試例子

值類型引用類型分別初始化N次消耗的時間,代碼以下
static void Main(string[] args)
    {
        Console.WriteLine("test start");
        int totalCount = 10000000;
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < totalCount; i++)
        {
            TestRef temp = new TestRef() { Id = i, Name = "test" };
        }
        sw.Stop();
        Console.WriteLine($"引用類型耗時:{sw.ElapsedMilliseconds}");
        sw.Reset();
        sw.Start();

        for (int i = 0; i < totalCount; i++)
        {
            TestVal temp = new TestVal() { Id = i, Name = "test" };
        }
        sw.Stop();
        Console.WriteLine($"值類型耗時:{sw.ElapsedMilliseconds}");
        Console.Read();
    }

    class TestRef
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    struct TestVal
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

運行結果:測試

引用類型耗時:205
值類型耗時:152

可見初始化速度值類型是優於引用類型的,也多是引用類型引起了GC致使。.net

做爲方法參數傳遞,代碼以下:
static void Main(string[] args)
    {
        Console.WriteLine("test start");
        long totalCount = 1000000000;
        Stopwatch sw = new Stopwatch();
        sw.Start();

        TestRef tempRef = new TestRef() { Id = 1, Name = "test" , Name2="r3rewfdsafdsa", Name3="fsrewfdsafdsafdsa", Name4="fdafdasfdsafdsa", Name5="432tretsfds", Name6="fdsafdasfdasfd" };
        for (int i = 0; i < totalCount; i++)
        {
            TestR(tempRef);
        }
        sw.Stop();
        Console.WriteLine($"引用類型耗時:{sw.ElapsedMilliseconds}");
        sw.Reset();
        sw.Start();
        TestVal tempVal = new TestVal() { Id = 1, Name = "test", Name2 = "r3rewfdsafdsa", Name3 = "fsrewfdsafdsafdsa", Name4 = "fdafdasfdsafdsa", Name5 = "432tretsfds", Name6 = "fdsafdasfdasfd" };
        for (int i = 0; i < totalCount; i++)
        {
            TestV(tempVal);
        }
        sw.Stop();
        Console.WriteLine($"值類型耗時:{sw.ElapsedMilliseconds}");
        Console.Read();
    }
    static void TestR(TestRef r)
    {
        return;
    }
    static void TestV(TestVal v)
    {
        return;
    }

    class TestRef
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Name2 { get; set; }
        public string Name3 { get; set; }
        public string Name4 { get; set; }
        public string Name5 { get; set; }
        public string Name6 { get; set; }
    }
    struct TestVal
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Name2 { get; set; }
        public string Name3 { get; set; }
        public string Name4 { get; set; }
        public string Name5 { get; set; }
        public string Name6 { get; set; }
    }

運行結果:

引用類型耗時:4437
值類型耗時:5226

可見在普通狀況下,做爲參數值類型和引用類型用時差距不大,可是,若是值類型的實例屬性比較多的狀況下差距降進一步拉大。

非正式環境測試用例,結果僅供參考

應用場景

不止是面試的時候常常問應用場景這個問題,就是本身平時寫程序也應該清楚。程序設計選擇的時候大部分場景都是用引用類型,可是若是你知足下列條件,值類型可能更適用:

  1. 類型不會派生出任何其它類型,也就是說不會有被繼承的可能
  2. 類型不須要繼承其餘類型
  3. 類型的實例比較小,而且不會被做爲方法參數,不會被頻繁賦值
  4. 你永遠不會用到類型釋放時候的通知,由於引用類型利用析構函數能夠利用其餘手段能夠獲得釋放時候的通知。
  5. 若是你的類型實例不會發生值的改變或者能夠認爲是readonly性質的,值類型或許是首選。

    其餘

  6. 全部的值類型都從System.ValueType 派生,System.ValueType繼承System.Object,可是System.ValueType 重寫了Equals 和GetHashCode 方法,其實在這裏纔是真正和引用類型的分割線。
  7. 由於值類型有裝箱拆箱的操做,因此像ArrayList這樣的集合性能是很是使人擔心的。因此c# 2.0 出現了泛型 例如:List .....來保證了類型安全,同時又避免了拆箱裝箱,由於不是我定義的類型 ,你TMD根本連編譯器那一關都過不了 哈哈哈~~~~

順便說一句,很久不寫博客,樣式真實花時間啊,後來乾脆寫markdown格式的,請你們見諒!!

請尊重一個猿的辛苦,轉載請標明出處 ^ ~ ^  。部分圖片來源於網絡,若是侵權請及時聯繫我。讓咱們一塊兒進步吧

一個不止於IT圈內容的微信公衆號,歡迎關注,交流更多的IT知識。不定時會有驚喜奧 ^ ~ ^

相關文章
相關標籤/搜索