阅读本文大概需要 2 分钟。

大家好,这是 C# 9.0 新特性系列的第 4 篇文章。

熟悉函数式编程的童鞋一定对“只读”这个词不陌生。为了保证代码块自身的“纯洁”,函数式编程是不能随便“弄脏”外来事物(参数、变量等)的,所以“只读”对函数式编程非常重要。

为了丰富 C# 对函数式编程支持,较新的 C# 版本引入了一些很有用的新特性。比如 C# 8 中就对 struct 类型的方法增加了 readonly 修饰符支持,被 readonly 修饰的方法是不能修改该方法所在类的属性的。举个例子:

public struct FooValue
{
private int A { get; set; }
public readonly int IncreaseA()
{
A = A + 1; // 报错
return A;
}
}

而 C# 9 又进一步增加了对“只读”的支持,此次增加了 init-only 属性和 record 相关特性,下面一一介绍。

Init-only 属性

我们知道类的属性有 set 和 get 两种访问器,现在 C# 9 增加一种属性访问器:init。init 是 set 访问器的变体,它的作用是使属性只能在对象初始化的时候对其赋值,之后该属性就是只读的,因此叫 init-only 属性。使用方式如下:

public class Foo
{
public string PropA { get; init; }
public string PropB { get; init; }
}

赋值操作:

var foo = new Foo {  PropA = "A", PropB = "B" };
foo.PropA = "AA"; // 报错,PropA 此时是只读的!

由于 init 是在初始化阶段赋值,所以它可以在类内部修改 readonly 修饰的字段。比如:

public class Foo
{
private readonly string propA;
private readonly string propB; public string PropA
{
get => propA;
init => propA = (value ?? throw new ArgumentNullException(nameof(propA)));
}
public string PropA
{
get => propB;
init => propB = (value ?? throw new ArgumentNullException(nameof(propB)));
}
}

如果你知道在构造函数中可以对只读字段/属性赋值就自然也理解这一点。

记录 (Record)

做过财务系统的人都知道交易记录一旦入账是不能修改的,如果录入错误,就要新录入一笔负的记录把之前的红冲掉,再录入正确的记录。应对类似这种只读记录的场景,C# 9 引入了 Record(记录,下文均使用中文的“记录”)的概念,它用来支持整个对象的只读特性(即实例化后为只读)。使用方式如下:

public data class Foo
{
public string PropA { get; init; }
public string PropB { get; init; }
}

这里用了一个 data 关键字,表示该类的对象只是纯粹的记录值,它不是可修改的状态(在函数式编程中,所有的数据修改都是状态在发生变化)。

上面的太麻烦了,可以这样简写:

public data class Foo
{
string PropA;
string PropB;
}

默认属性都是 public 的,如果实在要改为 private,可以在属性定义前面加上 private 修饰符。

定位记录 (Positional Record)

有时候为了初始化更方便,可以定义构造函数来给属性赋值,初始化时只需要把属性值按顺序传给构造函数即可,这个操作称为定位构造(Positional Construction)。同样,也可以使用解构函数(Deconstructor)来实现属性的解构,即按照解构函数的参数顺序从对象中提取属性的值,被称为定位解构(Positional Deconstructor)。实现了定位构造或定位解构的记录称为定位记录(Positional Record)。下面是一个定位记录的实现:

public data class Foo
{
string PropA;
string PropB;
public Foo(string propA, string propB)
=> (PropA, PropB) = (propA, propB);
public void Deconstruct(out string propA, out string propB)
=> (propA, propB) = (PropA, PropB);
}

这个写法太麻烦了,可以直接简写为:

public data class Foo(string PropA, string PropB);

这样简短一句代码,其内部默认实现了 init-only 自动属性,且同时为所有属性定义了构造函数和解构函数。

使用示例:

var foo = new Foo("AA", "BB");  // 构造定位
var (a, b) = foo; // 解构定位

可以想象,记录的大部分使用场景,以上简写的写法能满足需求。若有特殊场景,就不能简单,需要进行自定义修改其默认行为。

with 表达式

当处理不可变数据时,若要生成不同的状态,一个常见的场景是在一条旧记录基础上拷贝一条新的记录。比如我们要修改 Foo 对象的 PropA 属性,我们就要拷贝该对象生成一个新的对象。这个操作在函数式编程中被称为“非破坏性修改 (non-destructive mutation)”。为了支持记录这个操作,C# 9 引入了 with 表达式,它可以很方便在一条原有记录基础上创建一条新记录。示例:

var other = foo with { PropA = "AA" };

with 表达式内部其实是通过一个默认的 protected 构造函数来实现的,大致如下:

protected Foo(Foo original)
{
// 拷贝 original 的所有字段
}

如果默认实现的字段拷贝不符合你的需求,你也可以手动实现这个构造函数。

今天就分享到这里,敬请期待一下篇关于 C# 9 新特性的文章!

