C# 9.0中引入的新特性init和record的使用思考
.NET 5.0已經發布,C# 9.0也為我們帶來了許多新特性,其中最讓我印象深刻的就是init和record type,很多文章已經把這兩個新特性討論的差不多了,本文不再詳細討論,而是通過使用角度來思考這兩個特性。
init
init是C# 9.0中引入的新的訪問器,它允許被修飾的屬性在對象初始化的時候被賦值,其他場景作為只讀屬性的存在。直接使用的話,可能感受不到init的意義,所以我們先看看之前是如何設置屬性為只讀的。
private set設置屬性為只讀
設置只讀屬性有很多種方式,本文基于private set來討論。
首先聲明一個產品類,如下代碼所示,我們把Id設置成了只讀,這個時候也就只能通過構造函數來賦值了。在通常情況下,實體的唯一標識是不可更改的,同時也要防止Id被意外更改。
record方式設置只讀
使用init方式,是非常簡單的,只需要把private set改成init就行了:
public?int?Id?{?get;?init;?}兩者比較
為了方便比較,我們可以將ProductName設置成了private set,然后通過ILSpy來查看一下編譯后的代碼,看看編譯后的Id和ProductName有何不同咋一看,貌似沒啥區別,都使用到了initonly來修飾。但是如果僅僅只是替換聲明方式,那么這個新特性似乎就沒有什么意義了。
接下來我們看第二張圖:如圖標記的那樣,區別還是很明顯的,通過init修飾的屬性并沒有完全替換掉set,由此看來微軟在設計init的時候,還是挺用心思的,也為后面的賦值留下了入口。
另外在賦值的時候,使用private set修飾的屬性,需要定義構造函數,通過構造函數賦值。而使用了init修飾的屬性,則不需要定義構造函數,直接在對象初始化器中賦值即可。
Product?product?=?new?Product {Id?=?1,ProductName?=?"test001",Description?=?"Just?a?description" };product.Id?=?2;//Error?CS8852?Init-only?property?or?indexer?'Product.Id'?can?only?be?assigned?in?an?object?initializer,?or?on?'this'?or?'base'?in?an?instance?constructor?or?an?'init'?accessor.如上代碼所示,只讀屬性Id的賦值并沒有在構造函數中賦值,畢竟當一個類的只讀字段十分多的時候,構造函數也變得復雜。而且在賦值好之后,無法修改,這和我們對只讀屬性在通常情況下的理解是一致的。另外通過init修飾的好處便是省卻了一部分只讀屬性在操作上的復雜性,使得對象的聲明與賦值更加直觀。
在合適的場景下選擇最好的編程方式,是程序員的一貫追求,千萬不要為了炫技而把init當成了茴字的第N種寫法到處去問。
record
record是一個非常有用的特性,它是不可變類型,其相等性是通過內部的幾個屬性來確定的,同時它支持我們以更加方便的方式、像定義值類型那樣來定義不可變引用類型。
我們把之前的Product類改成record類型,如下所示:
然后查看一下IL,可以看到record會被編譯成類,同時繼承了System.Object,并實現了IEquatable泛型接口。
編譯器為我們提供的幾個重要方法如下:
Equals
GetHashCode()
Clone
PrintMembers和ToString()
比較重要的三個方法
Equals
通過圖片中的代碼,我們知道比較兩個record對象,首先需要比較類型是否相同,然后再依次比較內部屬性。
GetHashCode()
record類型通過基類型以及所有的屬性及字段的方式來計算HashCode,這在整個繼承層次結構中增強了基于值的相等性,也就意味著兩個同名同姓的人不會被認為是同一個人
Clone
這個方法貌似非常簡單,實在看不出有什么特別的地方,那么我們通過后面的內容再來解釋這個方法。
record在DDD值對象中的應用
record之前的定義方式
了解DDD值對象的小伙伴應該想到了,record類型的特性非常像DDD中關于值對象的描述,比如不可變性、其相等于是基于其內部的屬性的等等,我們先來看下值類型的定義方式。
main方法如下:
static?void?Main(string[]?args) {Address?address1?=?new?Address("aaa",?"bbb",?"ccc",?"ddd",?"fff");Console.WriteLine($"address1:?{address1}");Address?address2?=?new?Address("aaa",?"bbb",?"ccc",?"ddd",?"fff");Console.WriteLine($"address2:?{address2}");Console.WriteLine($"address1?==?address2:?{address1?==?address2}");string?jsonAddress1?=?address1.ToJson();Address?jsonAddress1Deserialize?=?jsonAddress1.FromJson<Address>();Console.WriteLine($"jsonAddress1Deserialize?==?address1:?{jsonAddress1Deserialize?==?address1}");Console.ReadKey(); }運行結果如下:
基于class: address1:?Street:?aaa,?City:?bbb,?State:?ccc,?Country:?ddd,?ZipCode:?fff address2:?Street:?aaa,?City:?bbb,?State:?ccc,?Country:?ddd,?ZipCode:?fff address1?==?address2:?True jsonAddress1Deserialize?==?address1:?True采用record方式定義
如果有大量的值對象需要我們編寫,這無疑是加重我們的開發量的,這個時候record就派上用場了,最簡潔的record風格的代碼如下所示,只有一行:
public?record?Address(string?Street,?string?City,?string?State,?string?Country,?string?ZipCode);IL代碼如下圖所示,從圖中我們也可以看到record類型的對象,默認情況下用到了init來限制屬性的只讀特性。main方法代碼不變,運行結果也沒有因為Address從class變成record而發生改變
基于record: address1:?Street:?aaa,?City:?bbb,?State:?ccc,?Country:?ddd,?ZipCode:?fff address2:?Street:?aaa,?City:?bbb,?State:?ccc,?Country:?ddd,?ZipCode:?fff address1?==?address2:?True jsonAddress1Deserialize?==?address1:?True如此看來我們的代碼節省的不止一點點,而是太多太多了,是不是很爽啊。
record對象屬性值的更改
使用方式如下:
class?Program {static?void?Main(string[]?args){Address?address1?=?new?Address("aaa",?"bbb",?"ccc",?"ddd",?"fff");Console.WriteLine($"1.?address1:?{address1}");Address?addressWith?=?address1?with?{?Street?=?"############"?};Console.ReadKey();} }public?record?Address(string?Street,?string?City,?string?State,?string?Country,?string?ZipCode);通過ILSpy查看如下所示:
private?static?void?Main(string[]?args) {Address?address1?=?new?Address("aaa",?"bbb",?"ccc",?"ddd",?"fff");Console.WriteLine($"1.?address1:?{address1}");Address?address2?=?address1.<Clone>$();address2.Street?=?"############";Address?addressWith?=?address2;Console.ReadKey(); }由此可以看到record在更改的時候,實際上是通過調用Clone而產生了淺拷貝的對象,這也非常符合DDD ValueObject的設計理念。
參考:
https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects
https://deviq.com/value-object/
總結
以上是生活随笔為你收集整理的C# 9.0中引入的新特性init和record的使用思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020.NET开发者大会大会线上同步直
- 下一篇: 使用 .NET Core 中的 Even