傳統面向對象編程的核心思想是一個對象有着惟一標識,表現爲對象引用,封裝着隨時可變的屬性狀態,若是你改變了一個屬性的狀態,這個對象仍是原來那個對象,就是對象引用沒有由於狀態的改變而改變,也就是說該對象能夠有不少種狀態。C#從最初開始也是一直這樣設計和工做的。可是一些時候,你可能很是須要一種剛好相反的方式,例如我須要一個對象只有一個狀態,那麼原來那種默認方式每每會成爲阻力,使得事情變得費時費力。web
當一個類型的對象在建立時被指定狀態後,就不會再變化的對象,咱們稱之爲不可變類型。這種類型是線程安全的,不須要進行線程同步,很是適合並行計算的數據共享。它減小了更新對象會引發各類bug的風險,更爲安全。System.DateTime和string就是不可變類型很是經典的表明。編程
原來,咱們要用類來建立一個不可變類型,你首先要定義只讀字段和屬性,而且還要重寫涉及相等判斷的方法等。在C#9.0中,引入了record,專門用來以最簡的方式建立不可變類型的新方式。若是你須要一個行爲像值類型的引用類型,你可使用record;若是你須要整個對象都是不可變的,且行爲像一個值,那麼你也可考慮將其聲明爲一個record類型。 那麼什麼是record類型?api
record類型是一種用record關鍵字聲明的新的引用類型,與類不一樣的是,它是基於值相等而不是惟一的標識符——對象引用。他有着引用類型的支持大對象、繼承、多態等特性,也有着結構的基於值相等的特性。能夠說有着class和struct二者的優點,在一些狀況下能夠用以替代class和struct。安全
提到不可變的類型,咱們會想到readonly struct,那麼爲何要選擇添加一個新的類型,而不是用readonly struct呢?這是由於記錄有着以下優勢:數據結構
在構造不可變的數據結構時,它的語法簡單易用。多線程
record爲引用類型,不用像值類型在傳遞時須要內存分配,並進行總體拷貝。併發
構造函數和結構函數爲一體的、簡化的位置記錄app
有力的相等性支持,重寫了Equals(object), IEquatable
record類型能夠定義爲可變的,也能夠是不可變的。如今,咱們用record定義一個只有只讀屬性的Person類型以下。這種只有只讀屬性的類型,由於其在建立好以後,屬性就不能再被修改,咱們一般把這種類型叫作不可變類型。函數
public record Person { public string LastName { get; } public string FirstName { get; } public Person(string first, string last) => (FirstName, LastName) = (first, last); }
上面這種聲明,在使用時,只能用帶參的構造函數進行初始化。要建立一個record對象跟類沒有什麼區別:
Person person = new("Andy", "Kang");
若是要支持用對象初始化器進行初始化,則在屬性中使用init關鍵字。這種形式,若是不須要用帶參的構造函數進行初始化,能夠不定義帶參的構造函數,上面的Person能夠改成下面形式。
public record Person { public string? FirstName { get; init; } public string? LastName { get; init; } }
如今,建立Person對象時,用初始化器進行初始化以下:
Person person = new() { FirstName = "Andy", LastName = "Kang"};
若是須要是可變類型的record,咱們定義以下。這種由於有set訪問器,全部它支持用對象初始化器進行初始化,若是你想用構造函數進行初始化,你能夠添加本身的構造函數。
public record Person { public string? FirstName { get; set; } public string? LastName { get; set; } }
爲了支持將record對象能解構成元組,咱們給record添加解構函數Deconstruct。這種record就稱爲位置記錄。下面代碼定義的Person,記錄的內容是經過構造函數的參數傳入,而且經過位置解構函數提取出來。你徹底能夠在記錄中定義你本身的構造和解構函數(注意不是析構函數)。以下所示:。
public record Person { public string FirstName { get; init; } public string LastName { get; init; } public Person(string firstName, string lastName) => (FirstName, LastName) = (firstName, lastName); public void Deconstruct(out string firstName, out string lastName) => (firstName, lastName) = (FirstName, LastName); }
針對上面如此複雜的代碼,C#9.0提供了更精簡的語法表達上面一樣的內容。須要注意的是,這種記錄類型是不可變的。這也就是爲何有record默認是不可變的說法由來。
public record Person(string FirstName, string LastName);
該方式聲明瞭公開的、僅可初始化的自動屬性、構造函數和解構函數。如今建立對象,你就能夠寫以下代碼:
var person = new Person("Mads", "Torgersen"); // 位置構造函數 var (firstName, lastName) = person; // 位置解構函數
固然,若是你不喜歡產生的自動屬性、構造函數和解構函數,你能夠自定義同名成員代替,產生的構造函數和解構函數將會只使用你自定義的那個。在這種狀況下,被自定義參數處於你用於初始化的做用域內,例如,你想讓FirstName是個保護屬性:
public record Person(string FirstName, string LastName) { protected string FirstName { get; init; } = FirstName; }
如上例子所示,對位置記錄進行擴展,你能夠在大括號裏添加你想要的任何成員。
一個位置記錄能夠像下面這樣調用父類構造函數。
public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);
record默認狀況下是被設計用來進行描述不可變類型的,所以位置記錄這種短小簡明的聲明方式是推薦方式。
當使用不可變的數據時,一個常見的模式是從現存的值建立新值來呈現一個新狀態。例如,若是Person打算改變他的姓氏(last name),咱們就須要經過拷貝原來數據,並賦予一個不一樣的last name值來呈現一個新Person。這種技術被稱爲非破壞性改變。做爲描繪隨時間變化的person,record呈現了一個特定時間的person的狀態。爲了幫助進行這種類型的編程,針對records就提出了with表達式,用於拷貝原有對象,並對特定屬性進行修改:
var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; var otherPerson = person with { LastName = "Torgersen" };
若是隻是進行拷貝,不須要修改屬性,那麼無須指定任何屬性修改,以下所示:
Person clone = person with { };
with表達式使用初始化語法來講明新對象在哪裏與原有對象不一樣。with表達式其實是拷貝原來對象的整個狀態值到新對象,而後根據對象初始化器來改變指定值。這意味着屬性必須有init或者set訪問器,才能用with表達式進行更改。
須要注意的是:
記錄(record)和類同樣,在面向對象方面,支持繼承,多態等全部特性。除過前面提到的record專有的特性,其餘語法寫法跟類也是同樣。同其餘類型同樣,record的基類依然是object。
要注意的是:
記錄只能從記錄繼承,不能從類繼承,也不能被任何類繼承。
record不能定義爲static的,可是能夠有static成員。
下面一個學生record,它繼承自Person:
public record Person { public string? FirstName { get; init; } public string? LastName { get; init; } } public sealed record Student : Person { public int ID { get; init; } }
對於位置記錄,只要保持record特有的寫法便可:
public record Person(string FirstName, string LastName); public sealed record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName); public sealed record Teacher(string FirstName, string LastName, string Title) : Person(FirstName, LastName) { public override string ToString() { StringBuilder s = new(); base.PrintMembers(s); return $"{s.ToString()} is a Teacher"; } }
with表達式和值相等性與記錄的繼承結合的很好,由於他們不只是靜態的已知類型,並且考慮到了整個運行時對象。好比,我建立一個Student對象,將其存在Person變量裏。
Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };
with表達式仍然拷貝整個對象並保持着運行時的類型:
var otherStudent = student with { LastName = "Torgersen" }; WriteLine(otherStudent is Student); // true
一樣地,值相等性確保兩個對象有着一樣的運行時類型,而後比較他們的全部狀態:
Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 }; WriteLine(student != similarStudent); //true, 因爲ID值不一樣
從本質上來說,record仍然是一個類,可是關鍵字record賦予這個類額外的幾個像值的行爲。也就是,當你定義了record時候,編譯器會自動生成如下方法,來實現基於值相等的特性(即只要兩個record的全部屬性都相等,且類型相同,那麼這兩個record就相等)、對象的拷貝和成員及其值的輸出。
基於值相等性的比較方法,如Equals,==,!=,EqualityContract等。
重寫GetHashCode()
拷貝和克隆成員
PrintMembers和ToString()方法
例如我先定義一個Person的記錄類型:
public record Person(string FirstName, string LastName);
編譯器生成的代碼和下面的代碼定義是等價的。可是要注意的是,跟編譯器實際生成的代碼相比,名字的命名是有所不一樣的。
public class Person : IEquatable<Person> { private readonly string _FirstName; private readonly string _LastName; protected virtual Type EqualityContract { get { return typeof(Person); } } public string FirstName { get { return _FirstName; } init { _FirstName = value; } } public string LastName { get { return _LastName; } init { _LastName = value; } } public Person(string FirstName, string LastName) { _FirstName = FirstName; _LastName = LastName; } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Person"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("FirstName"); builder.Append(" = "); builder.Append((object)FirstName); builder.Append(", "); builder.Append("LastName"); builder.Append(" = "); builder.Append((object)LastName); return true; } public static bool operator !=(Person r1, Person r2) { return !(r1 == r2); } public static bool operator ==(Person r1, Person r2) { return (object)r1 == r2 || ((object)r1 != null && r1.Equals(r2)); } public override int GetHashCode() { return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(_FirstName)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(_LastName); } public override bool Equals(object obj) { return Equals(obj as Person); } public virtual bool Equals(Person other) { return (object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(_FirstName, other._FirstName) && EqualityComparer<string>.Default.Equals(_LastName, other._LastName); } public virtual Person Clone() { return new Person(this); } protected Person(Person original) { _FirstName = original._FirstName; _LastName = original._LastName; } public void Deconstruct(out string FirstName, out string LastName) { FirstName = this.FirstName; LastName = this.LastName; } }
這些由編譯器生成的一些成員,是容許編程人員自定義的,一旦編譯器發現有自定義的某個成員,它就不會再生成這個成員。
因而可知,record實際上就是編譯器特性,而且records由他們的內容來界定,不是他們的引用標識符。從這一點上講,records更接近於結構,可是他們依然是引用類型。
全部對象都從object類型繼承了 Equals(object),這是靜態方法 Object.Equals(object, object) 用來比較兩個非空參數的基礎。結構重寫了這個方法,經過遞歸調用每一個結構字段的Equals方法,從而有了「基於值的相等」。Recrods也是這樣。這意味着只要他們的值保持一致,兩個record對象能夠不是同一個對象實例就會相等。例如咱們將修改的Last name又修改回去了:
var originalPerson = otherPerson with { LastName = "Nielsen" };
如今咱們會獲得 ReferenceEquals(person, originalPerson) = false (他們不是同一對象),可是 Equals(person, originalPerson) = true (他們有一樣的值).。與基於值的Equals一塊兒的,還伴有基於值的GetHashCode()的重寫。另外,records實現了IEquatable
基於值的相等和可變性契合的不老是那麼好。一個問題是改變值可能引發GetHashCode的結果隨時變化,若是這個對象被存放在哈希表中,就會出問題。咱們沒有不容許使用可變的record,可是咱們不鼓勵那樣作,除非你已經想到了後果。
若是你不喜歡默認Equals重寫的字段與字段比較行爲,你能夠進行重寫。你只須要認真理解基於值的相等時如何在records中工做原理,特別是涉及到繼承的時候。
除了熟悉的Equals,==和!=操做符以外,record還多了一個新的EqualityContract只讀屬性,該屬性返回類型是Type類型,返回值默認爲該record的類型。該屬性用來在判斷兩個具備繼承關係不一樣類型的record相等時,該record所依據的類型。下面咱們看一個有關EqualityContract的例子,定義一個學生record,他繼承自Person:
public record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName);
這個時候,咱們分別建立一個Person和Student實例,都用來描述一樣的人:
Person p = new Person("Jerry", "Kang"); Person s = new Student("Jerry", "Kang", 1); WriteLine(p == s); // False
這二者比較的結果是False,這與咱們實際需求不相符。那麼咱們能夠重寫EqualityContract來實現兩種相等:
public record Student(string FirstName, string LastName, int Level) : Person(FirstName, LastName) { protected override Type EqualityContract { get => typeof(Person); } }
通過此改造以後,上面例子中的兩個實例就會相等。EqualityContract的修飾符是依據下面狀況肯定的:
一個record在編譯的時候,會自動生成一個帶有保護訪問級別的「拷貝構造函數」,用來將現有record對象的字段值拷貝到新對象對應字段中:
protected Person(Person original) { /* 拷貝全部字段 */ } // 編譯器生成
with表達式就會引發拷貝構造函數被調用,而後應用對象初始化器來有限更改屬性相應值。若是你不喜歡默認的產生的拷貝構造函數,你能夠自定義該構造函數,編譯器一旦發現有自定義的構造函數,就不會在自動生成,with表達式也會進行調用。
public record Person(string FirstName, string LastName) { protected Person(Person original) { this.FirstName = original.FirstName; this.LastName = original.LastName; } }
編譯器默認地還會生成with表達式會使用的一個Clone方法用於建立新的record對象,這個方法是不能在record類型裏面自定義的。
2.4.3 PrintMembers和ToString()方法
若是你用Console.WriteLine來輸出record的實例,就會發現其輸出與用class定義的類型的默認的ToString徹底不一樣。其輸出爲各成員及其值組成的字符串:
Person {FirstName = Andy, LastName = Kang}
這是由於,基於值相等的類型,咱們更加關注於具體的值的狀況,所以在編譯record類型時會自動生成重寫了ToString的行爲的代碼。針對record類型,編譯器也會自動生成一個保護級別的PrintMembers方法,該方法用於生成各成員及其值的字符串,即上面結果中的紅色字體部分。ToString中,就調用了PrintMembers來生成其成員字符串部分,其餘部分即藍色字體部分在ToString中補充。
咱們也能夠定義PrintMembers和重寫ToString方法來實現本身想要的功能,以下面實現ToString輸出爲Json格式:
public record Person(string FirstName, string LastName) { protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("\"FirstName\""); builder.Append(" : "); builder.Append($"\"{ FirstName}\""); builder.Append(", "); builder.Append("\"LastName\""); builder.Append(" : "); builder.Append($"\"{ LastName}\""); return true; } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{"); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } }
record由於都是繼承自Object,所以ToString都是採用override修飾符。而PrintMembers方法修飾符是依據下面狀況決定的:
若是記錄不是sealed而是從object繼承的, 該方法是protected virtual;
若是記錄基類是另外一個record類型,則該方法是protected override;
若是記錄類型是sealed,則該方法也是private的。
用於web api返回的數據,一般做爲一種一次性的傳輸型數據,不須要是可變的,所以適合使用record。
做爲不可變數據類型record對於並行計算和多線程之間的數據共享很是適合,安全可靠。
record自己的不可變性和ToString的數據內容的輸出,不須要不少人工編寫不少代碼,就適合進行日誌處理。
其餘涉及到有大量基於值類型比較和複製的場景,也是record的經常使用的使用場景。
在生產應用中,有着衆多的使用場景,以便咱們用record來替換寫一個類。未知的還在等咱們進一步探索。