C# 9.0 新特性之只读属性和记录的更多相关文章

  1. C#发展历程以及C#6.0新特性

    一.C#发展历程 下图是自己整理列出了C#每次重要更新的时间及增加的新特性,对于了解C#这些年的发展历程,对C#的认识更加全面,是有帮助的. 二.C#6.0新特性 1.字符串插值 (String In ...

  2. c# 6.0新特性(二)

    写在前面 上篇文章介绍了c#6.0的using static,Auto Property Initializers,Index Initializers新的特性,这篇文章将把剩下的几个学习一下. 原文 ...

  3. [C#]6.0新特性浅谈

    原文:[C#]6.0新特性浅谈 C#6.0出来也有很长一段时间了,虽然新的特性和语法趋于稳定,但是对于大多数程序猿来说,想在工作中用上C#6.0估计还得等上不短的一段时间.所以现在再来聊一聊新版本带来 ...

  4. ASP.NET Web API 2.0新特性:Attribute Routing1

    ASP.NET Web API 2.0新特性:Attribute Routing[上篇] 对于一个针对ASP.NET Web API的调用请求来说,请求的URL和对应的HTTP方法的组合最终决定了目标 ...

  5. C#6.0,C#7.0新特性

    C#6.0新特性 Auto-Property enhancements(自动属性增强) Read-only auto-properties (真正的只读属性) Auto-Property Initia ...

  6. C#7.0&6.0新特性 — 完整版

    C#2.0 泛型 部分类型 匿名方法 迭代器 可空类型 Getter / setter单独可访问性 方法组转换(代表) Co- and Contra-variance for delegates 静态 ...

  7. 浅谈Tuple之C#4.0新特性那些事儿你还记得多少?

    来源:微信公众号CodeL 今天给大家分享的内容基于前几天收到的一条留言信息,留言内容是这样的: 看了这位网友的留言相信有不少刚接触开发的童鞋们也会有同样的困惑,除了用新建类作为桥梁之外还有什么好的办 ...

  8. Java基础和JDK5.0新特性

    Java基础 JDK5.0新特性 PS: JDK:Java Development KitsJRE: Java Runtime EvironmentJRE = JVM + ClassLibary JV ...

  9. Visual Studio 2015速递(1)——C#6.0新特性怎么用

    系列文章 Visual Studio 2015速递(1)——C#6.0新特性怎么用 Visual Studio 2015速递(2)——提升效率和质量(VS2015核心竞争力) Visual Studi ...

随机推荐

  1. 【C++】常见易犯错误之数值类型取值溢出与截断(3)

    0.  前言 本节是“[C++]常见易犯错误之数值类型取值溢出与截断(1)” 的补充,主要探讨浮点型的取值溢出. 1. 相关知识 (1) 浮点型数据取值范围如下: 单精度型 float 3.4 * 1 ...

  2. [PHP自动化-进阶]004.Snoopy VS CURL 模拟Discuz.net登陆

    引言:采集论坛第一步就是要模拟登陆,由于各个站点登录表单各不相同,验证方式又是多种多样,所以直接提交用户名密码到登录页面就比较繁琐. 所以我们采用cookie来模拟登陆无疑是最佳捷径. 今天我们要处理 ...

  3. Java IO(十九)PrintStream 和 PrintWriter

    Java IO(十九)PrintStream 和 PrintWriter 一.介绍 (一).PrintStream PrintStream 是打印输出流,它继承于FilterOutputStream. ...

  4. Java中的集合(二)单列集合顶层接口------Collection接口

    Java中的集合(二)单列集合顶层接口------Collection接口 Collection是一个高度封装的集合接口,继承自Iterable接口,它提供了所有集合要实现的默认方法.由于Iterab ...

  5. jchdl - GSL实例:FullAdder

    https://mp.weixin.qq.com/s/CtT08xZON0YxnheqDM2FAw 全加器是能够计算低位进位的二进制加法电路.与半加器相比,全加器不只考虑本位计算结果是否有进位,也考虑 ...

  6. jchdl - RTL实例 - MOS6502 ALU (Verilog)

    https://mp.weixin.qq.com/s/jLUz757FQZjMEYzYb2AIww   MOS6502是简单,但是曾经相当流行的一款CPU.网上有很多模拟程序可供学习使用.这里使用一个 ...

  7. 企业级Python开发大佬利用网络爬虫技术实现自动发送天气预告邮件

    前天小编带大家利用Python网络爬虫采集了天气网的实时信息,今天小编带大家更进一步,将采集到的天气信息直接发送到邮箱,带大家一起嗨~~拓展来说,这个功能放在企业级角度来看,只要我们拥有客户的邮箱,之 ...

  8. Java实现 LeetCode 581 最短无序连续子数组(从两遍搜索找两个指针)

    581. 最短无序连续子数组 给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序. 你找到的子数组应是最短的,请输出它的长度. 示例 1: 输入: ...

  9. Java实现 蓝桥杯VIP 算法训练 和为T

    问题描述 从一个大小为n的整数集中选取一些元素,使得它们的和等于给定的值T.每个元素限选一次,不能一个都不选. 输入格式 第一行一个正整数n,表示整数集内元素的个数. 第二行n个整数,用空格隔开. 第 ...

  10. Java中lang包的常用方法介绍

    JAVA API(图片来源:实验楼) 包装类 Integer包装类 方法 返回值 功能描述 byteValue() byte 以 byte 类型返回该 Integer 的值 intValue() in ...