C# 9.0新特性详解系列之一:只初始化设置器(init only setter)

1、背景与动机

自C#1.0版本以来,我们要定义一个不可变数据类型的基本做法就是:先声明字段为readonly,再声明只包含get访问器的属性。例子如下:

struct Point{    public int X { get; }    public int Y { get; }    public Point(int x, int y)    {        this.X = x;        this.Y = y;    }}

这种方式虽然很有效,但是它是以添加大量代码为代价的,并且类型越大,属性就越多,工作量就大,也就意味着更低的生产效率。

为了节省工作量,我们也用对象初始化方式来解决。对于创建对象来说,对象初始化方式是一种非常灵活和可读的方式,特别对一口气创建含有嵌套结构的树状对象来说更有用。下面是一个简单的对象初始化的例子:

var person = new Person{ FirstName = "Mads", LastName = "Torgersen" };

从这个例子,可以看出,要进行对象初始化,我们不得不先要在需要初始化的属性中添加set访问器,然后在对象初始化器中,通过给属性或者索引器赋值来实现。

public class Person{    public string? FirstName { get; set; }    public string? LastName { get; set; }}

这种方式最大的局限就是,对于初始化来说,属性必须是可变的,也就是说,set访问器对于初始化来说是必须的,而其他情况下又不需要set,而且我们需要的是不可变对象类型,因此这个setter明显在这里就不合适。既然有这种常见的需要和局限性,那么我为何不引入一个只能用来初始化的Setter呢?于是只用来初始化的init设置访问器就出现了。这时,上面的Point结构定义就可以简化成下面的样子:

struct Point{    public int X { get; init; }    public int Y { get; init; }}

那么现在,我们使用对象初始化器来创建一个实例:

var p = new Point() { X = 54, Y = 717 };

第二例子Person类型中,将set访问器换为init就成了不可变类型了。同时,使用对象初始化器方式没有变化,依然如前面所写。

public class Person{    public string? FirstName { get; init; }    public string? LastName { get; init; }}

通过采用init访问器,编码量减少,满足了只读需求,代码简洁易懂。

2. 定义和要求

只初始化属性或索引器访问器是一个只在对象构造阶段进行初始化时用来赋值的set访问器的变体,它通过在set访问器的位置来使用init来实现的。init有着如下限制:

  • init访问器只能用在实例属性或索引器中,静态属性或索引器中不可用。

  • 属性或索引器不能同时包含init和set两个访问器

  • 如果基类的属性有init,那么属性或索引器的所有相关重写,都必须有init。接口也一样。

2.1 init访问器可设置值的时机

除过在局部方法和lambda表达式中,带有init访问器的属性和索引器在下面情况是被认为可设置的。这几个可以进行设置的时机,在这里统称为对象的构造阶段。除过这个构造阶段之外,其他的后续赋值操作是不允许的。

  • 在对象初始化器工作期间

  • 在with表达式初始化器工作期间

  • 在所处或者派生的类型的实例构造函数中,在this或者base使用上

  • 在任意属性init访问器里面,在this或者base使用上

  • 在带有命名参数的attribute使用中

根据这些时机,这意味着Person类可以按如下方式使用。在下面代码中第一行初始化赋值正确,第二行再次赋值就不被允许了。这说明,一旦初始化完成之后,只初始化属性或索引就保护着对象的状态免于改变。

var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; // OKperson.LastName = "Torgersen"; // 错误!

2.2 init属性访问器和只读字段

因为init访问器只能在初始化时被调用,所以在init属性访问器中可以改变封闭类的只读字段。需要注意的是,从init访问器中来给readonly字段赋值仅限于跟init访问器处于同一类型中定义的字段,通过它是不能给父类中定义的readonly字段赋值的,关于这继承有关的示例,我们会在2.4类型间的层级传递中看到。

public class Person{    private readonly string firstName = "<unknown>";    private readonly string lastName = "<unknown>";        public string FirstName     {         get => firstName;         init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));    }    public string LastName     {         get => lastName;         init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));    }}

2.3 类型层级间的传递

我们知道只包含get访问器的属性或索引器只能在所处类的自身构造函数中被初始化,但init访问器可以进行设置的规则是可以跨类型层级传递的。带有init访问器的成员只要是可访问的,对象实例并能在构造阶段被知晓,那这个成员就是可设置的。

class Person{    public Person()    {        //下面这段都是允许的        Name = "Unknown";        Age = 0;    }    public string Name { get; init; }    public int Age { get; }}class Adult : Person{    public Adult()    {        // 只有get访问器的属性会出错,但是带有init是允许的        Name = "Unknown Adult"; //正确        Age = 18; //错误    }}class Consumption{    void Example()    {        var d = new Adult()         {             Name = "Jack", //正确            Age = 23 //错误,因为是只读,只有get        };    }}

从init访问器能被调用这一方面来看,对象实例在开放的构造阶段就可以被知晓。因此除过正常set可以做之外,init访问器的下列行为也是被允许的。

  • 通过this或者base调用其他可用的init访问器

