【C#】值類型和引用類型

工作之外暫沒有可以上手寫的東西,這周主要內容還是對C#一些關於類型的知識進行鞏固,涉及到的書籍主要是深入理解C#(第3版)

 

  • 值類型和引用類型

先從現實生活中的值和引用來討論這點:

       假設你正在讀一份報紙,覺得裏面的內容很棒,希望一個朋友也去讀,影印了報紙的全部內容並交給他。屆時,他將獲得屬於他自己的一份完整的報紙。在這種情況下,我們處理的是值類型的行爲。所有信息都在你的手上,不需要從任何其他地方獲得。製作了副本之後,你的這份信息和朋友的那份是各自獨立的。可以在自己的報紙上添加一些註解,他的報紙根本不會改變。
       再假設你正在讀的是一個網頁。與前一次相比,這一次,唯一需要給朋友的就是網頁的URL。這是引用類型的行爲,URL代替引用。爲了真正讀到文檔,必須在瀏覽器中輸入URL,並要求它加載網頁來導航引用。另一方面,加入網頁由於某種原因發生了變化(如一個維基頁面,你在上面添加了自己的註釋),你和你的朋友下次載入頁面時,都會看到那個改變。

.NET中大多數類型都是引用類型

 類(使用class來聲明)是引用類型,而結構(使用struct)來聲明是值類型。特殊情況包括:
1.數組類型是引用類型,即使元素類型是值類型(所以即便int是值類型,int[]仍是引用類型);
2.枚舉(使用enum來聲明)是值類型;
3.委託類型(使用delegate來聲明)是引用類型;
4.接口類型(使用interface來聲明)是引用類型,但可由值類型實現。

那麼,首先我們需要知道爲什麼要分爲值類型和引用類型呢: 

        引用類型總是從託管堆分配,C#的new操作符返回對象的內存地址,如果所有類型都是引用類型,則程序在運行的過程中,需要進行很多次內存分配,會顯著影響程序性能
       值類型的實例一般在線程棧上分配(雖然也可作爲字段嵌入引用類型的對象中),代表值類型實例的對象中不包含指向實例的指針,其不受垃圾回收器的控制。因此,值類型的使用緩解了託管堆的壓力,並減少了應用程序生存期內的垃圾回收次數。

可以這樣說:對於值類型的表達式,它的值就是表達式的值,與此同時,對於引用類型的值,它的值是一個引用,這個引用指向它在堆中的位置。我們用下一段代碼,通過對不同類型的變量內部進行值比較以及賦值等操作來理解前面提到的這些:

 

        static void Main(string[] args)
        {
            //定義一個值類型變量,其值存放在線程棧上
            int intVal = 0;
            //定義一個引用類型變量,其實際數據位於堆中,值(其引用)存放在線程棧上
            List<int> objListRef = new List<int>();

            bool bolResult;

            #region 變量值比較

            //先定義兩個新的變量,用來與前面的兩個變量作比較
            int intCompare = 0;
            List<int> objListCompare = new List<int>();
            bolResult = (intVal == intCompare);
            //這裏返回true,因爲這兩個值類型的值是相同的
            Console.WriteLine($"{nameof(intVal)}和{nameof(intCompare)}{(bolResult==true ? "":"不")}相等");

            bolResult = (objListRef == objListCompare);
            //這裏返回false,因爲這兩個值(這兩個變量的引用)位於堆中不同的位置
            Console.WriteLine($"{nameof(objListRef)}和{nameof(objListCompare)}{(bolResult == true ? "" : "不")}相等");

            #endregion

            #region 變量賦值

            intCompare = intVal;
            intCompare ++;
            //這裏會輸出0,因爲雖然intCompare的值加了1,但是其是值類型
            //intCompare = intVal把intVal的值賦給了intCompare,相當於於複製了一個intVal,其值和intCompare相同
            //所以對intCompare的更改不會作用到intVal
            Console.WriteLine($"{nameof(intVal)}:{intVal}");

            objListCompare = objListRef;
            objListCompare.Add(0);
            //這裏會輸出1,因爲這裏的變量是引用類型
            //objListCompare = objListRef表示把objListRef的值(它的引用)賦給objListCompare
            //因爲它們指向堆中的同一個位置,所以從此時開始,對objListCompare做的任何修改操作都會作用到objListRef
            Console.WriteLine($"{nameof(objListRef)}長度爲:{objListRef.Count}");

            objListCompare =  new List<int>();
            //這裏仍然會輸出1,因爲當objListCompare重新初始化,它的值和objListRef的值已經不是指向堆中的同一個位置
            //所以對它們的操作又重新變成對堆中不同數據的操作
            Console.WriteLine($"{nameof(objListRef)}長度爲:{objListRef.Count}");
            #endregion

        }

所以不難理解在日常使用方法的產生的一些疑惑: 

Question1:爲什麼日常使用一些方法時,方法參數用默認的val傳遞,在方法內部對傳遞過來的參數進行修改,在方法執行完畢後,調用位置的那個變量的數據不會發生改變(當參數爲值類型),數據發生改變(當參數爲引用類型) 呢?

Answer:在進行參數值傳遞時,對於值類型來說,相當於傳遞了它的值的一個副本,對於引用類型來說,相當於傳遞了它的引用的一個副本。所以在方法內部對值的副本進行改變,外部值類型變量本身的數據不會發生變化,而引用類型變量變化時(前提是不改變其引用),因爲它們都指向同一個地址,所以可以實現數據的改變;

Tips:值傳遞時,如果在方法內修改了參數的引用,則代表切斷了與外部變量的聯繫,即在值傳遞時,是無法在方法內部修改調用處變量的引用的實際值的,只是把這個值賦值過來“用”。如果想改變調用處變量的引用,需要使用ref傳遞,如下面的代碼:

        static void Main(string[] args)
        {
            List<int> objList = null;
            SomeByValMethod(objList);
            //這裏判斷的結果是等於null
            Console.WriteLine($"{nameof(objList)}{(objList==null ? "":"不")}等於null");
            SomeByRefMethod(ref objList);
            //這裏判斷的結果是不等於null
            Console.WriteLine($"{nameof(objList)}{(objList == null ? "" : "不")}等於null");
        }

        private static void SomeByValMethod(List<int> objList)
        {
            objList = new List<int>();
        }

        private static void SomeByRefMethod(ref List<int> objList)
        {
            objList = new List<int>();
        }

Question2:爲什麼ref傳遞時,在傳參時不能寫形如ref null這樣的形式,而是必須要傳一個實際的參數,但當一個引用類型的變量等於null,卻是可以這樣寫且不會報錯的呢?

Answer:關於爲何不能寫成ref null的形式,因爲不難看出這樣做的不合理性,因爲不可能“改變null在內存中指向的位置”;而當一個引用類型的變量等於null的時候,只是代表還沒有爲其在堆中重新分配內存,還未引用任何一個對象(內存中用全零來表示null),本質上它還是採用和其他引用一樣的方式來存儲的,所以在方法內部把這個變量的地址指向一個新的位置也就說得通了


今天就總結到這兒,以後根據個人理解可能會修改或者新增一些內容,晚安啦!(之前發到知乎沒什麼人看,以後還是在這兒更新吧)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章