C# 9.0 功能預覽 (2) record

前一篇我們提到了 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 是個甚麼玩意兒?

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 指示編譯器做了些甚麼?

 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,這也就是說當所有欄位內容相同的時候,就會視為兩個執行個體相等。

ToString 方法輸出長啥樣?

 以上述的例子來看,它輸出的樣子就是這樣:

Person { id = 10, Name = Bill, Age = 28 }
搭配 with expression

當 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