C# 9 新特性——record的相关总结

Intro

C# 9 中引入了 record,record 是一个特殊类,用它来实现 model 在有些情况下会非常的好用

Sample

record RecordPerson
{
 public string Name { get; init; }

 public int Age { get; init; }
}

record RecordPerson2(string Name, int Age);

public static void MainTest()
{
 var p1 = new RecordPerson()
 {
  Name = "Tom",
  Age = 12,
 };
 Console.WriteLine(p1);

 var p2 = p1 with { Age = 10 };
 Console.WriteLine(p2);

 var p3 = new RecordPerson() { Name = "Tom", Age = 12 };
 Console.WriteLine(p3);
 Console.WriteLine($"p1 Equals p3 =:{p1 == p3}");

 RecordPerson2 p4 = new("Tom", 12);
 Console.WriteLine(p4);
}

这里的示例,用 record 声明了两个 model,第二个 model 声明的时候使用了简化的写法,

record RecordPerson2(string Name, int Age); 这样的声明意味着,构造方法有两个参数,分别是 string Name int Age,并对应着两个属性,属性的声明方式和 RecordPerson 一样 public string Name { get; init; } 都是一个 get 一个 init

对于 record 支持一个 with 表达式,来修改某几个属性的值,这对于有很多属性都相同的场景来说是及其方便的,来看一下上面示例的输出结果

What inside

那么 record 内部发生了什么呢,我们来反编译看一下,我们看一下使用 DnSpy 反编译的结果

RecordPerson

private class RecordPerson : IEquatable<RecordSample.RecordPerson>
{
 // Token: 0x17000007 RID: 7
 // (get) Token: 0x06000027 RID: 39 RVA: 0x000025F4 File Offset: 0x000007F4
 [Nullable(1)]
 protected virtual Type EqualityContract
 {
  [NullableContext(1)]
  [CompilerGenerated]
  get
  {
   return typeof(RecordSample.RecordPerson);
  }
 }

 // Token: 0x17000008 RID: 8
 // (get) Token: 0x06000028 RID: 40 RVA: 0x00002600 File Offset: 0x00000800
 // (set) Token: 0x06000029 RID: 41 RVA: 0x00002608 File Offset: 0x00000808
 public string Name
 {
  [CompilerGenerated]
  get
  {
   return this.<Name>k__BackingField;
  }
  [CompilerGenerated]
  set
  {
   this.<Name>k__BackingField = value;
  }
 }

 // Token: 0x17000009 RID: 9
 // (get) Token: 0x0600002A RID: 42 RVA: 0x00002611 File Offset: 0x00000811
 // (set) Token: 0x0600002B RID: 43 RVA: 0x00002619 File Offset: 0x00000819
 public int Age
 {
  [CompilerGenerated]
  get
  {
   return this.<Age>k__BackingField;
  }
  [CompilerGenerated]
  set
  {
   this.<Age>k__BackingField = value;
  }
 }

 // Token: 0x0600002C RID: 44 RVA: 0x00002624 File Offset: 0x00000824
 public override string ToString()
 {
  StringBuilder stringBuilder = new StringBuilder();
  stringBuilder.Append("RecordPerson");
  stringBuilder.Append(" { ");
  if (this.PrintMembers(stringBuilder))
  {
   stringBuilder.Append(" ");
  }
  stringBuilder.Append("}");
  return stringBuilder.ToString();
 }

 // Token: 0x0600002D RID: 45 RVA: 0x00002678 File Offset: 0x00000878
 [NullableContext(1)]
 protected virtual bool PrintMembers(StringBuilder builder)
 {
  builder.Append("Name");
  builder.Append(" = ");
  builder.Append(this.Name);
  builder.Append(", ");
  builder.Append("Age");
  builder.Append(" = ");
  builder.Append(this.Age.ToString());
  return true;
 }

 // Token: 0x0600002E RID: 46 RVA: 0x000026EA File Offset: 0x000008EA
 [NullableContext(2)]
 public static bool operator !=(RecordSample.RecordPerson r1, RecordSample.RecordPerson r2)
 {
  return !(r1 == r2);
 }

 // Token: 0x0600002F RID: 47 RVA: 0x000026F6 File Offset: 0x000008F6
 [NullableContext(2)]
 public static bool operator ==(RecordSample.RecordPerson r1, RecordSample.RecordPerson r2)
 {
  return r1 == r2 || (r1 != null && r1.Equals(r2));
 }

