Value type vs Reference type in C#

来源:http://www.cnblogs.com/xiaodongy/archive/2017/12/17/7989711.html
-Advertisement-
Play Games

[MY NOTE] [轉載請註明出處] Reference Source: http://www.albahari.com/valuevsreftypes.aspx http://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in ...


[MY NOTE]  

[轉載請註明出處]

Reference Source:

http://www.albahari.com/valuevsreftypes.aspx

http://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-reference-type-parameters#passing-reference-types-by-value

註:下麵的示意圖主要是為了輔助理解,不代表記憶體真實情況。

Introduction

    類型基礎是C#的基礎概念,瞭解類型基礎及背後的工作原理更有助於我們在coding的時候明白數據在記憶體中的分配與傳遞,以及解決一些不明原因的bug和寫出效率更高的程式。C#提供了值類型和引用類型,值類型如struct, 引用類型如class。 這裡主要說明一下它們在記憶體分配與傳遞上的區別, 具體有哪些值類型和引用類型,可以去www.baidu.com。

記憶體分配

    首先要瞭解一下記憶體中棧和堆的概念。

    棧(Stack)

     ##棧是一種先進後出的記憶體結構。

     方法的調用追蹤就是在棧上完成的。比如我們有一個main方法(程式入口), 在main方法中會調用一個GetPoint的方法。線上程執行時,會將main方法壓入棧底(包括編譯好的方法指令,參數,和方法內部變數),然後再將GetPoint的方法壓入棧底,GetPoint中沒有調用其它方法,壓棧完畢。出棧順序是先進後出,也就是後進先出,棧頂的方法GetPoint先執行完畢,然後出棧,所占記憶體清空,接著main方法執行後出棧,所占記憶體清空。

//示意圖:自己腦補吧...

  從上面方法的壓棧出棧中可以看出:

     ##棧只能在一端對數據進行操作,也就是棧頂端進行操作。’

     ##棧也是一種記憶體自我管理的結構,壓棧自動分配記憶體,出棧自動清空所占記憶體。

  另外值得註意的兩點:

     ##棧中的記憶體不能動態請求,只能為大小確定的數據分配記憶體,靈活性不高,但是棧的執行效率很高。

     ##棧的可用空間並不大,所以我們在操作分配到棧上的數據時要註意數據的大小帶來的影響。

   堆(Heap)

     ##堆與棧有所區別,堆在C#中用於存儲實實例對象,能存儲大量數據,而且堆能夠動態分配存儲空間。

     ##相比棧只能在一端操作,堆中的數據可以隨意存取。

     ##但堆的結構使得堆的執行效率不如棧高,而且不能自動回收使用過的對象。對於堆中的記憶體回收,C++程式員需要進行手動回收,這也是C++編程值得註意的一點,否則很容易造成記憶體溢出。而對於.NET程式員,平臺提供了垃圾回收(GC)機制,可以自動回收堆中過期的對象(實現原理大概就是當發現沒有“引用”指向此對象時,表明此對象可以回收,此文主要討論值類型和引用類型,對於GC,感興趣的可以搜索相關資料)。

    值類型和引用類型在棧和堆中的分配

    這兒有兩個原則:

    1.創建引用類型時,runtime會為其分配兩個空間,一塊空間分配在堆上,存儲引用類型本身的數據,另一個塊空間分配在棧上,存儲對堆上數據的引用,實際上存儲的堆上的記憶體地址,也就是指針。

    2.創建值類型時, runtime會為其分配一個空間,這個空間分配在變數創建的地方,如:

       ##如果值類型是在方法內部創建,則跟隨方法入棧,分配到棧上存儲。

      ##如果值類型是引用類型的成員變數,則跟隨引用類型,存儲在堆上。

   在此我們舉例說明。

 定義一個Point類:  

 public class Point
   {
        public double PointX { get; set; }
        public double PointY { get; set; }   
    }

StartProgram類,有方法Start()和InitialPoint():

  class StartProgram
    {
        void Start()
        {
            double pointX = 100.1;
            InitialPoint(pointX);
        }
        void InitialPoint(double pointX)
        {
            var point = new Point();
            point.PointX = pointX;
        }
    }

示例分析:假設主線程從Start()進入執行,我們從分析一下方法中的變數在記憶體中的大致分配情況,不深究細節。

 首先將Start()方法指令壓入棧底,然後壓入局部變數pointX;緊接著將InitialPoint()方法壓入棧底,形參pointX壓入棧底,在堆上實例化Point對象(包括其成員變數PointX和PointY),併在棧上創建point變數指向堆上的Point對象,最後給成員變數PointX賦值,參考圖如下:

    註:註意不要混淆code中的pointx,雖然變數名相同,但是它們是不同的變數。 

      

 

