概述
在C#9.0下,record是一個關鍵字,微軟官方目前暫時將它翻譯為記錄型別。
傳統物件導向的程式設計的核心思想是一個物件有著唯一標識,封裝著隨時可變的狀態。C#也是一直這樣設計和工作的。但是一些時候,你就非常需要剛好對立的方式。原來那種預設的方式往往會成為阻力,使得事情變得費時費力。如果你發現你需要整個物件都是不可變的,且行為像一個值,那麼你應當考慮將其宣告為一個record型別。
所以record型別的實際是一個引用型別 ,但是他具有值型別的行為。
先來回顧一下引用型別,C# 中有兩種型別:引用型別和值型別。 引用型別的變數儲存對其資料(物件)的引用,而值型別的變數直接包含其資料。 對於引用型別,兩種變數可引用同一物件;因此,對一個變數執行的操作會影響另一個變數所引用的物件。 對於值型別,每個變數都具有其自己的資料副本,對一個變數執行的操作不會影響另一個變數。
那我們舉個例子,建立一個實體,包含使用者名稱、暱稱、年齡
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的值發生改變的時候,他所引用的物件也傳送了變化:
這個是我們所熟悉的知識,那麼怎樣理解 Record 實際是一個引用型別 ,但具有值型別的行為的特徵。這時候就要認識一下它的 with 表示式。
with表示式
當我們使用引用型別時,最常用的一種方式是我們想用基於當前的物件,去修改他的值以便產生一個新的物件,這時候就不能直接賦值等於,否則會出現上述的改變引用物件情況。
如果我想修改年齡,就需要拷貝一份使用者資訊表,並且基於這份拷貝的新物件來修改值。這樣的做法有個專業的名詞叫做 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在這邊基於值相等,結果如下:
位置記錄:Positional Records
使用記錄(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 表示式),不同時間點的資料狀態是不一樣的;一個是物件的比較(基於值的相等),避免我們進行逐個比較。