  • 在同一类型中定义的readonly字段,是可以通过this给赋值的

class Example{    public Example()    {        Prop1 = 1;    }    readonly int Field1;    string Field2;    int Prop1 { get; init; }    public bool Prop2    {        get => false;        init        {            Field1 = 500; // 正确            Field2 = "hello"; // 正确            Prop1 = 50; // 正确        }    }}

前面2.2节中提到,init中是不能更改父类中的readonly字段的,只能更改本类中readonly字段。示例代码如下:

class BaseClass{    protected readonly int Field;    public int Prop    {        get => Field;        init => Field = value; // 正确    }    internal int OtherProp { get; init; }}class DerivedClass : BaseClass{    protected readonly int DerivedField;    internal int DerivedProp    {        get => DerivedField;        init        {            DerivedField = 89;  // 正确            Prop = 0;       // 正确            Field = 35;     // 出错,试图修改基类中的readonly字段Field        }    }    public DerivedClass()    {        Prop = 23;  // 正确         Field = 45;     // 出错,试图修改基类中的readonly字段Field    }}

如果init被用于virtual修饰的属性或者索引器,那么所有的覆盖重写都必须被标记为init,是不能用set的。同样地,我们不可能用init来覆盖重写一个set的。

class Person{    public virtual int Age { get; init; }    public virtual string Name { get; set; }}class Adult : Person{    public override int Age { get; init; }    public override string Name { get; set; }}class Minor : Person{    // 错误: 属性必须有init来重写Person.Age    public override int Age { get; set; }    // 错误: 属性必须有set来重写Person.Name    public override string Name { get; init; }}

2.4 init和接口

一个接口中的默认实现,也是可以采用init进行初始化,下面就是一个应用模式示例。

interface IPerson{    string Name { get; init; }}class Initializer{    void NewPerson<T>() where T : IPerson, new()    {        var person = new T()        {            Name = "Jerry"        };        person.Name = "Jerry"; // 错误    }}

2.5 init和readonly struct

init访问器是允许在readonly struct中的属性中使用的,init和readonly的目标都是一致的,就是只读。示例代码如下:

readonly struct Point{    public int X { get; init; }     public int Y { get; init; }}

但是要注意的是:

  • 不管是readonly结构还是非readonly结构,不管是手工定义属性还是自动生成属性,init都是可以使用的。

  • init访问器本身是不能标记为readonly的。但是所在属性或索引器可以被标记为readonly

struct Point{    public readonly int X { get; init; } // 正确    public int Y { get; readonly init; } // 错误}
(0)

相关推荐

  • 最权威的 Android Oreo 新特性详解(带有中文视频讲解)

    [公众号回复 "编程视频" 送您一个特别推送] Android 8.0 是谷歌推出的智能手机操作系统,2017年3月21日Google 为开发者推出了新的 Android O 首个 ...

  • 从0到1详解在线旅游产品设计

    转自人人都是产品经理,仅供参考 本文从立足于旅游市场,从市场的方向简析,也是本人参赛的一次复盘,产品学生,还望大家多指导. 一.旅游大市场分析 1.1 总体来看,市场上升强劲 旅游大市场来看,根据国家 ...

  • MySQL8.0新特性

    MySQL从5.7一跃直接到8.0,这其中的缘由,咱就不关心那么多了,有兴趣的朋友自行百度,本次的版本更新,在功能上主要有以下6点: 账户与安全 优化器索引 通用表表达式 窗口函数 InnoDB 增强 ...

  • iOS14.7发布新更新,修复朋友圈定位修改BUG性能提升,新功能详解

    本次更新依然只是例行惯例的修复BUG 和玄学性的性能提升,可能是大的改进要留给iOS15吧. 朋友圈关注公众平台"光闪速秒" ① 空气质量指数支持更多国家 在该版本中天气APP的[ ...

  • Vue3.0 新特性以及使用变更总结(实际工作用到的)

    前言 Vue3.0 在去年9月正式发布了,也有许多小伙伴都热情的拥抱Vue3.0.去年年底我们新项目使用Vue3.0来开发,这篇文章就是在使用后的一个总结, 包含Vue3新特性的使用以及一些用法上的变 ...

  • Vue3.0 新特性以及使用经验总结

    vue3.0Vue3.0 在去年9月正式发布了,也有许多小伙伴都热情的拥抱Vue3.0.去年年底我们新项目使用Vue3.0来开发,这篇文章就是在使用后的一个总结, 包含Vue3新特性的使用以及一些用法 ...

  • C#8.0新特性

    只读成员 private struct Point { public Point(double x, double y) { X = x; Y = y; } private double X { ge ...

  • 港股打新怎么操作?港股打新流程详解

    港股打新将涉及以下几大步骤申请香港银行卡---券商开户---入金---认购新股的选择---上市卖出策略 首先,在这里需要明确一个概念,香港的券商开户一般需要什么?一般需要身份证.手机号.香港银行卡.以 ...

  • 6日黄陂和新洲新增病例继续携手为0,同时详解何为武汉疫情存量和增量

    一.昨夜今晨重要消息解读 1.昨天(3月6日)晚上,湖北省委书记应勇同志在武汉召开疫情防控会议,谈到了武汉病例的存量和增量问题,报道如下: 在听取武汉市疫情防控工作汇报后,应勇指出,武汉市疫情防控形势 ...