C#9.0:Records

概述

在C#9.0下,record是一个关键字,微软官方目前暂时将它翻译为记录类型。编程

传统面向对象的编程的核心思想是一个对象有着惟一标识,封装着随时可变的状态。C#也是一直这样设计和工做的。可是一些时候,你就很是须要恰好对立的方式。原来那种默认的方式每每会成为阻力,使得事情变得费时费力。若是你发现你须要整个对象都是不可变的,且行为像一个值,那么你应当考虑将其声明为一个record类型。函数

因此record类型的实际是一个引用类型 ,可是他具备值类型的行为。ui

先来回顾一下引用类型,C# 中有两种类型:引用类型和值类型。 引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据。 对于引用类型,两种变量可引用同一对象;所以,对一个变量执行的操做会影响另外一个变量所引用的对象。 对于值类型,每一个变量都具备其本身的数据副本,对一个变量执行的操做不会影响另外一个变量。spa

那咱们举个例子,建立一个实体,包含用户名、昵称、年龄翻译

1 /// <summary>
2 /// 用户信息对象
3 /// </summary>
4 public class UserInfo
5 {
6      public string UserName { get; init; }
7      public string UserNickName { get; init; }
8      public int UserAge { get; set; }
9

由于UserInfo是个类对象,是引用类型,因此咱们进行以下输出:设计

 1  UserInfo u = new UserInfo()
 2             {
 3                 UserName = "翁智华",
 4                 UserNickName = "Brand",
 5                 UserAge = 10
 6             };
 7             var uclone = u;
 8             uclone.UserAge = 11;
 9             Console.WriteLine(ReferenceEquals(u,uclone));
10             Console.WriteLine("u:{0},uclone:{1}", JsonConvert.SerializeObject(u), JsonConvert.SerializeObject(uclone));

输出结果以下,能够看出,这两个对象是相等的,当ucolone的值发生改变的时候,他所引用的对象也发送了变化:code

  这个是咱们所熟悉的知识,那么怎样理解 Record 实际是一个引用类型 ,但具备值类型的行为的特征。这时候就要认识一下它的 with 表达式。对象

with表达式

当咱们使用引用类型时,最经常使用的一种方式是咱们想用基于当前的对象,去修改他的值以便产生一个新的对象,这时候就不能直接赋值等于,不然会出现上述的改变引用对象状况。blog

若是我想修改年龄,就须要拷贝一份用户信息表,而且基于这份拷贝的新对象来修改值。这样的作法有个专业的名词叫作 non-destructive mutation,即 非破坏性突变。继承

而记录类型(record)不是表明 对象在一段时间内的 状态,而是表明对象在给定时间点的状态,而且使用with表达式来实现给定时间点的状态的产生。

举个例子,记住下面record的使用:

1  /// <summary>
2  /// 用户信息对象
3  /// </summary>
4  public record UserInfoRecord
5  {
6         public string UserName { get; init; }
7         public string UserNickName { get; init; }
8         public int UserAge { get; init; }
9

在使用with表达式的时间点,ucolone 就是 u 对象所在这个时间点的产生的新状态,这时候 u 对象和 uclone 对象并不相等,值也不一致。

1  var u = new UserInfoRecord()
2  {
3          UserName = "翁智华",
4          UserNickName = "Brand",
5          UserAge = 10
6   };
7  var uclone = u with { UserAge=11 }; 

以上就是两个不一样时间点对象状态的比较,跟咱们上面理解的一致,若是实际场景须要,能够产生多个对应时间点的对象状态。

因此record和with本质是使用现有对象,并将对象内的字段逐一的复制到新的对象的过程。来看看微软官方的说明:

记录(record)隐式定义了一个受保护的(protected)“复制构造函数”——一个接受现有记录对象并逐字段将其复制到新记录对象的构造函数:

protected Person(Person original) { /* copy all the fields */ } // generated

with 表达式会调用“复制构造函数”,而后在上面应用对象初始化器来相应地变动属性。

若是您不喜欢生成的“复制构造函数”的默认行为,您能够定义本身的“复制构造函数”,它将被 with 表达式捕获。 

基于值的相等

咱们知道,C#的对象可使用Object.Equals(object, object)来比较两个非空参数,判断是否相等。结构重写了这个方法,经过递归调用每一个结构字段的Equals方法,因此有 “基于值的相等”。

recrods也是这样,因此着只要他们的值保持一致,两个record对象能够不是同一个对象也会相等(这种相等是基于值的相等,并非指他们是一个对象)。

基于上面定义的record,咱们作以下修改: 

 1 UserInfoRecord u1 = new UserInfoRecord()
 2 {
 3       UserName = "翁智华",
 4       UserNickName = "Brand",
 5       UserAge = 10
 6 };
 7 
 8 UserInfoRecord u2 = new UserInfoRecord()
 9 {
10       UserName = "翁智华",
11       UserNickName = "Brand",
12       UserAge = 10
13 };
14 Console.WriteLine("ReferenceEquals:" + (ReferenceEquals(u1,u2)), Encoding.GetEncoding("GB2312"));
15 Console.WriteLine("Equals:" + (u1.Equals(u2)), Encoding.GetEncoding("GB2312")); 

经过上面的结果,咱们能够获得 ReferenceEquals(person, originalPerson) = false (他们不是同一对象),可是 Equals(person, originalPerson) = true (他们有一样的值)。

与基于值的Equals一块儿的,还伴有基于值的GetHashCode()的重写。同时,records实现了IEquatable<T>并重载了==和 !=这两个操做符,以便于基于值的行为在全部的不一样的相等机制方面显得一致。 

继承性:Inheritance 

基础类(class)不能从记录(record)中继承,不然会提示错误,只有记录(record)能够从其余记录(record)继承,以下,咱们继承上面的那个记录:

1   /// <summary>
2   ///  继承用户信息record,并扩展Sex属性
3   /// </summary>
4   public record UserInfoRecord2 : UserInfoRecord
5   {
6       public int Sex { get; init; }
7   }

 

对应地,with表达式和基于值的对等性,也相应的结合在一块儿,下面是继承后,对类型的判断:

1 UserInfoRecord u1 = new UserInfoRecord2()
2  {
3           UserName = "翁智华",
4           UserNickName = "Brand",
5           UserAge = 10,
6           Sex = 1
7  };
8 var u2 = u1 with { UserAge=18  };
9 Console.WriteLine("IsUserInfoRecord2:" + (u2 is UserInfoRecord2)); 

 两个对象在运行时保证了一样的类型的基础上,就能够用基于值的相等来进行比较了:

1  UserInfoRecord u3 = new UserInfoRecord2()
2  {
3          UserName = "翁智华",
4          UserNickName = "Brand",
5          UserAge = 18,
6          Sex = 1
7   };
8   Console.WriteLine("u2 equal u3:" + (u2.Equals(u3)));

 

由于u2以前UserAge改为18了,因此u2跟u3在这边基于值相等,结果以下:

位置记录:Positional Records

使用记录(record)能够明确数据在整个实体中的位置,采用构造函数的参数的方式提供,而且能够经过位置解构提取出数据。

原来咱们想要经过构造和解构进行赋值和获取值须要这么写:

 1 /// <summary>
 2 /// 用户信息对象
 3 /// </summary>
 4 public record UInfoRecord
 5 {
 6     public string UserName;
 7     public string NickName;
 8     public int Age;
 9     public UInfoRecord(string userName, string nickName,int age) => (UserName, NickName,Age) = (userName,nickName,age);
10     public void Deconstruct(out string userName,out string nickName,out int age) => (userName, nickName, age) = (UserName, NickName, Age);
11

经过构造来提供内容和经过解构来获取内容:

1 var uinfo = new UInfoRecord("翁智华", "Brand",18); // 构造
2 String name ="", nick = "";
3 int age = 0;
4 uinfo.Deconstruct(out name,out nick,out age); // 解构
5 Console.WriteLine("解构获取值,name:{0},nick:{1},age:{2}",name,nick,age); 

 如今能够经过更加精简的方式完成上面的工做,称为 参数名称包装模式(modulo casing of parameter names),

只要用包装模式声明记录(记录的位置是严格区分的),包含了三个自动属性 ,就可使用构造函数和解构函数来提供内容和获取内容了,上面的内容能够改写成以下:

1 /// <summary>
2 /// 用户对象
3 /// </summary>
4 public record UInfoRecord(string UserName,string NickName,string Age); 
1 var uinfo = new UInfoRecord("翁智华", "Brand",18); // 位置构造函数 / positional construction
2 var (name,nick,age) = uinfo;                        // 位置解构函数 / deconstruction
3 Console.WriteLine("解构获取值,name:{0},nick:{1},age:{2}",name,nick,age);

得到的结果是同样的。

若是你想修改默认提供的自动属性,能够自定义的同名属性代替,产生的构造函数和解构函数将会只使用你自定义的那个。以下,从新定义了Age自动属性,并默默的把值+1:

1 /// <summary>
2 /// 用户对象
3 /// </summary>
4 public record UInfoRecord(string UserName, string NickName, int Age)
5 {
6      public int Age { get; init; } = Age+1;
7 }

  

总结

我的感受record的出现使得对象的使用更加的便捷,一个是对象的复制和使用(with 表达式),不一样时间点的数据状态是不同的;一个是对象的比较(基于值的相等),避免咱们进行逐个比较。

相关文章
相关标签/搜索