 // Token: 0x06000030 RID: 48 RVA: 0x0000270C File Offset: 0x0000090C
 public override int GetHashCode()
 {
  return (EqualityComparer<Type>.Default.GetHashCode(this.EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.<Name>k__BackingField)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(this.<Age>k__BackingField);
 }

 // Token: 0x06000031 RID: 49 RVA: 0x0000274C File Offset: 0x0000094C
 [NullableContext(2)]
 public override bool Equals(object obj)
 {
  return this.Equals(obj as RecordSample.RecordPerson);
 }

 // Token: 0x06000032 RID: 50 RVA: 0x0000275C File Offset: 0x0000095C
 [NullableContext(2)]
 public virtual bool Equals(RecordSample.RecordPerson other)
 {
  return other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.<Name>k__BackingField, other.<Name>k__BackingField) && EqualityComparer<int>.Default.Equals(this.<Age>k__BackingField, other.<Age>k__BackingField);
 }

 // Token: 0x06000033 RID: 51 RVA: 0x000027B0 File Offset: 0x000009B0
 [NullableContext(1)]
 public virtual RecordSample.RecordPerson <Clone>$()
 {
  return new RecordSample.RecordPerson(this);
 }

 // Token: 0x06000034 RID: 52 RVA: 0x000027B8 File Offset: 0x000009B8
 protected RecordPerson([Nullable(1)] RecordSample.RecordPerson original)
 {
  this.Name = original.<Name>k__BackingField;
  this.Age = original.<Age>k__BackingField;
 }

 // Token: 0x06000035 RID: 53 RVA: 0x000027D9 File Offset: 0x000009D9
 public RecordPerson()
 {
 }

 // Token: 0x0400000C RID: 12
 [CompilerGenerated]
 [DebuggerBrowsable(DebuggerBrowsableState.Never)]
 private readonly string <Name>k__BackingField;

 // Token: 0x0400000D RID: 13
 [CompilerGenerated]
 [DebuggerBrowsable(DebuggerBrowsableState.Never)]
 private readonly int <Age>k__BackingField;
}

从上面的反编译结果可以看的出来,record 其实就是一个 class,只是编译器会帮我们做一些事情,编译器帮我们做了哪些事呢?

  • 实现了基于属性值的相等性比较,不再使用默认的引用,并且重写了 ==/!= operator 和 GetHashCode
  • 为了方便调试,重写了 ToString 方法,也提供了 PrintMembers 方法来实现比较方便只显示某些比较重要的参数
  • 实现了 EqualityContract 方法来指定类型比较的类型,默认是当前类型
  • 实现了 <Clone>$ 方法和一个特殊的构造方法,用来克隆一个对象,相当于 record 帮我们实现了一个浅复制 的 原型模式,还是强类型的,这个方法在代码里不能直接调用,当我们使用 with 表达式的时候,编译器会调用这个方法,并对某些属性进行赋值

再来看一下 RecordPerson2

private class RecordPerson2 : IEquatable<RecordSample.RecordPerson2>
{
 // Token: 0x06000036 RID: 54 RVA: 0x000027E2 File Offset: 0x000009E2
 public RecordPerson2(string Name, int Age)
 {
  this.Name = Name;
  this.Age = Age;
  base..ctor();
 }

 // Token: 0x1700000A RID: 10
 // (get) Token: 0x06000037 RID: 55 RVA: 0x000027F9 File Offset: 0x000009F9
 [Nullable(1)]
 protected virtual Type EqualityContract
 {
  [NullableContext(1)]
  [CompilerGenerated]
  get
  {
   return typeof(RecordSample.RecordPerson2);
  }
 }

 // Token: 0x1700000B RID: 11
 // (get) Token: 0x06000038 RID: 56 RVA: 0x00002805 File Offset: 0x00000A05
 // (set) Token: 0x06000039 RID: 57 RVA: 0x0000280D File Offset: 0x00000A0D
 public string Name
 {
  [CompilerGenerated]
  get
  {
   return this.<Name>k__BackingField;
  }
  [CompilerGenerated]
  set
  {
   this.<Name>k__BackingField = value;
  }
 }

 // Token: 0x1700000C RID: 12
 // (get) Token: 0x0600003A RID: 58 RVA: 0x00002816 File Offset: 0x00000A16
 // (set) Token: 0x0600003B RID: 59 RVA: 0x0000281E File Offset: 0x00000A1E
 public int Age
 {
  [CompilerGenerated]
  get
  {
   return this.<Age>k__BackingField;
  }
  [CompilerGenerated]
  set
  {
   this.<Age>k__BackingField = value;
  }
 }

