博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C# 9.0中引入的新特性init和record的使用思考
阅读量:4036 次
发布时间:2019-05-24

本文共 7141 字,大约阅读时间需要 23 分钟。

.NET 5.0已经发布,C# 9.0也为我们带来了许多新特性,其中最让我印象深刻的就是init和record type,很多文章已经把这两个新特性讨论的差不多了,本文不再详细讨论,而是通过使用角度来思考这两个特性。

init

init是C# 9.0中引入的新的访问器,它允许被修饰的属性在对象初始化的时候被赋值,其他场景作为只读属性的存在。直接使用的话,可能感受不到init的意义,所以我们先看看之前是如何设置属性为只读的。

private set设置属性为只读

设置只读属性有很多种方式,本文基于private set来讨论。

首先声明一个产品类,如下代码所示,我们把Id设置成了只读,这个时候也就只能通过构造函数来赋值了。在通常情况下,实体的唯一标识是不可更改的,同时也要防止Id被意外更改。

public class Product{    public Product(int id)    {        this.Id = id;    }    public int Id { get; private set; }    //public int Id { get; }    public string ProductName { get; set; }    public string Description { get; set; }}class Program{    static void Main(string[] args)    {        Product product = new Product(1)        {            ProductName = "test001",            Description = "Just a description"        };        Console.WriteLine($"Current Product Id: {product.Id},\n\rProduct Name: {product.ProductName}, \n\rProduct Description: {product.Description}");                //运行结果        //Current Product Id: 1,        //Product Name: test001,        //Product Description: Just a description                Console.ReadKey();    }}

record方式设置只读

使用init方式,是非常简单的,只需要把private set改成init就行了:

public int Id { get; init; }

两者比较

为了方便比较,我们可以将ProductName设置成了private set,然后通过ILSpy来查看一下编译后的代码,看看编译后的Id和ProductName有何不同咋一看,貌似没啥区别,都使用到了initonly来修饰。但是如果仅仅只是替换声明方式,那么这个新特性似乎就没有什么意义了。

接下来我们看第二张图:如图标记的那样,区别还是很明显的,通过init修饰的属性并没有完全替换掉set,由此看来微软在设计init的时候,还是挺用心思的,也为后面的赋值留下了入口。

instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Id (   int32 'value'  )

另外在赋值的时候,使用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类型,如下所示:

public record Product{    public Product(int id, string productName, string description) => (Id, ProductName, Description) = (id, productName, description);    public int Id { get; }    public string ProductName { get; }    public string Description { get; }}

然后查看一下IL,可以看到record会被编译成类,同时继承了System.Object,并实现了IEquatable泛型接口。

编译器为我们提供的几个重要方法如下:

  • Equals

  • GetHashCode()

  • Clone

  • PrintMembers和ToString()

比较重要的三个方法

Equals

通过图片中的代码,我们知道比较两个record对象,首先需要比较类型是否相同,然后再依次比较内部属性。

GetHashCode()

record类型通过基类型以及所有的属性及字段的方式来计算HashCode,这在整个继承层次结构中增强了基于值的相等性,也就意味着两个同名同姓的人不会被认为是同一个人

Clone

这个方法貌似非常简单,实在看不出有什么特别的地方,那么我们通过后面的内容再来解释这个方法。

record在DDD值对象中的应用

record之前的定义方式

了解DDD值对象的小伙伴应该想到了,record类型的特性非常像DDD中关于值对象的描述,比如不可变性、其相等于是基于其内部的属性的等等,我们先来看下值类型的定义方式。

public abstract class ValueObject{    public static bool operator ==(ValueObject left, ValueObject right)    {        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))        {            return false;        }        return ReferenceEquals(left, null) || left.Equals(right);    }    public static bool operator !=(ValueObject left, ValueObject right)    {        return !(left == right);    }    protected abstract IEnumerable GetEqualityComponents();    public override bool Equals(object obj)    {        if (obj == null || obj.GetType() != GetType())        {            return false;        }        var other = (ValueObject)obj;        return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());    }    public override int GetHashCode()    {        return GetEqualityComponents()            .Select(x => x != null ? x.GetHashCode() : 0)            .Aggregate((x, y) => x ^ y);    }    // Other utility methods}public class Address : ValueObject{    public string Street { get; private set; }    public string City { get; private set; }    public string State { get; private set; }    public string Country { get; private set; }    public string ZipCode { get; private set; }    public Address(string street, string city, string state, string country, string zipcode)    {        Street = street;        City = city;        State = state;        Country = country;        ZipCode = zipcode;    }    protected override IEnumerable GetEqualityComponents()    {        // Using a yield return statement to return each element one at a time        yield return Street;        yield return City;        yield return State;        yield return Country;        yield return ZipCode;    }    public override string ToString()    {        return $"Street: {Street}, City: {City}, State: {State}, Country: {Country}, ZipCode: {ZipCode}";    }}

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
();    Console.WriteLine($"jsonAddress1Deserialize == address1: {jsonAddress1Deserialize == address1}");    Console.ReadKey();}

运行结果如下:

基于class:address1: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fffaddress2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fffaddress1 == address2: TruejsonAddress1Deserialize == 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: fffaddress2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fffaddress1 == address2: TruejsonAddress1Deserialize == 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.
$(); 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/

转载地址:http://ngkdi.baihongyu.com/

你可能感兴趣的文章
/etc/resolv.conf
查看>>
container_of()传入结构体中的成员,返回该结构体的首地址
查看>>
linux sfdisk partition
查看>>
ipconfig,ifconfig,iwconfig
查看>>
opensuse12.2 PL2303 minicom
查看>>
电平触发方式和边沿触发的区别
查看>>
网络视频服务器移植
查看>>
Encoding Schemes
查看>>
移植QT
查看>>
如此调用
查看>>
计算机的发展史
查看>>
带WiringPi库的交叉编译如何处理一
查看>>
带WiringPi库的交叉笔译如何处理二之软链接概念
查看>>
Spring事务的七种传播行为
查看>>
ES写入找不到主节点问题排查
查看>>
Java8 HashMap集合解析
查看>>
ArrayList集合解析
查看>>
欢迎使用CSDN-markdown编辑器
查看>>
Android计算器实现源码分析
查看>>
Android系统构架
查看>>