1. 程式人生 > >C#的值型別和引用型別解析

C#的值型別和引用型別解析

值型別:byte,sbyte,short,int,long,float,double,decimal,char,uint,ushort,ulong,bool ,列舉型別,使用者定義的結構體struct

引用型別:class、delegate、dynamic、interface、object(Object)、string(String)、內插字串(這個樣子的:$"Name = {name}, hours = {hours:hh}")。


特殊情況:

(1)陣列。陣列是引用型別,陣列的物件在棧上。其每一個元素均分配在託管堆上。如果是值型別陣列(int,float....)那麼會在堆上對陣列自動進行初始化。如果是引用型別陣列,那麼不會初始化任何元素。此時陣列為null。

(2)巢狀型別:如果宣告一個類,該類中包含值型別的欄位,同時包含值型別的區域性變數,那麼它同樣是引用型別。但是值型別欄位和值型別區域性變數位置是不同的。

例如:    

class A

    {

        int a;      //值型別欄位

        public A()

        {

            int b;      //值型別區域性變數

        }

}

欄位跟隨例項儲存,所以值型別欄位a儲存在託管堆上。但是這裡的b是區域性變數,它是值型別,它在棧上。

三、注意事項

1.值型別測試

using System;

namespace ValueAndReference

{

    class Program

    {

        static void Main(string[] args)

        {

            int VT1, VT2;       //值型別變數VT1,VT2

            VT1 = 5;

            VT2 = VT1;

            VT1 = 6;

            Console.WriteLine("VT1 is " + VT1 +"  VT2 is " + VT2);      //傳遞的只是值

        }

    }

}

這個沒啥可注意的,很正常。

2.引用型別測試(class)

using System;

namespace ValueAndReference

{

    //引用型別定義

    class ReferenceType

    {

        public int field;      //定義欄位

        public ReferenceType(int v)       //建構函式,初始化field

        {

            field = v;

        }

    }

    class Program

    {

        static void Main(string[] args)

        {

            ReferenceType RT1 = new ReferenceType(5);        //定義並例項化引用型別物件RT1,初始欄位值為5

            ReferenceType RT2 = RT1;

            Console.WriteLine("RT1 is " + RT1.field +",RT2 is " + RT2.field);        //輸出RT1、RT2欄位值,均為5

            RT2 = new ReferenceType(6);     //例項化RT2,並初始欄位值為6

            Console.WriteLine("RT1 is " + RT1.field +",RT2 is " + RT2.field);        //輸出RT1欄位值為5、RT2欄位值為6

            ReferenceType RT3 = new ReferenceType(5);        //定義並例項化引用型別物件RT3,初始欄位值為5

            ReferenceType RT4 = RT3;

            Console.WriteLine("RT3 is "+RT3.field+",RT4 is "+RT4.field);        //輸出RT3、RT4欄位值,均為5

            RT4.field = 6;  //更改RT3的欄位值為6

            Console.WriteLine("RT3 is " + RT3.field +",RT4 is " + RT4.field);        //輸出RT3、RT4欄位值,均為6

        }

    }

}

這個比較需要注意,RT1的field欄位初始為5,然後RT1賦值給RT2,由於RT2是引用變數,那麼修改RT2就是修改RT1,可是為什麼RT1在後邊沒變化呢?這是因為這種情況下RT1和RT2指向的不是一個堆的地址,具體可以看下邊的圖。RT3和RT4是正常的情況,修改了RT4的值,RT3同時跟著變化。關於這兩種情況其實很簡單,而且這個很好的反應了引用型別的性質。為什麼第一種情況不行呢?是因為第一種情況改變了引用物件本身,重新進行了例項化,那麼就是重新聲明瞭一個新的例項。而第二種情況改變的是物件的屬性,兩個物件還是對應的同一個例項。看下面兩幅圖:

第一種情況:


第一次例項化RT2時拷貝的RT1,此時RT1、RT2是一個物件。接下來的例項化是對RT2本身的改變,並且在這裡是會重新分配RT2地址的。所以這個RT2是一個全新的例項,與RT1沒有任何聯絡。所以它再怎麼改欄位的值也不會在影響RT1的欄位。

第二種情況:


這裡修改的只是欄位,所以修改後並沒有產生新的例項,那麼RT1、RT2對應的還是一個例項。修改任何一個的欄位值,另外一個會同時變化。

以上這些情況同樣適用於方法中的引數,如果在方法中重新例項了物件,那麼原始的物件在方法結束時不會做出任何改變,如果僅是改變物件例項的欄位,那麼沒有任何問題,可以修改。如果想要在方法中對物件的例項本身做修改,使用ref傳遞引數,這個可以做到。


3.引用型別測試(string)

using System;

using System.Runtime.InteropServices;

namespace ValueAndReference

{

    class Program

    {

        static void Main(string[] args)

        {

            string STR1 ="aaaaa";

            string STR2 = STR1;

            Console.WriteLine("STR1 is " + STR1+",STR2 is "+STR2);     //輸出STT1、STR2

            STR2 = "bbbbb";

            Console.WriteLine("STR1 is " + STR1 +",STR2 is " + STR2);  //輸出STT1、STR2

        }

    }

}

最後在說下這個string(String),在這裡STR1並沒有隨著STR2的變化而變化,這麼看起來它確實像值型別。但實際上它確實是引用型別。為什麼出現這種情況那,這是因為string做了運算子過載這樣看起來是string更像是字串。可以把它這個賦值理解為new,這樣就和上邊的class一樣了,所以這種情況是正常的。