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


    阅读本文大概需要 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 新特性的文章!

  • 相关阅读:
    Postman-插入断言
    浅谈如何提高自动化测试的稳定性和可维护性 (pytest&allure)
    pytest+allure+jenkins-持续集成平台生成allure报告
    pytest简介及入门
    allure描述用例详细讲解
    allure标记用例级别severity
    allure-pytest 功能用例与自动化用例完美对接
    allure-pytest 环境准备和初步运行
    selenium常用操作之上传操作
    selenium常用操作之JS处理日历控件
  • 原文地址:https://www.cnblogs.com/willick/p/13175191.html
Copyright © 2020-2023  润新知