數據傳遞

   按值傳遞原則

   在C#中數據傳遞預設按值傳遞,先看一個示例。

 現在有一個結構體PointSturct, 一個類PointClass:

  public struct PointStruct
    {
        public double PointX { get; set; }
        public double PointY { get; set; }   
    }
  public class PointClass
    {
        public double PointX { get; set; }
        public double PointY { get; set; }   
    }

併在一個方法中執行執行以下代碼:

1  void Excute()
2  {
3       var pointStruct1 = new PointStruct();
4       var pointClass1 = new PointClass();
5       var pointStruct2 = pointStruct1;
6       var pointClass2 = pointClass1;
7   }

示例分析:第3,4行代碼分別創建了一個結構體pointStruct1和一個類實例pointClass1, 結合上面的記憶體分配規則,對於pointSturct1,會在棧上分配記憶體存儲其數據本身,對於pointClass1,會在堆上分配記憶體存儲實例,且在棧上存儲指向堆上實例的指針,參考圖如下:

     經過執行5,6行代碼後,記憶體分配應該是怎樣的呢? 對於值類型(pointStruct1),會在棧上開闢一塊新的空間,將數據完全複製過去,因此pointStruct2和pointStruct1是互相獨立的,對其中一個的修改不會影響到另一個;對於引用類型(pointClass1),也會在棧上開闢一個新的空間,將棧上的數據(指向堆上實例的指針)複製到新的空間, 但是註意,此處複製的是指針,也就是說棧上的兩個變數pointClass1和pointClass2雖然是不同的空間,但是它們的存儲內容---指針(記憶體地址), 都是指向堆上的同一實例,所以當通過pointClass2對實例的數據進行修改以後,通過pointClass1再訪問實例的數據,將會是修改過的數據,反之亦然,參考圖如下:

    參數傳遞

     當程式中進行參數傳遞的時候,也是預設按值傳遞,值類型複製數據本身,形成獨立的數據塊,引用類型複製引用,指向同一實例。簡單一點就是傳遞時複製棧上的數據到新的棧上空間。

我們將之前的StartProgram類中的方法改成如下 :

class StartProgram
{
   void Start()
   {
      double pointX1 = 100.1;
      var point1 = new Point();
      point1.PointX = 200.1;
      InitialPoint(pointX1, point1);
      Console.WriteLine(string.Format("pointX1:{0}", pointX1));
      Console.WriteLine(string.Format("point1.PointX:{0}", point1.PointX));
      Console.ReadKey();
    }
    void InitialPoint(double pointX2, Point point2)
    {
       pointX2 = 300.1;
       point2.PointX = pointX2;
    }
 }
/*Output:pointX1:100.1
         point1.PointX:300.1 
*/

示例分析:從輸出結果可以看到,pointX1還是原來的值,沒有受到pointX2影響,而point1.PointX的值是point2對PointX更改後的值。在記憶體中,將值類型pointX1傳遞給pointX2後,在棧上形成兩個獨立的記憶體塊,因此對pointX2更改後,並不會影響到pointX1;而對於引用類型point1,傳遞給point2後,它們兩塊記憶體存儲的指針指向同一實例,因此再InitialPoint()方法內對point2.PointX賦值為300.1後,再Start()方法裡面取point1取PointX的值,也是300.1。

既然point1和point2指向同一實例,那麼如果我們在InitialPoint()方法的最後將point2設置為null,會不會影響到Start()方法里的point1呢?用point.PointX取值的時候,會不會得到實例為null的異常呢?

 void InitialPoint(double pointX2, Point point2)
 {
    pointX2 = 300.1;
    point2.PointX = pointX2;
    point2 = null;
 }
 /*Output:pointX1:100.1
          point1.PointX:300.1 
 */

示例分析:還是會得到之前的結果,沒有檢測到null異常。這不難想象,因為我們將point2設置為null,並不是將堆上的實例變為null,而是設置棧上的point2這塊存儲指針的記憶體為null,而棧上point1和point2雖然指向同一實例,但是它們是兩塊不同的記憶體,所以將point2設置為null後,point1仍然指向堆上的實例,並且point2設置為null是在對堆上的實例進行更新以後,因此point1.PointX的到的值是更新後的值,參考圖如下:

    按引用傳遞(Ref和Out關鍵字)

    註:Ref和Out的區別在於Ref在傳遞前需要初始化。

   我們知道C#中的Ref和Out關鍵字可以在值類型的傳參上實現跟引用類型一樣的效果,那麼在引用類型參數上加入ref和out關鍵字跟預設的引用類型傳參有什麼區別呢?很多人覺得應該沒有什麼用,其實不然,我們繼續將StartProgram類的方法改為按ref傳遞,看看會有什麼不同。

