C# 9.0:Records

2023-10-26

转自:翁智华

cnblogs.com/wzh2010/p/13950647.html

概述

在C# 9.0下record是一个关键字,微软官方目前暂时将它翻译为记录类型。传统面向对象的编程的核心思想是一个对象有着唯一标识,封装着随时可变的状态。

C#也是一直这样设计和工作的。但是一些时候,你就非常需要刚好对立的方式。原来那种默认的方式往往会成为阻力,使得事情变得费时费力。如果你发现你需要整个对象都是不可变的,且行为像一个值,那么你应当考虑将其声明为一个record类型。

所以record类型的实际是一个引用类型 ,但是他具有值类型的行为。

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

那我们举个例子,创建一个实体,包含用户名、昵称、年龄

/// <summary>
/// 用户信息对象
/// </summary>
public class UserInfo
{
     public string UserName { get; init; }
     public string UserNickName { get; init; }
     public int UserAge { get; set; }
}

因为UserInfo是个类对象,是引用类型,所以我们进行如下输出:

UserInfo u = new UserInfo()
      {
          UserName = "翁智华",
          UserNickName = "Brand",
          UserAge = 10
      };
      var uclone = u;
      uclone.UserAge = 11;
      Console.WriteLine(ReferenceEquals(u,uclone));
      Console.WriteLine("u:{0},uclone:{1}", JsonConvert.SerializeObject(u), JsonConvert.SerializeObject(uclone));

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

211460fb306fd3ed309e1a08d5ba1c41.png

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

with表达式

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

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

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

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

/// <summary>
/// 用户信息对象
/// </summary>
public record UserInfoRecord
{
   public string UserName { get; init; }
   public string UserNickName { get; init; }
   public int UserAge { get; init; }
}

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

var u = new UserInfoRecord()
{
    UserName = "翁智华",
    UserNickName = "Brand",
    UserAge = 10
};
var uclone = u with { UserAge=11 };

474cc6aca9d774da019f5c3df71c7a6c.png

以上就是两个不同时间点对象状态的比较,跟我们上面理解的一致,如果实际场景需要,可以产生多个对应时间点的对象状态。

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

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

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

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

如果您不喜欢生成的“复制构造函数”的默认行为,您可以定义自己的“复制构造函数”,它将被 with 表达式捕获。 

基于值的相等

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

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

基于上面定义的record,我们做如下修改: 

UserInfoRecord u1 = new UserInfoRecord()
{
  UserName = "翁智华",
  UserNickName = "Brand",
  UserAge = 10
};
UserInfoRecord u2 = new UserInfoRecord()
{
  UserName = "翁智华",
  UserNickName = "Brand",
  UserAge = 10
};
Console.WriteLine("ReferenceEquals:" + (ReferenceEquals(u1,u2)), Encoding.GetEncoding("GB2312"));
Console.WriteLine("Equals:" + (u1.Equals(u2)), Encoding.GetEncoding("GB2312"));

4cc93a70e43c21d08595f86aa72e4ba4.png

通过上面的结果,我们可以得到 ReferenceEquals(person, originalPerson) = false (他们不是同一对象),但是 Equals(person, originalPerson) = true (他们有同样的值)。

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

继承性:Inheritance

基础类(class)不能从记录(record)中继承,否则会提示错误,只有记录(record)可以从其他记录(record)继承,如下,我们继承上面的那个记录:

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

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

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

abec75fcdaa2cc4e4827dc80f50468b9.png

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

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

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

961bc5baf3f37fda8b21bbb07d1edb91.png

位置记录:Positional Records

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

原来我们想要通过构造和解构进行赋值和获取值需要这么写:

/// <summary>
/// 用户信息对象
/// </summary>
public record UInfoRecord
{
    public string UserName;
    public string NickName;
    public int Age;
    public UInfoRecord(string userName, string nickName,int age) => (UserName, NickName,Age) = (userName,nickName,age);
    public void Deconstruct(out string userName,out string nickName,out int age) => (userName, nickName, age) = (UserName, NickName, Age);
}

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

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

f57f5f9f3c501b8514ef235c2f819849.png

现在可以通过更加精简的方式完成上面的工作,称为 参数名称包装模式(modulo casing of parameter names),

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

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

获得的结果是一样的。

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

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

e7a31b44699094e74c88491331b2abef.png

总结

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

- EOF -

技术群:添加小编微信dotnet999

公众号:dotnet讲堂

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C# 9.0:Records 的相关文章

