概述

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

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

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

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

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

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的值发生改变的时候,他所引用的对象也发送了变化:

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

with表达式

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

如果我想修改年龄,就需要拷贝一份用户信息表,并且基于这份拷贝的新对象来修改值。这样的做法有个专业的名词叫做 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 表达式),不同时间点的数据状态是不一样的;一个是对象的比较(基于值的相等),避免我们进行逐个比较。

C#9.0:Records的更多相关文章

  1. Android安全升级的7.0: Nougat

    Tamic http://www.jianshu.com/users/3bbb1ddf4fd5/latest_articles 今年夏天以来,Google做了多种增强的安全性在Android的7.0N ...

  2. 【C#】datetimepicker里面如何设置日期为当天日期,而时间设为0:00或23:59?

    今天无意中发现要根据日期查询时间,datatimepicker控件会把时间默认成当前时间(当你的控件只显示日期时),这样查询出来的出来的数据会有误差,用来下面的办法成功设置日期为当天日期,而时间设为0 ...

  3. Java-MyBatis-3.0:MyBatis 3 简介

    ylbtech-Java-MyBatis-3.0:MyBatis 3 简介 1.返回顶部 1. 简介 什么是 MyBatis ? MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程 ...

  4. 学习《人人都是产品经理2.0:写给泛产品经理》高清中文PDF+苏杰(作者)

    <人人都是产品经理2.0--写给泛产品经理>将从人开始,以人结束,中间说事,以一个产品从无到有的过程为框架--想清楚.做出来.推出去,外加一章综合案例.其中,最重要的想清楚.做出来.推出去 ...

  5. 一图了解 CODING 2.0:企业级持续交付解决方案

    近日,CODING 在 KubeCon 2019 上海站上正式推出了 DevOps 的一站式解决方案:CODING 2.0. CODING 2.0 进行了产品.产品理念.功能.首页的升级,对用户服务进 ...

  6. CODING 2.0:如何通过设计给品牌创造价值?

    升级背景 伴随着 CODING 理念的全面升级,CODING 正构建起覆盖构想到交付的全覆盖工具链,用户注册即可实践敏捷开发与 DevOps,提升软件交付质量与速度. 一直以来,CODING 作为软件 ...

  7. SQL Server温故系列(0):导航目录

    创建本系列博文通用库表及数据的 SQL 语句:下载 SQL Server温故系列(0):导航目录 SQL Server温故系列(1):SQL 数据操作 CRUD 之增删改合 SQL Server温故系 ...

  8. Markdown温故知新(0):导航目录

    Markdown温故知新(0):导航目录 Markdown温故知新(1):Markdown面面观 Markdown温故知新(2):详解七大标准语法 Markdown温故知新(3):六个实用扩展语法 M ...

  9. 信息安全-OAuth2.0:NuGetFromMicrosoft

    ylbtech-信息安全-OAuth2.0:NuGetFromMicrosoft 1.返回顶部 1. https://login.microsoftonline.com/common/oauth2/v ...

随机推荐

  1. js对比两个时间的大小

    /** * 时间对比 开始=结束返回0;开始>结束返回-1;开始<结束返回1 */ function dateComparison(date1,date2){ var start =new ...

  2. Deep Neural Networks for YouTube Recommendations YouTube的经典推荐框架

    https://zhuanlan.zhihu.com/p/52169807 王喆大佬的讲解

  3. 【NC基础操作】开发环境配置初体验

    当我们拿到开发工具UAP-STUDIO-6.5.0.2和Home文件的时候,意味着我们可以用这两样东西开始进行项目开发了(默认其他准备已就绪). 运行UAP-STUDIO-6.5.0.2 双击进入&q ...

  4. 搭建web攻防环境

    提示:本实验仅用于学习参考,不可用作其他用途! 任务一.基于centos7搭建dvwa web服务靶机 在centos7安装LAMP并启动,访问phpinfo页面 从互联网下载dvwa并解压到/var ...

  5. Java基础经典案例

    案例列表 01减肥计划switch版本 02减肥计划if版本 03逢七跳过 04不死神兔 05百钱白鸡 06数组元素求和 07判断两个数组是否相同 08查找元素在数组中的索引 09数组元素反转 10评 ...

  6. ES6 Set.Map.Symbol数据结构

    一.ES6 Set数据结构 ES6新推出了Set数据结构,它与数组很类似,Set内部的成员不允许重复,每一个值在Set中都是唯一的,如果有重复的值出现会自动去重(也可以理解为忽略掉),返回的是集合对象 ...

  7. Linux 下 swap 分区及作用详解

    我们在安装系统的时候已经建立了 swap 分区.swap 分区是 Linux 系统的交换分区,当内存不够用的时候,我们使用 swap 分区存放内存中暂时不用的数据.也就是说,当内存不够用时,我们使用 ...

  8. swoole中websoket创建在线聊天室(php)

    swoole中websoket创建在线聊天室(php) swoole现仅支持Linix,macos 创建websocket服务器 首先现在服务器创建一个websocket服务器 <?php // ...

  9. Centos 打开ssh 密码验证 和 root 登录

    # 1 打开系统的密码验证功能: vim /etc/ssh/sshd_config #允许使用密码登录(注释此行 就是允许证书登录) PasswordAuthentication yes # 2 打开 ...

  10. 立完flag,你可能需要对flag进行量化

    DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师. 官方网站:devui.design Ng组件库:ng-devui(欢 ...