 // Token: 0x0600003C RID: 60 RVA: 0x00002828 File Offset: 0x00000A28
 public override string ToString()
 {
  StringBuilder stringBuilder = new StringBuilder();
  stringBuilder.Append("RecordPerson2");
  stringBuilder.Append(" { ");
  if (this.PrintMembers(stringBuilder))
  {
   stringBuilder.Append(" ");
  }
  stringBuilder.Append("}");
  return stringBuilder.ToString();
 }

 // Token: 0x0600003D RID: 61 RVA: 0x0000287C File Offset: 0x00000A7C
 [NullableContext(1)]
 protected virtual bool PrintMembers(StringBuilder builder)
 {
  builder.Append("Name");
  builder.Append(" = ");
  builder.Append(this.Name);
  builder.Append(", ");
  builder.Append("Age");
  builder.Append(" = ");
  builder.Append(this.Age.ToString());
  return true;
 }

 // Token: 0x0600003E RID: 62 RVA: 0x000028EE File Offset: 0x00000AEE
 [NullableContext(2)]
 public static bool operator !=(RecordSample.RecordPerson2 r1, RecordSample.RecordPerson2 r2)
 {
  return !(r1 == r2);
 }

 // Token: 0x0600003F RID: 63 RVA: 0x000028FA File Offset: 0x00000AFA
 [NullableContext(2)]
 public static bool operator ==(RecordSample.RecordPerson2 r1, RecordSample.RecordPerson2 r2)
 {
  return r1 == r2 || (r1 != null && r1.Equals(r2));
 }

 // Token: 0x06000040 RID: 64 RVA: 0x00002910 File Offset: 0x00000B10
 public override int GetHashCode()
 {
  return (EqualityComparer<Type>.Default.GetHashCode(this.EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.<Name>k__BackingField)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(this.<Age>k__BackingField);
 }

 // Token: 0x06000041 RID: 65 RVA: 0x00002950 File Offset: 0x00000B50
 [NullableContext(2)]
 public override bool Equals(object obj)
 {
  return this.Equals(obj as RecordSample.RecordPerson2);
 }

 // Token: 0x06000042 RID: 66 RVA: 0x00002960 File Offset: 0x00000B60
 [NullableContext(2)]
 public virtual bool Equals(RecordSample.RecordPerson2 other)
 {
  return other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.<Name>k__BackingField, other.<Name>k__BackingField) && EqualityComparer<int>.Default.Equals(this.<Age>k__BackingField, other.<Age>k__BackingField);
 }

 // Token: 0x06000043 RID: 67 RVA: 0x000029B4 File Offset: 0x00000BB4
 [NullableContext(1)]
 public virtual RecordSample.RecordPerson2 <Clone>$()
 {
  return new RecordSample.RecordPerson2(this);
 }

 // Token: 0x06000044 RID: 68 RVA: 0x000029BC File Offset: 0x00000BBC
 protected RecordPerson2([Nullable(1)] RecordSample.RecordPerson2 original)
 {
  this.Name = original.<Name>k__BackingField;
  this.Age = original.<Age>k__BackingField;
 }

 // Token: 0x06000045 RID: 69 RVA: 0x000029DD File Offset: 0x00000BDD
 public void Deconstruct(out string Name, out int Age)
 {
  Name = this.Name;
  Age = this.Age;
 }

 // Token: 0x0400000E RID: 14
 [CompilerGenerated]
 [DebuggerBrowsable(DebuggerBrowsableState.Never)]
 private readonly string <Name>k__BackingField;

 // Token: 0x0400000F RID: 15
 [CompilerGenerated]
 [DebuggerBrowsable(DebuggerBrowsableState.Never)]
 private readonly int <Age>k__BackingField;
}

RecordPerson2 相比 RecordPerson 的区别在于构造器不同:

看上面反编译的结果,可以看出:

  • RecordPeron2 RecordPerson 都声明了两个属性,都是 public string Name { get; init; }/public int Age { get; init; }
  • RecordPerson 的构造方法是无参构造方法,而 RecordPerson2 的构造方法是 RecordPerson2(string Name, int Age)
  • 多出来一个 Deconstruct 方法,使得我们可以比较方便的解析一个对象的值,可以参考下面这个示例
