.NET 5的新型別是什麼?答案是記錄類定義,它只在.NET 5及以上版本中有效。
我在程式集中建立的許多類都是我所謂的“模型型別”或POCO類,在大多數情況下,這些類主要用於傳輸資料進出我通常使用ASP編寫的後端API.NET使用Web API。您可以將這些類視為在實體框架中使用的程式碼優先類。它們仍然應該遵循良好的架構、編碼標準,但這些類主要只是用來表示資料。
例如,我在測試框架中使用的Person類。導航到它,你會看到它有這樣的自動屬性。
/// <summary> /// Gets or sets the postal code. /// </summary> /// <value>The postal code.</value> public string PostalCode { get; set; } This property really should look like this from the PersonProper class that includes appropriate documentation, attributes and more importantly validating data in the setter. /// <summary> /// Gets or sets the postal code. /// </summary> /// <value>The postal code.</value> /// <exception cref="ArgumentOutOfRangeException">PostalCode</exception> [DataMember(Name = "postalCode")] [XmlElement] public string PostalCode { get { return this._postalCode; } set { if (this._postalCode == value) { return; } this._postalCode = value.HasValue(0, 15) == false ? throw new ArgumentOutOfRangeException(nameof(this.PostalCode), Resources.PostalCodeLengthIsLimitedTo15Characters) : value; } }
如上所示,在正確的驗證之上,這些類還應該實現IComparable<T>, IEquatable<T>,並覆蓋ToString(), GetHashCode()和所有的運算子。我很少看到開發人員實現這麼多額外的工作。我原來的Person類從99行擴充套件到PersonProper的647行! Visual Studio在幫助開發人員正確建立這些資料類方面不是很有用。通常,這些模型類也需要是不可變的,或者像它一樣行事。其他一些需要花費時間去實現的東西。
當.NET 5在2020年釋出時,微軟的.NET團隊已經幫助改進了這一點,.NET提供了新的record類型別,這對我們需要編寫更多的“樣板”程式碼有很大幫助。首先,定義一個記錄型別就像定義一個類一樣,使用record而不是class。例如,我的PersonClass將這樣定義。
public record PersonRecord : IDataRecord<PersonRecord, string>
設定值
記錄型別的第一個大區別是如何設定值。我們不使用set,而是像這樣使用init。
public string PostalCode { get { return this._postalCode; } init { if (string.IsNullOrEmpty(value)) { throw new ArgumentNullException(nameof(this.PostalCode), "Value for postal code cannot be null or empty."); } this._postalCode = value.Length > 20 ? throw new ArgumentOutOfRangeException(nameof(this.PostalCode), "Postal code length is limited to 20 characters.") : value; } }
你可以看到init的工作原理和set一樣,除了規則,
Init值可以在建構函式中設定,就像只讀變數一樣。初始化值可以在物件初始化過程中設定。一旦建立了物件,資料就不能被修改,就像不可變型別一樣。如果我為PersonProper反編譯這個屬性,它看起來是這樣的。
.property instance string PostalCode { .get instance string dotNetTips.Spargine.Tester.Models.PersonProper::get_PostalCode() .set instance void dotNetTips.Spargine.Tester.Models.PersonProper::set_PostalCode(string) .custom instance void [System.Runtime.Serialization.Primitives]System.Runtime.Serialization.DataMemberAttribute::.ctor() = { Name=string('postalCode') } .custom instance void [System.Xml.ReaderWriter]System.Xml.Serialization.XmlElementAttribute::.ctor() }
但是記錄型別中的這個屬性是這樣的,
.property instance string PostalCode { .get instance string dotNetTips.Spargine.Tester.Models.AddressRecord::get_PostalCode() .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) dotNetTips.Spargine.Tester.Models.AddressRecord::set_PostalCode(string) }
記錄型別的init背後的“魔力”是modreq。Modreq是IL中使用者定義的修飾符,它有以下約定:如果編譯器識別出Modreq,它可以做任何有意義的事情。然而,如果編譯器在將要使用的成員上看到一個它不理解的modreq,它就必須失敗並報錯。對於記錄型別,這可以保護setter不被普通setter無意中呼叫。如果你使用C# 8或更低的版本,init setter總是會失敗。
更新值既然記錄類的值一旦建立就不能更新,那麼程式碼如何更新值,例如在客戶端,以便在後端更新資料?簡單地說,需要建立一個新物件,透過使用下面所示的with關鍵字,記錄型別使這變得非常容易,使用我的Tester程式集和NuGet包。
var person1 = RandomData.GeneratePersonCollection(count: 1, addressCount: 2).First(); // Update Postal code var person2 = person1 with { CellPhone = "(858) 123-1234"};
現在,person2是這樣的,
PersonRecord { BornOn = 2/18/1981 11:41:33 AM -08:00, CellPhone = (858) 123-1234, Email = [email protected], FirstName = uiSq`JsON^GeoWh, HomePhone = 666-283-3580, Id = 157beaefb49749bd8793f8f3b9931984, LastName = aFsNhYo_NVS^\yB\RAihpclmc }
等式無論何時建立類,尤其是模型類,都必須實現IComparable<T>、IEquatable<T>,並重寫如下等式運算子,
public static bool operator !=(PersonProper left, PersonProper right) => !( left == right );
這會產生大量額外的程式碼,特別是對於CompareTo()方法。它還建立了更多的工作來維護這些型別。因此,我很少看到開發人員實現這些方法。對於記錄型別,不必擔心這些方法中的任何一種。這些方法自動生成,如下所示。
[Nullable(1)] protected virtual Type EqualityContract { [NullableContext(1), CompilerGenerated] get { return Type.GetTypeFromHandle(PersonRecord); } } [NullableContext(2)] public static bool operator !=(PersonRecord r1, PersonRecord r2) { return r1 != r2; } [NullableContext(2)] public static bool operator ==(PersonRecord r1, PersonRecord r2) { if (r1 != r2) { if (r1 != null) { return r1.Equals(r2); } return false; } return true; } [NullableContext(2)] public override bool Equals(object obj) { return this.Equals(obj as PersonRecord); } [NullableContext(2)] public virtual bool Equals(PersonRecord other) { if (!other == null || !(this.EqualityContract == other.EqualityContract) || !EqualityComparer<List<IAddressRecord>>.Default.Equals(this._addresses, other._addresses) || !EqualityComparer<DateTimeOffset>.Default.Equals(this._bornOn, other._bornOn) || !EqualityComparer<string>.Default.Equals(this._cellPhone, other._cellPhone) || !EqualityComparer<string>.Default.Equals(this._email, other._email) || !EqualityComparer<string>.Default.Equals(this._firstName, other._firstName) || !EqualityComparer<string>.Default.Equals(this._homePhone, other._homePhone) || !EqualityComparer<string>.Default.Equals(this._id, other._id)) { return EqualityComparer<string>.Default.Equals(this._lastName, other._lastName); } return false; }
記錄型別的一個重要特性是,當使用這些相等運算子時,它檢查的是物件的實際資料值,而不是它們的例項。
雜湊物件也不需要重寫GetHashCode()方法,因為它們是自動生成的,如下所示。也不必擔心這種方法的維護問題!
public override int GetHashCode() { return (((((((EqualityComparer<Type>.Default.GetHashCode(this.EqualityContract) * -1521134295 + EqualityComparer<List<IAddressRecord>>.Default.GetHashCode(this._addresses)) * -1521134295 + EqualityComparer<DateTimeOffset>.Default.GetHashCode(this._bornOn)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this._cellPhone)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this._email)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this._firstName)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this._homePhone)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this._id)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this._lastName); }
過載ToString()types中ToString()的預設實現是返回型別的名稱,這不是很有用,因此由我們來覆蓋它以返回更好地表示物件中資料的內容。
記錄型別也包含了這個!現在為PersonRecord呼叫ToString()如下所示,
PersonRecord { Addresses = System.Collections.Generic.List`1[dotNetTips.Spargine.Tester.Models.AddressRecord], BornOn = 2/20/1974 1:06:36 PM -08:00, CellPhone = (858) 123-1234, Email = [email protected], FirstName = `OkRd_TQXfONhtH, HomePhone = 744-817-4861, Id = d6e1664bb11b421fb80fb8f1ef1804ab, LastName = gUbkABVdnrZ[crPCgTMfoGoe[ }
下面是自動生成的ToString()程式碼。
public override string ToString() { StringBuilder builder = new StringBuilder(); builder.Append("PersonRecord"); builder.Append(" { "); if (this.PrintMembers(builder)) { builder.Append(" "); } builder.Append("}"); return builder.ToString(); } [NullableContext(1)] protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("Addresses"); builder.Append(" = "); builder.Append(this.Addresses); builder.Append(", "); builder.Append("BornOn"); builder.Append(" = "); builder.Append(this.BornOn.ToString()); builder.Append(", "); builder.Append("CellPhone"); builder.Append(" = "); builder.Append(this.CellPhone); builder.Append(", "); builder.Append("Email"); builder.Append(" = "); builder.Append(this.Email); builder.Append(", "); builder.Append("FirstName"); builder.Append(" = "); builder.Append(this.FirstName); builder.Append(", "); builder.Append("HomePhone"); builder.Append(" = "); builder.Append(this.HomePhone); builder.Append(", "); builder.Append("Id"); builder.Append(" = "); builder.Append(this.Id); builder.Append(", "); builder.Append("LastName"); builder.Append(" = "); builder.Append(this.LastName); return true; }
正如您所看到的,生成了PrintMembers()方法來處理這個問題。我當然希望我們的專案已經是.NET 5了,我發現ToString()的唯一問題是它不適用於集合屬性。相反,它列印物件名。在這一點得到改進之前,我們可以使用上面列出的文章中討論的PropertiesToString()方法。這是PersonRecord方法的輸出。
效能由於程式碼效能對我們來說很重要,我們來看看使用新的record類型別是否更有效能。
克隆
由於記錄型別的工作方式,您無法進行常規克隆,只需建立一個新物件並使用with更改任何值。下面的基準測試顯示了建立一個新記錄物件與從我的測試NuGet包中克隆一個普通類之間的速度差異。
哇,PersonRecord快了7862ns!
計算SHA256雜湊
下面是建立這兩種型別的SHA256雜湊的結果。
正如你所看到的,PersonRecord快了2611ns!
將物件轉換為JSON
即使使用內建的.NET5 JSON序列化程式將記錄型別轉換為JSON,速度也更快!
小結結論是:使用記錄型別可以節省大量的編碼,並在以後節省所有的維護成本。例如,我正常地完全實現Person型別是681行程式碼,而我的Person記錄型別只有260行!
下面是我使用record的主要原因。
使得建立不可變類非常容易使用with關鍵字使更新型別變得容易繼承的工作方式與普通類一樣相等運算子、GetHashCode()和返回屬性及其值的ToString() 記錄型別效能更好