在C#9.0下,record是一個關鍵字,微軟官方目前暫時將它翻譯爲記錄類型。編程
傳統面向對象的編程的核心思想是一個對象有着惟一標識,封裝着隨時可變的狀態。C#也是一直這樣設計和工做的。可是一些時候,你就很是須要恰好對立的方式。原來那種默認的方式每每會成爲阻力,使得事情變得費時費力。若是你發現你須要整個對象都是不可變的,且行爲像一個值,那麼你應當考慮將其聲明爲一個record類型。函數
因此record類型的實際是一個引用類型 ,可是他具備值類型的行爲。ui
先來回顧一下引用類型,C# 中有兩種類型:引用類型和值類型。 引用類型的變量存儲對其數據(對象)的引用,而值類型的變量直接包含其數據。 對於引用類型,兩種變量可引用同一對象;所以,對一個變量執行的操做會影響另外一個變量所引用的對象。 對於值類型,每一個變量都具備其本身的數據副本,對一個變量執行的操做不會影響另外一個變量。spa
那咱們舉個例子,建立一個實體,包含用戶名、暱稱、年齡翻譯
1 /// <summary> 2 /// 用戶信息對象 3 /// </summary> 4 public class UserInfo 5 { 6 public string UserName { get; init; } 7 public string UserNickName { get; init; } 8 public int UserAge { get; set; } 9 }
由於UserInfo是個類對象,是引用類型,因此咱們進行以下輸出:設計
1 UserInfo u = new UserInfo() 2 { 3 UserName = "翁智華", 4 UserNickName = "Brand", 5 UserAge = 10 6 }; 7 var uclone = u; 8 uclone.UserAge = 11; 9 Console.WriteLine(ReferenceEquals(u,uclone)); 10 Console.WriteLine("u:{0},uclone:{1}", JsonConvert.SerializeObject(u), JsonConvert.SerializeObject(uclone));
輸出結果以下,能夠看出,這兩個對象是相等的,當ucolone的值發生改變的時候,他所引用的對象也發送了變化:code
這個是咱們所熟悉的知識,那麼怎樣理解 Record 實際是一個引用類型 ,但具備值類型的行爲的特徵。這時候就要認識一下它的 with 表達式。對象
當咱們使用引用類型時,最經常使用的一種方式是咱們想用基於當前的對象,去修改他的值以便產生一個新的對象,這時候就不能直接賦值等於,不然會出現上述的改變引用對象狀況。blog
若是我想修改年齡,就須要拷貝一份用戶信息表,而且基於這份拷貝的新對象來修改值。這樣的作法有個專業的名詞叫作 non-destructive mutation,即 非破壞性突變。繼承
而記錄類型(record)不是表明 對象在一段時間內的 狀態,而是表明對象在給定時間點的狀態,而且使用with表達式來實現給定時間點的狀態的產生。
舉個例子,記住下面record的使用:
1 /// <summary> 2 /// 用戶信息對象 3 /// </summary> 4 public record UserInfoRecord 5 { 6 public string UserName { get; init; } 7 public string UserNickName { get; init; } 8 public int UserAge { get; init; } 9 }
在使用with表達式的時間點,ucolone 就是 u 對象所在這個時間點的產生的新狀態,這時候 u 對象和 uclone 對象並不相等,值也不一致。
1 var u = new UserInfoRecord() 2 { 3 UserName = "翁智華", 4 UserNickName = "Brand", 5 UserAge = 10 6 }; 7 var uclone = u with { UserAge=11 };
以上就是兩個不一樣時間點對象狀態的比較,跟咱們上面理解的一致,若是實際場景須要,能夠產生多個對應時間點的對象狀態。
因此record和with本質是使用現有對象,並將對象內的字段逐一的複製到新的對象的過程。來看看微軟官方的說明:
記錄(record
)隱式定義了一個受保護的(protected
)「複製構造函數」——一個接受現有記錄對象並逐字段將其複製到新記錄對象的構造函數:
protected Person(Person original) { /* copy all the fields */ } // generated
with
表達式會調用「複製構造函數」,而後在上面應用對象初始化器來相應地變動屬性。
若是您不喜歡生成的「複製構造函數」的默認行爲,您能夠定義本身的「複製構造函數」,它將被 with
表達式捕獲。
咱們知道,C#的對象可使用Object.Equals(object, object)來比較兩個非空參數,判斷是否相等。結構重寫了這個方法,經過遞歸調用每一個結構字段的Equals方法,因此有 「基於值的相等」。
recrods也是這樣,因此着只要他們的值保持一致,兩個record對象能夠不是同一個對象也會相等(這種相等是基於值的相等,並非指他們是一個對象)。
基於上面定義的record,咱們作以下修改:
1 UserInfoRecord u1 = new UserInfoRecord() 2 { 3 UserName = "翁智華", 4 UserNickName = "Brand", 5 UserAge = 10 6 }; 7 8 UserInfoRecord u2 = new UserInfoRecord() 9 { 10 UserName = "翁智華", 11 UserNickName = "Brand", 12 UserAge = 10 13 }; 14 Console.WriteLine("ReferenceEquals:" + (ReferenceEquals(u1,u2)), Encoding.GetEncoding("GB2312")); 15 Console.WriteLine("Equals:" + (u1.Equals(u2)), Encoding.GetEncoding("GB2312"));
經過上面的結果,咱們能夠獲得 ReferenceEquals(person, originalPerson) = false (他們不是同一對象),可是 Equals(person, originalPerson) = true (他們有一樣的值)。
與基於值的Equals一塊兒的,還伴有基於值的GetHashCode()的重寫。同時,records實現了IEquatable<T>並重載了==和 !=這兩個操做符,以便於基於值的行爲在全部的不一樣的相等機制方面顯得一致。
繼承性:Inheritance
基礎類(class)不能從記錄(record)中繼承,不然會提示錯誤,只有記錄(record)能夠從其餘記錄(record)繼承,以下,咱們繼承上面的那個記錄:
1 /// <summary> 2 /// 繼承用戶信息record,並擴展Sex屬性 3 /// </summary> 4 public record UserInfoRecord2 : UserInfoRecord 5 { 6 public int Sex { get; init; } 7 }
對應地,with表達式和基於值的對等性,也相應的結合在一塊兒,下面是繼承後,對類型的判斷:
1 UserInfoRecord u1 = new UserInfoRecord2() 2 { 3 UserName = "翁智華", 4 UserNickName = "Brand", 5 UserAge = 10, 6 Sex = 1 7 }; 8 var u2 = u1 with { UserAge=18 }; 9 Console.WriteLine("IsUserInfoRecord2:" + (u2 is UserInfoRecord2));
兩個對象在運行時保證了一樣的類型的基礎上,就能夠用基於值的相等來進行比較了:
1 UserInfoRecord u3 = new UserInfoRecord2() 2 { 3 UserName = "翁智華", 4 UserNickName = "Brand", 5 UserAge = 18, 6 Sex = 1 7 }; 8 Console.WriteLine("u2 equal u3:" + (u2.Equals(u3)));
由於u2以前UserAge改爲18了,因此u2跟u3在這邊基於值相等,結果以下:
使用記錄(record
)能夠明確數據在整個實體中的位置,採用構造函數的參數的方式提供,而且能夠經過位置解構提取出數據。
原來咱們想要經過構造和解構進行賦值和獲取值須要這麼寫:
1 /// <summary> 2 /// 用戶信息對象 3 /// </summary> 4 public record UInfoRecord 5 { 6 public string UserName; 7 public string NickName; 8 public int Age; 9 public UInfoRecord(string userName, string nickName,int age) => (UserName, NickName,Age) = (userName,nickName,age); 10 public void Deconstruct(out string userName,out string nickName,out int age) => (userName, nickName, age) = (UserName, NickName, Age); 11 }
經過構造來提供內容和經過解構來獲取內容:
1 var uinfo = new UInfoRecord("翁智華", "Brand",18); // 構造 2 String name ="", nick = ""; 3 int age = 0; 4 uinfo.Deconstruct(out name,out nick,out age); // 解構 5 Console.WriteLine("解構獲取值,name:{0},nick:{1},age:{2}",name,nick,age);
如今能夠經過更加精簡的方式完成上面的工做,稱爲 參數名稱包裝模式(modulo casing of parameter names),
只要用包裝模式聲明記錄(記錄的位置是嚴格區分的),包含了三個自動屬性 ,就可使用構造函數和解構函數來提供內容和獲取內容了,上面的內容能夠改寫成以下:
1 /// <summary> 2 /// 用戶對象 3 /// </summary> 4 public record UInfoRecord(string UserName,string NickName,string Age);
1 var uinfo = new UInfoRecord("翁智華", "Brand",18); // 位置構造函數 / positional construction 2 var (name,nick,age) = uinfo; // 位置解構函數 / deconstruction 3 Console.WriteLine("解構獲取值,name:{0},nick:{1},age:{2}",name,nick,age);
得到的結果是同樣的。
若是你想修改默認提供的自動屬性,能夠自定義的同名屬性代替,產生的構造函數和解構函數將會只使用你自定義的那個。以下,從新定義了Age自動屬性,並默默的把值+1:
1 /// <summary> 2 /// 用戶對象 3 /// </summary> 4 public record UInfoRecord(string UserName, string NickName, int Age) 5 { 6 public int Age { get; init; } = Age+1; 7 }
我的感受record的出現使得對象的使用更加的便捷,一個是對象的複製和使用(with 表達式),不一樣時間點的數據狀態是不同的;一個是對象的比較(基於值的相等),避免咱們進行逐個比較。