c#中值型別和引用型別的基礎教程
前言
值型別和引用型別,是c#比較基礎,也必須掌握的知識點,但是也不是那麼輕易就能掌握,今天跟著老胡一起來看看吧。
典型型別
首先我們看看這兩種不同的型別有哪些比較典型的代表。
典型值型別
int,long,float,double等原始型別中表示數字的型別都是值型別,表示時間的datatime也是值型別,除此之外我們還可以通過關鍵字struct自定義值型別。
典型引用型別
原始型別中,array,list,dictionary,queue,stack和string都是引用型別,除此之外我們通過關鍵字class自定義引用型別。
基類
c#中所有的型別都最終繼承自Object,這是沒有疑問的,但是這其中還有些微區別。
值型別基類
對於值型別來說,除了最終繼承自Object,還繼承自ValueType,繼承鏈如下
但是請不要誤解,這裡僅僅指的是值型別天然是ValueType,但是不代表值型別能夠這麼宣告
struct Struct1 : ValueType { }
這樣是會引起編譯錯誤的,值型別不能繼承任何其他型別,值型別只能實現介面,不能繼承自其它型別。只有引用型別既可以實現介面也能繼承自其它型別。順便說一下,還有一點比較重要的是,ValueType重寫了Object基類的Equals方法和GetHashCode方法,所以當使用Equals比較兩個值型別的時候,系統會比較兩個值型別的各個屬性是否相等,再返回結果,這就是所謂的相等性。與此相對,引用型別在使用Equals的時候,會在後臺呼叫object.ReferenceEquals,換言之,引用型別在比較相等性的時候會考慮同一性。
引用型別基類
對於引用型別就沒有那麼麻煩,引用型別不會繼承自ValueType。引用型別可以繼承其他型別。
在記憶體中的表現
我們都知道,C#將記憶體分為了兩部分,一個是Stack,另外一個是Managed Heap。一般來說,用於函式呼叫進棧,函式返回出棧,用的是Stack,而當創造一個新的例項時,會根據建立的例項屬於值型別還是引用型別決定使用Stack還是Managed Heap。
值型別在記憶體中
當建立一個值型別物件時,c#會在Stack上面建立一塊空間,這塊空間就存放這個值型別物件。
int是一個典型的值型別,如下語句
int age = 10;
會存在於記憶體中的Stack上面。
如果把值型別的例項賦值給另外一個值型別,那麼效果就是複製一個新的值型別例項。
int myAge = age;
引用型別在記憶體中
與值型別在記憶體中的表現不一樣,建立一個引用型別的例項,不但會在Stack上面新建一個引用,還會在Heap上面劃分出記憶體以容納該引用型別例項。使用者在使用的時候通過Stack上面的變數間接引用該例項。
class Author { public string Name{get;set;} public int Age{get;set;} } Author author = new Author(){Name="deatharthas",Age= 32};
注意看和值型別在記憶體中的區別,引用型別通過Stack上的變數訪問位於Heap上面的例項。
在賦值的時候,拷貝的僅僅是Stack上面的變數,新拷貝出來的物件和舊的物件指向的是同一塊記憶體。
Author myAuthor = author;
這個時候,author和myAuthor指向同一塊記憶體,稱為同一性,通過呼叫
object.ReferenceEquals(myAuthor,author);
可以得到驗證。
但可能有細心的朋友會有疑問了,不是說int是值型別,值型別是存在於Stack上面的嗎?為什麼在author類裡面,它會在Heap裡面呢?贊一個細心!值型別一般存在於Stack上面,但如果某個值型別包含於引用型別,那麼它也會隨著那個引用型別存放在Heap上面。
當引數時的行為區別
c#中的引數傳遞預設都是傳值(by value),但是根據所傳遞物件是值型別還是引用型別,它們的行為還是有所區別,現在我們來看看。
值型別當引數
值型別當引數的時候,傳遞到函式內部的是一份值型別的拷貝,所以在函式內部修改這個拷貝不會影響原物件。除非我們在傳遞引數的時候使用了ref或者out。
引用型別當引數
如果引數是引用型別,傳遞到函式內部的依然是一份拷貝,但是這個拷貝是其在Stack上面的變數的拷貝,就像上面的賦值那個例子。所以這個時候這份拷貝其實和原物件指向同一塊記憶體(指向同一性),修改這個物件可以反映到原物件上面。
謹慎返回引用型別
程式設計是一項需要謹慎的工作,有時候我們經常會犯一些錯誤,而這些錯誤又是那麼的不明顯以至於不摔坑幾次,我們根本察覺不了,考慮下面一個例子。
class People { public string Name { get; set; } public int Age { get; set; } private People _Father = null; public People Father { get { return _Father; } } public People(People father) { _Father = father; } public void ShowFather() { Console.WriteLine("father's name is " + Father.Name + " and his age is " + Father.Age); } } class Program { static void Main(string[] args) { People father = new People(null) { Name = "father",Age = 60 }; People son = new People(father); son.ShowFather(); Console.ReadLine(); } }
看起來沒什麼問題,對吧?Father沒有提供setter,似乎是安全的。但是我們試試下面的程式碼。
static void Main(string[] args) { People father = new People(null) { Name = "father",Age = 60 }; People son = new People(father); var f = son.Father; f.Name="Changed"; son.ShowFather(); Console.ReadLine(); }
看,發現了什麼,外部改變了本來應該被封裝所保護的Father屬性,封裝被破壞了!
稍微一想我們應該能明白這個道理,Father屬性返回的拷貝的變數和原Father變數指向同一塊例項。要想解決這個問題,我們要麼返回一個值型別,要麼返回一個全新的物件。修改Father屬性如下:
public People Father { get { return new People(_Father._Father) { Name = _Father.Name,Age = _Father.Age }; } }
再次測試,
這次封裝就沒問題了。
總結
我們大概知道了值型別和引用型別的區別,包括它們的行為,在記憶體的居住方式,以及使用引用型別時可能會遇到的暗坑,希望大家通過閱讀這篇文章,能夠加深一些對它們的瞭解,少走一些彎路。
今天也簡單的提到了比較時的同一性,和預防封裝被破壞所採用的返回一個新的例項拷貝的策略(這個時候適合使用DeepCopy),我們之後有機會再詳細聊。
到此這篇關於c#中值型別和引用型別的文章就介紹到這了,更多相關c#值型別和引用型別內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!