foreach (var (name, age) in new[] { p4 })
{
 Console.WriteLine($"{name}={age}");
}

再来看一下测试方法的反编译结果:

RecordSample.RecordPerson p = new RecordSample.RecordPerson
{
 Name = "Tom",
 Age = 12
};
Console.WriteLine(p);
RecordSample.RecordPerson recordPerson = p.<Clone>$();
recordPerson.Age = 10;
RecordSample.RecordPerson p2 = recordPerson;
Console.WriteLine(p2);
RecordSample.RecordPerson p3 = new RecordSample.RecordPerson
{
 Name = "Tom",
 Age = 12
};
Console.WriteLine(p3);
Console.WriteLine(string.Format("p1 Equals p3 =:{0}", p == p3));
RecordSample.RecordPerson2 p4 = new RecordSample.RecordPerson2("Tom", 12);
Console.WriteLine(p4);

这里主要可以看到 with 表达式的实现,其实就是调用 <Clone>$ 方法复制了一个对象,并修改指定的属性值

More

record 实现了基于值的相等性比较,并且实现了 原型模式,可以比较方便的创建一个新的值完全相等的对象,这对于有一些业务场景来说是非常适合使用 record 来代替原来的实现的

Reference

https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9
https://github.com/WeihanLi/SamplesInPractice/tree/master/CSharp9Sample
https://github.com/WeihanLi/SamplesInPractice/blob/master/CSharp9Sample/RecordSample.cs

以上就是C# 9 新特性——record的相关总结的详细内容,更多关于c# 9 新特性的资料请关注我们其它相关文章!

(0)

