zoukankan      html  css  js  c++  java
  • 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被意外更改。

       1:  public class Product
       2:  {
       3:      public Product(int id)
       4:      {
       5:          this.Id = id;
       6:      }
       7:   
       8:      public int Id { get; private set; }
       9:      //public int Id { get; }
      10:   
      11:      public string ProductName { get; set; }
      12:   
      13:      public string Description { get; set; }
      14:  }
      15:   
      16:  class Program
      17:  {
      18:      static void Main(string[] args)
      19:      {
      20:          Product product = new Product(1)
      21:          {
      22:              ProductName = "test001",
      23:              Description = "Just a description"
      24:          };
      25:   
      26:          Console.WriteLine($"Current Product Id: {product.Id},
    
    Product Name: {product.ProductName}, 
    
    Product Description: {product.Description}");
      27:          
      28:          //运行结果
      29:          //Current Product Id: 1,
      30:          //Product Name: test001,
      31:          //Product Description: Just a description
      32:          
      33:          Console.ReadKey();
      34:      }
      35:  }

    record方式设置只读

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

       1:  public int Id { get; init; }

    两者比较

    为了方便比较,我们可以将ProductName设置成了private set,然后通过ILSpy来查看一下编译后的代码,看看编译后的Id和ProductName有何不同

    a3b12fcd-fa21-4be9-9003-7e7ffef9ee34

    咋一看,貌似没啥区别,都使用到了initonly来修饰。但是如果仅仅只是替换声明方式,那么这个新特性似乎就没有什么意义了。
    接下来我们看第二张图:

    1dbcc742-7f95-4456-b3c8-fba0e4a549ed

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

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

    另外在赋值的时候,使用private set修饰的属性,需要定义构造函数,通过构造函数赋值。而使用了init修饰的属性,则不需要定义构造函数,直接在对象初始化器中赋值即可。

       1:  Product product = new Product
       2:  {
       3:      Id = 1,
       4:      ProductName = "test001",
       5:      Description = "Just a description"
       6:  };
       7:   
       8:  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类型,如下所示:

       1:  public record Product
       2:  {
       3:      public Product(int id, string productName, string description) => (Id, ProductName, Description) = (id, productName, description);
       4:   
       5:      public int Id { get; }
       6:   
       7:      public string ProductName { get; }
       8:   
       9:      public string Description { get; }
      10:  }

    然后查看一下IL,可以看到record会被编译成类,同时继承了System.Object,并实现了IEquatable泛型接口。
    编译器为我们提供的几个重要方法如下:

    • Equals
    • GetHashCode()
    • Clone
    • PrintMembers和ToString()

    比较重要的三个方法

    Equals:

    3

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

    GetHashCode():

    4

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

    Clone:

    5

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

    record在DDD值对象中的应用

    record之前的定义方式:

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

       1:  public abstract class ValueObject
       2:  {
       3:      public static bool operator ==(ValueObject left, ValueObject right)
       4:      {
       5:          if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
       6:          {
       7:              return false;
       8:          }
       9:          return ReferenceEquals(left, null) || left.Equals(right);
      10:      }
      11:   
      12:      public static bool operator !=(ValueObject left, ValueObject right)
      13:      {
      14:          return !(left == right);
      15:      }
      16:   
      17:      protected abstract IEnumerable<object> GetEqualityComponents();
      18:   
      19:   
      20:      public override bool Equals(object obj)
      21:      {
      22:          if (obj == null || obj.GetType() != GetType())
      23:          {
      24:              return false;
      25:          }
      26:   
      27:          var other = (ValueObject)obj;
      28:   
      29:          return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
      30:      }
      31:   
      32:      public override int GetHashCode()
      33:      {
      34:          return GetEqualityComponents()
      35:              .Select(x => x != null ? x.GetHashCode() : 0)
      36:              .Aggregate((x, y) => x ^ y);
      37:      }
      38:      // Other utility methods
      39:  }
      40:  public class Address : ValueObject
      41:  {
      42:      public string Street { get; private set; }
      43:      public string City { get; private set; }
      44:      public string State { get; private set; }
      45:      public string Country { get; private set; }
      46:      public string ZipCode { get; private set; }
      47:   
      48:      public Address(string street, string city, string state, string country, string zipcode)
      49:      {
      50:          Street = street;
      51:          City = city;
      52:          State = state;
      53:          Country = country;
      54:          ZipCode = zipcode;
      55:      }
      56:   
      57:      protected override IEnumerable<object> GetEqualityComponents()
      58:      {
      59:          // Using a yield return statement to return each element one at a time
      60:          yield return Street;
      61:          yield return City;
      62:          yield return State;
      63:          yield return Country;
      64:          yield return ZipCode;
      65:      }
      66:   
      67:      public override string ToString()
      68:      {
      69:          return $"Street: {Street}, City: {City}, State: {State}, Country: {Country}, ZipCode: {ZipCode}";
      70:      }
      71:  }

    main方法如下:

       1:  static void Main(string[] args)
       2:  {
       3:      Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
       4:      Console.WriteLine($"address1: {address1}");
       5:   
       6:      Address address2 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
       7:      Console.WriteLine($"address2: {address2}");
       8:   
       9:      Console.WriteLine($"address1 == address2: {address1 == address2}");
      10:   
      11:      string jsonAddress1 = address1.ToJson();
      12:      Address jsonAddress1Deserialize = jsonAddress1.FromJson<Address>();
      13:      Console.WriteLine($"jsonAddress1Deserialize == address1: {jsonAddress1Deserialize == address1}");
      14:   
      15:      Console.ReadKey();
      16:  }

    运行结果如下:

       1:  基于class:
       2:  address1: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
       3:  address2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
       4:  address1 == address2: True
       5:  jsonAddress1Deserialize == address1: True
    采用record方式定义:

    如果有大量的值对象需要我们编写,这无疑是加重我们的开发量的,这个时候record就派上用场了,最简洁的record风格的代码如下所示,只有一行:

       1:  public record Address(string Street, string City, string State, string Country, string ZipCode);

    IL代码如下图所示,从图中我们也可以看到record类型的对象,默认情况下用到了init来限制属性的只读特性。

    6

    main方法代码不变,运行结果也没有因为Address从class变成record而发生改变

       1:  基于record:
       2:  address1: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
       3:  address2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
       4:  address1 == address2: True
       5:  jsonAddress1Deserialize == address1: True

    如此看来我们的代码节省的不止一点点,而是太多太多了,是不是很爽啊。

    record对象属性值的更改

    使用方式如下:

       1:  class Program
       2:  {
       3:      static void Main(string[] args)
       4:      {
       5:          Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
       6:          Console.WriteLine($"1. address1: {address1}");
       7:   
       8:          Address addressWith = address1 with { Street = "############" };
       9:   
      10:          Console.ReadKey();
      11:      }
      12:  }
      13:   
      14:  public record Address(string Street, string City, string State, string Country, string ZipCode);

    通过ILSpy查看如下所示:

       1:  private static void Main(string[] args)
       2:  {
       3:      Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
       4:      Console.WriteLine($"1. address1: {address1}");
       5:      Address address2 = address1.<Clone>$();
       6:      address2.Street = "############";
       7:      Address addressWith = address2;
       8:      Console.ReadKey();
       9:  }

    由此可以看到record在更改的时候,实际上是通过调用Clone而产生了浅拷贝的对象,这也非常符合DDD ValueObject的设计理念。

    参考:

  • 相关阅读:
    [LeetCode] 909. Snakes and Ladders 蛇梯棋
    [LeetCode] 857. Minimum Cost to Hire K Workers 雇佣K名工人的最低成本
    [LeetCode] 908. Smallest Range I 最小区间
    [LeetCode] 862. Shortest Subarray with Sum at Least K 和至少为K的最短子数组
    [LeetCode] 907. Sum of Subarray Minimums 子数组最小值之和
    [LeetCode] 864. Shortest Path to Get All Keys 获得所有钥匙的最短路径
    [LeetCode] 906. Super Palindromes 超级回文数
    [LeetCode] 871. Minimum Number of Refueling Stops 最少的加油站个数
    [LeetCode] 905. Sort Array By Parity 按奇偶排序数组
    [LeetCode] 1028. Recover a Tree From Preorder Traversal 从先序遍历还原二叉树
  • 原文地址:https://www.cnblogs.com/edison0621/p/14135746.html
Copyright © 2011-2022 走看看