随机推荐

  • 免费的 PPT 模版资源

    1 第一 PPT 第一PPT站内资源以免费下载为基础 本着开放的共享为原则 服务于国内广大国内PPT爱好者 目前第一PPT站内的所有PowerPoint资源 PPT模板 PPT背景 PPT 素材 PPT教程 PPT软件 均是免费下载 所以请
  • openVPN服务端搭建

    搭建步骤 云服务器 Ubuntu 20 04 1 LTS 搭建服务端 公网IP 47 215 测试客户端 部门内部成员的windows10 或者windows11 及mac电脑 还有现场linux环境 最后目标是实现所有客户端之间能够互联
  • Electron桌面程序开发入门

    1 Electron结合vue项目配置 Electron是利用web前端技术进行桌面应用开发的一套框架 是由 github 开发的开源框架 允许开发者使用 Web 技术构建跨平台的桌面应用 它的基本结构 Electron Chromium
  • Vuluhub靶场-breach1

    网络设置和准备 该靶场的ip 192 168 110 140 我们要设置为仅主机模式 在虚拟机中将仅主机模式的ip地址范围包含靶机的ip 除了网络设置 还要准备两台kali 一台连接外网 一台和靶机一样要仅主机模式 信息收集 Nmap扫描
  • lvgl 自定义控制表格行高、颜色和外框样式

    lvgl 自定义控制表格行高 颜色和外框样式 lvgl版本 8 3 7 lvgl自带表格控件能够指定列宽 但是表格行高是根据内容动态渲染的 表格自带样式如图 带有蓝色的外框和白底 如果想要手动控制表格行高 颜色和外框等属性 需要监听表格绘制
  • 国产加密实际运用:使用SM3加盐存储密码,并且使用SM2进行登录认证

    目录 1 简要 2 开发环境及工具 3 后台密码加密部分 3 1加密代码 3 2 SM3加密类 Sm3crypto 3 3国密SM3工具类 Sm3Utils 3 4国密相关依赖包 4 登录认证部分 4 1前端部分关键代码 4 2后端logi
  • 查看tensorflow是否支持GPU,以及测试程序

    测试程序 Python import tensorflow as tf hello tf constant Hello TensorFlow sess tf Session print sess run hello 是否支持GPU impo
  • 【新手入门篇】React+ant design

    本篇着重讲解如何使用官方的demo 至于React及antd的安装及配置在本文末尾会给出相应的参考链接 创建一个React项目之后 create react app 你的项目名 在新建的项目目录下引入antd组件库 yarn add ant
  • Ubuntu 23.10 支持基于 TPM 的全磁盘加密

    将于下个月发布的 Ubuntu 23 10 增加了一项实验性功能 初步支持基于 TPM 的全磁盘加密 该功能利用系统的可信平台模块 TPM 缺点是这种额外的安全性依赖于 Snaps 包括内核和 GRUB 引导加载器 Ubuntu 开发商 C
  • 输出该单链表的中间结点的值,如果链表长度为偶数,则输出中间靠右的结点

    输出该单链表的中间结点的值 如果链表长度为偶数 则输出中间靠右的结点 题目要求 输入数据创建一个单链表 实现一种算法 输出该单链表的中间结点的值 如果链表长度为偶数 则输出 中间靠右 的结点 如果链表只有一个元素 则输出唯一的元素 算法思路
  • 【华为机试真题 JAVA】水果搬运问题-200

    题目描述 一组工人搬运一批水果 用一维数组存储工人编号和水果名称以及搬运重量 要求先按水果分组 然后按搬运重量排序输出 输入描述 第一行包括一个整数 N 1 N 100 代表工人的个数 接下来的 N 行每行包括两个整数 p 和 q 分别代表
  • 关于STM32的SPI使用DAM首发的回调问题

    本人第一次使用HAL库 然后用SPI操作FLAH 担心数据量大 于是打算使用DMA 之前是用的LL库 然后发现了一个问题 SPI怎么都接收不到数据 想了一下应该是片选引脚的问题 我应该在DMA传输结束时关闭引脚 但是之前都是用LL库 判断标
  • spring无侵入自动生成接口文档

    背景 spring cloud多个微服务开发了很多接口 紧急对接前端 需要快速提供一批接口的文档 且不同微服务的接口由多位同事开发且注释非常的少各有不同 现在需要不修改代码不添加注释的情况下能自动的扫描接口并生成文档 本文将详细介绍实现此需
  • X264的参考帧设置

    1 以r1884为例 r ref lt 整数 gt Reference Frame 即参考帧 决定最多可能的参考帧数 有效范围值1 16 该值越大 压缩率越高 但大于6后对压缩率的贡献很低 可以看压制完后x264 info ref 项 例如
  • sqlserver 登录名和用户名

    解释 登录名 通俗的讲 平时连接数据库是用的就是登录名 而不是用户名 是数据库服务级别 登录数据库之后 这个登录名有什么权限 比如可以访问那个数据库 或者表 存储过程 视图等 甚至字段权限 是有与之对应的用户 用户名 决定 注 也可以从服务
  • 手风琴(折叠面板)

    目录 一 Layui手风琴 1 1 引用layui的css和js 1 2 开启手风琴的代码示例 1 3 静态数据 1 4 最终效果图 二 Bootstrap手风琴 2 1 引用Bootstrap的css和js 2 2 开启手风琴的代码示例
  • Python 第一章 基础知识(6) 函数

    函数就像可以用来实现特定功能的小程序一样 Python的很多函数都能做很奇妙的事情 先来介绍一个内建函数 即是Python自带的已经定义好的函数 可以直接用 gt gt gt pow 2 3 8 这个函数实现了2 2 2的算法 这种使用函数
  • Angular 中 web worker的使用

    web worker就是在web应用程序中使用的worker 这个worker是独立于web主线程的 在后台运行的线程 web worker的优点就是可以将工作交给独立的其他线程去做 这样就不会阻塞主线程 第一步 ng g webWorke
  • 快速生成26个英文字母

    在学习中经常会拿26个英文字母序列做为字符串的例子来说明 但是自己又不想每次都自己手动输入 所以就想写个方法能快速的生成这个字符串 generate 26 english Characters return void public stat
  • C# 9.0:Records

    转自 翁智华 cnblogs com wzh2010 p 13950647 html 概述 在C 9 0下record是一个关键字 微软官方目前暂时将它翻译为记录类型 传统面向对象的编程的核心思想是一个对象有着唯一标识 封装着随时可变的状态