相关推荐

  • C#9.0新特性详解——顶级程序语句(Top-Level Programs)

    1 背景与动机 通常,如果只想用C#在控制台上打印一行"Hello World!",这可不是Console.WriteLine("Hello World!");一条语句就可以搞定的,还涉及到其他必要基础代码(如定义类和入口函数Main),例如下面: using System; class Program { static void Main() { Console.WriteLine("Hello World!"); } } 就打印一句"

  • 浅析C# 9.0 新特性之 Lambda 弃元参数

    大家好,这是 C# 9.0 新特性短系列的第 5 篇文章. 弃元(Discards) 是在 C# 7.0 的时候开始支持的,它是一种人为丢弃不使用的临时虚拟变量.语法上它是用来赋值的,但它却不被分配存储空间,即没有值,所以不能从中读取值.弃元用 _(下划线) 表示,下划线是一个关键字,只能赋值,不能读取,例如: 在 C# 7.0 中,弃元的使用场景主要有下面四种: 元组和对象的解构 使用 is 和 switch 的模式匹配 对具有 out 参数的方法的调用 作用域内独立使用场景 针对这几个场景,

  • C#9新特性之增强的模式匹配

    Intro C# 9 中进一步增强了模式匹配的用法,使得模式匹配更为强大,我们一起来了解一下吧 Sample C# 9 中增强了模式匹配的用法,增加了 and / or / not 操作符,而且可以直接判断属性,来看一下下面的这个示例: var person = new Person(); // or // string.IsNullOrEmpty(person.Description) if (person.Description is null or { Length: 0 }) { Con

  • c# 9.0新特性——模块初始化器

    作者:MarkKang 出处:https://www.cnblogs.com/markkang/ 1 背景动机 关于模块或者程序集初始化工作一直是C#的一个痛点,微软内部外部都有大量的报告反应很多客户一直被这个问题困扰,这还不算没有统计上的客户.那么解决这个问题,还有基于什么样的考虑呢? 在库加载的时候,能以最小的开销.无需用户显式调用任何接口,使客户做一些期望的和一次性的初始化. 当前静态构造函数方法的一个最大的问题是运行时会对带有静态构造函数的类型做一些额外的检查.这是因为要决定静态构造函数

  • C# 9 中新加入的关键词 init,record,with

    一:背景 1. 讲故事 .NET5 终于在 2020-08-25 也就是大前天发布了第八个预览版,这么多的预览版搞得我都麻木了,接踵而来的就是更多的新特性加入到了 C# 9 中,既然还想呆在这条船上,得继续硬着头皮学习哈,这一篇跟大家聊聊新增的几个关键词. 二:新增关键词 1. init 出来一个新语法糖,首先要做的就是去揭它的老底,这样可以方便推测它的应用场景,为了方便表述,我先上一个例子: public class Person { public string Name { get; ini

  • c# 9.0新特性nint和Pattern matching的使用方法

    一:背景 1. 讲故事 上一篇跟大家聊到了Target-typed new 和 Lambda discard parameters,看博客园和公号里的阅读量都达到了新高,甚是欣慰,不管大家对新特性是多头还是空头,起码还是对它抱有一种极为关注的态度,所以我的这个系列还得跟,那就继续开撸吧,今天继续带来两个新特性,更多新特性列表,请大家关注:新特性预览 二:新特性研究 1. Native ints 从字面上看貌似是什么原生类型ints,有点莫名其妙,还是看一看Issues上举得例子吧: Summar

  • C#9新特性init only setter的使用

    C# 9 中新支持了 init 关键字,这是一个特殊的 setter,用来指定只能在对象初始化的时候进行赋值,另外支持构造器简化的写法,比如:Target-typed new expression 在已知类型的情况下可以使用 new() 来代表构造方法的简化用法,可以简化字段的声明,也可以简化一次声明多个相同类型的变量 Sample 来看一个示例,我们定义一个测试用的 Person 类,测试代码如下: public class Person { public int Age { get; ini

  • c# record的使用场景

    Intro 之前我们有介绍过 record 基本知识,record 会实现基于值的类型比较,最近遇到的几个问题觉得用 record 来解决会非常方便,分享一下 基于值的类型比较 最近有遇到一个场景,需要比较两个 JSON 字符串是否相等,字符串比较简单,就是一个固定值的 Dictionary,或者认为它就是一个简单的 Model,但是 JSON 字符串的的属性顺序可能不同,比如说下面的这个示例: {"Id":1, "Name":"Tom"}, {

  • C# 9 新特性之增强的foreach详解

    Intro 在 C# 9 中增强了 foreach 的使用,使得一切对象都有 foreach 的可能 我们来看一段代码,这里我们试图遍历一个 int 类型的值 思考一下,我们可以怎么做使得上面的代码编译通过呢? 迭代器模式 迭代器模式,提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示. 迭代器模式是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可以让外部代码透明地访问集合内部的数据. foreach 其实是一个迭代器模式的语法糖

  • 浅谈C# 9.0 新特性之只读属性和记录

    大家好,这是 C# 9.0 新特性系列的第 4 篇文章. 熟悉函数式编程的童鞋一定对"只读"这个词不陌生.为了保证代码块自身的"纯洁",函数式编程是不能随便"弄脏"外来事物(参数.变量等)的,所以"只读"对函数式编程非常重要. 为了丰富 C# 对函数式编程支持,较新的 C# 版本引入了一些很有用的新特性.比如 C# 8 中就对 struct 类型的方法增加了 readonly 修饰符支持,被 readonly 修饰的方法是不能

  • C# 9.0 新特性之模式匹配简化的实现

    记得在 MS Build 2020 大会上,C# 语言开发项目经理 Mads Torgersen 宣称 C# 9.0 将会随着 .NET 5 在今年 11 月份正式发布.目前 .NET 5 已经到了 Preview 5 阶段了,C# 9.0 也已经初具规模.忍不住激动的心情,暂停更新<C#.NET 拾遗补漏>系列几天,先要和大家分享一下我了解到的 C# 9.0 的新特性.由于新特性比较多,所以会分成几篇来讲.这是第一篇,专讲模式匹配这个特性的简化. 模式匹配(Pattern Matching)

  • C# 9.0新特性——扩展方法GetEnumerator支持foreach循环

    1.介绍 我们知道,我们要使一个类型支持foreach循环,就需要这个类型满足下面条件之一: 该类型实例如果实现了下列接口中的其中之一: System.Collections.IEnumerable System.Collections.Generic.IEnumerable<T> System.Collections.Generic.IAsyncEnumerable<T> 该类型中有公开的无参GetEnumerator()方法,且其返回值类型必须是类,结构或者接口,同时返回值类型

  • 使用C#9中records作为强类型ID的实例教程

    强类型ID 实体通常是整数,GUID或者string类型,因为数据库直接支持这些类型,但是,如果实体的ID的类型是一样的,比如都是整数的ID,这有可能会出现ID值传错的问题,看下边的示例. public void AddProductToOrder(int orderId, int productId, int count) { ... } ... // 这个地方,参数传错了 AddProductToOrder(productId, orderId, int count); 上面的代码可以很好地

随机推荐