class StartProgram
{void Start()
    {
        double pointX1 = 100.1;
        var point1 = new Point();
        point1.PointX = 200.1;
        InitialPoint(ref pointX1, ref point1);
        Console.WriteLine(string.Format("pointX1:{0}", pointX1));
        if (point1 != null) Console.WriteLine(string.Format("point1.PointX:{0}", point1.PointX));
        else Console.WriteLine(string.Format("point1 is null"));
        Console.ReadKey();
    }
    void InitialPoint(ref double pointX2, ref Point point2)
    {
        pointX2 = 300.1;
        point2.PointX = pointX2;
        point2 = null;
    }
    /*Output:
pointX1:300.1 point1 is null
*/
}

示例分析:從運行結果可以看到,對於值類型, pointX2對值的更改影響到了pointX1;對於引用類型,將point2設置為null後,point1也變成了null,之前我們沒有加ref參數的時候,point2設置為null,並不會影響到point1本身。我們可以看到,通過加入ref和out參數後,在記憶體中並不是像值傳遞一樣將棧上的數據拷貝一份到新的空間。在這裡,我並沒有去研究C#對ref和out參數在記憶體上的實現原理,但是可以想到,要實現這種效果並不難,在按引用傳遞時我們將棧上的變數的地址(如存儲pointX1,point1的記憶體地址)copy到新的棧記憶體空間中,這樣就可以將新的變數和舊的變數關聯起來,達到互相影響的效果。

Summary

   本文從記憶體中棧和堆的結構特點出發,分析了C#值類型和引用類型在棧和堆上的分配情況,接著分析了數據傳遞過程,包括按值傳遞(賦值,參數傳遞),按引用傳遞(ref,out關鍵字),僅供參考。


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

-Advertisement-
Play Games
更多相關文章
  • 使用fmsb包繪製雷達圖 {r} library("fmsb") radarfig ...
  • 1.解釋器路徑 2.編碼 1.ascill 00000000 (8個位表示) 缺點:表示不了英文 2.unicode 0000000000000000+ (至少16位表示) 缺點:消耗記憶體,當表示位不需要16位以上,造成多餘記憶體消耗 Python3 無需關註 Python2 每個文件中只要出現中文, ...
  • [TOC] 一、簡介 java中的日期處理一直是個問題,沒有很好的方式去處理,所以才有第三方框架的位置比如joda。 文章主要對java日期處理的詳解,用1.8可以不用joda。 1. 相關概念 首先我們對一些基本的概念做一些介紹,其中可以將GMT和UTC表示時刻大小等同。 1.1 UT時間 UT反 ...
  • 一、簡介 JavaBean組件是一些可移植、可重用並可組裝到應用程式中的Java類,類必須是具體的和公共的。 符合下列設計規則的任何Java類均是以JavaBean: 1.對數據類型“protype”的每個可讀屬性,Bean下必須有下麵簽名的一個方法:public proptype getPrope ...
  • 1 import java.util.HashMap; 2 import java.util.Iterator; 3 import java.util.Map; 4 5 public class TestMap { 6 public static void main(String[] args) {... ...
  • 背水一戰 Windows 10 之 控制項(自定義控制項): 自定義控制項的 Layout 系統, 自定義控制項的控制項模板和事件處理的相關知識點 ...
  • 前言 ASP.NET Core 2.0 怎麼發佈到Ubuntu伺服器?又如何在伺服器上配置使用ASP.NET Core網站綁定到指定的功能變數名稱,讓外網用戶可以訪問呢? 步驟 第1步:準備工作 一臺Liunx伺服器:筆者用的是【[搬瓦工][1]】的VPS伺服器(CDN加速,支持支付寶,多機房選擇) 低配版 ...
  • 閱讀目錄 前言 成熟的解決方案 剖析 性能測試 結語 一、前言 在上一篇分散式系統系列中《分散式系統中的必備良藥 —— 服務治理》中闡述了服務治理的一些概念,那麼與服務治理配套的必然會涉及到RPC框架。在當前互聯網的大背景下,RPC的運用應該大家或多或少都有涉及,國內外的RPC框架也是百花齊放。那麼 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...