概述
在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 表達式),不同時間點的資料狀态是不一樣的;一個是對象的比較(基于值的相等),避免我們進行逐個比較。
架構與思維·公衆号:撰稿者為bat、位元組的幾位高階研發/架構。不做廣告、不賣課、不要打賞,隻分享優質技術
碼字不易,歡迎關注,歡迎轉載
作者:翁智華
出處:https://www.cnblogs.com/wzh2010/
本文采用「CC BY 4.0」知識共享協定進行許可,轉載請注明作者及出處。