前一篇我們提到了 init only setter,這一篇隆重登場的是 C# 9.0 的重量級人物 -- record。
Visual Studio 2019 - 16.8.0 Preview 3.1
.NET 5.0.0-rc.1.20451.14
record 實際上的身分還是 class -- 也就是個參考型別。作為一種不可變性物件類別設計的簡潔方案,但即便如此,record 並沒有限制屬性一定要使用 init,還是可以使用 set,也就是說這個新語法沒有強迫檢查不可變性 -- 至少在目前 16.8.0 Preview 3.1 的版本是如此,就目前的狀況來看,只要是懶得自己寫 Equals、GetHashCode 和 ToString 的時候都可以用,不一定非得為不可變性。
若是我們遵從不可變的設計,應該這麼寫:
record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
但是這樣寫也不會造成編譯警告或失敗:
record Person
{
public string Name { get; set; }
public int Age { get; set; }
}
record 在編譯之後的 IL Code 宣告明白說了它是個 class,同時會看到它自動實作了 IEquatable<T> 介面:
.class private auto ansi beforefieldinit RecordSample001.Person
extends [System.Runtime]System.Object
implements class [System.Runtime]System.IEquatable`1<class RecordSample001.Person>
{
}
record 是一個糖分非常高的語法糖,來瞧瞧它倒底自動生成哪些程式碼:
(1) implement IEquatable<T> interface,以上述的 Person record 會增加一個方法 void Equals(Person other),這個內容基本上就是比較所有的欄位是否相等 -- 透過 EqualityComparer<T>.Default 。
(2) override void Equals(Object other) 方法,內容則為呼叫 this.Equals(other as Person)。
(3) overload == 和 != 運算子,讓這兩個運算子的行為和 Equals 方法一致。
(4) override int GetHashCode() 方法。
(5) 如果沒有手動寫建構式的話,編譯器會產生兩個建構式,一個是無參數建構式 (這是原本編譯器處理類別的行為,和 record 無關);另外一個是具有自身型別為參數的建構式,若為編譯器產出的這個建構式存取修飾會被宣告為 protected。
(6) 產生一個 public virtual Person <Clone>$() 方法,這個方法的內部會呼叫 new Person(this) 回傳一個新的執行個體,這個方法不能直接在 C# 使用程式碼直接呼叫,必須透過新的 with expression 來使用。
(7) 產生一個唯讀的屬性 protected virtual Type EqualityContract,內容很簡單,就是 return typeof(Person); 。
(8) 加入一個 protected virtual bool PrintMembers(StringBuilder builder) 方法,這個方法的內容是串聯欄位與屬性值字串,基本上是為了給 ToString() 方法呼叫。
(9) override ToString() 方法。
大概就是搞了這些事情。
誠如前面所提,使用 record 所宣告的類別會自動產生關於相等比較的程式碼,假設我們有一個這樣的 record (請注意 id 是欄位):
record Person
{
public int id;
public string Name { get; init; }
public int Age { get; init; }
}
接著寫個簡單的 Console 程式來觀察結果:
static void Main(string[] args)
{
var p1 = new Person { id = 10, Age = 28, Name = "Bill" };
var p2 = new Person { id = 10, Age = 28, Name = "Bill" };
Console.WriteLine(object.ReferenceEquals(p1, p2));
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(object.Equals(p1, p2));
Console.WriteLine(p1 == p2);
Console.ReadLine();
}
object.ReferenceEquals(p1, p2) 得到的結果是 fasle,表示 p1 和 p2 變數所指向的執行個體是不同的;但其它結果皆為 true,這也就是說當所有欄位內容相同的時候,就會視為兩個執行個體相等。
以上述的例子來看,它輸出的樣子就是這樣:
Person { id = 10, Name = Bill, Age = 28 }
當 record 搭配 with expression 的時候,編譯器會產生程式碼呼叫前面提到的 <Clone>$ 方法 (雖然它被編譯為 protected ,但沒有甚麼擋得住編譯器的對吧?),並且根據 with expression 改變新產出執行個體的相對應欄位/屬性值,例如:
class Program
{
static void Main(string[] args)
{
var p1 = new Person { id = 10, Age = 28, Name = "Bill" };
var p2 = p1 with { id = 11, Name = "Tom" };
Console.WriteLine(p2);
Console.ReadLine();
}
}
p2 會指向一個新的執行個體,這個執行個體的 id 欄位會被改變為 11,Name 屬性則會變更成 "Tom";這種功能很接近 Prototype pattern 的作用。
今天先談到這兒,關於 record 還有一些內容下次聊。
參考資料:Record types