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" }; // OK
person.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; } // 错误
}

如对您有价值,请推荐,您的鼓励是我继续的动力,在此万分感谢。关注本人公众号“码客风云”,享第一时间阅读最新文章。

C# 9.0新特性详解系列之一:只初始化设置器(init only setter)的更多相关文章

  1. C#9.0新特性详解系列之六:增强的模式匹配

    自C#7.0以来,模式匹配就作为C#的一项重要的新特性在不断地演化,这个借鉴于其小弟F#的函数式编程的概念,使得C#的本领越来越多,C#9.0就对模式匹配这一功能做了进一步的增强. 为了更为深入和全面 ...

  2. C# 9.0新特性详解系列之五:记录(record)和with表达式

    1 背景与动机 传统面向对象编程的核心思想是一个对象有着唯一标识,表现为对象引用,封装着随时可变的属性状态,如果你改变了一个属性的状态,这个对象还是原来那个对象,就是对象引用没有因为状态的改变而改变, ...

  3. C# 9.0新特性详解系列之二:扩展方法GetEnumerator支持foreach循环

    1.介绍 我们知道,我们要使一个类型支持foreach循环,就需要这个类型满足下面条件之一: 该类型实例如果实现了下列接口中的其中之一: System.Collections.IEnumerable ...

  4. C# 9.0新特性详解系列之三:模块初始化器

    1 背景动机 关于模块或者程序集初始化工作一直是C#的一个痛点,微软内部外部都有大量的报告反应很多客户一直被这个问题困扰,这还不算没有统计上的客户.那么解决这个问题,还有基于什么样的考虑呢? 在库加载 ...

  5. [转]Servlet 3.0 新特性详解

    原文地址:http://blog.csdn.net/xiazdong/article/details/7208316 Servlet 3.0 新特性概览 1.Servlet.Filter.Listen ...

  6. Servlet 3.0 新特性详解

    转自:http://www.ibm.com/developerworks/cn/java/j-lo-servlet30/#major3 Servlet 是 Java EE 规范体系的重要组成部分,也是 ...

  7. 【转帖】Servlet 3.0 新特性详解

    http://www.ibm.com/developerworks/cn/java/j-lo-servlet30/ Servlet 3.0 新特性概述 Servlet 3.0 作为 Java EE 6 ...

  8. Servlet 3.0 新特性详解 (转载)

    原文地址:https://www.ibm.com/developerworks/cn/java/j-lo-servlet30/ Servlet 3.0 新特性概述 Servlet 3.0 作为 Jav ...

  9. Android6.0 新特性详解

    一 运行时权限 Android6.0 引入了一个新的应用权限模型,期望对用户更容易理解,更易用和更安全.该模型将标记为危险的权限从安装时权限(Install Time Permission)模型 移动 ...

随机推荐

  1. 基于Docker的MySql

    MySQL Server安装教程 考虑到实际情况需要经常使用MySQL,为了方便大家能够快速基于Docker搭建MySQL这里以Linux下为例 进行说明,对于Windows用户来说直接通过查看官网H ...

  2. D - 活动选择

    Description 学校的大学生艺术中心周日将面向全校各个学院的学生社团开放,但活动中心同时只能供一个社团活动使用,并且每一个社团活动开始后都不能中断.现在各个社团都提交了他们使用该中心的活动计划 ...

  3. C++学习笔记---指针

    1.基本概念 作用:可以通过指针直接访问内存 内存编号是从0开始记录的,一般用16进制数字表示 可以利用指针变量保存地址 2.定义和使用 定义指针 他们的关系就是这样的 运行输出 使用指针 运行输出会 ...

  4. [C#] (原创)一步一步教你自定义控件——03,SwitchButton(开关按钮)

    一.前言 技术没有先进与落后,只有合适与不合适. 本篇的自定义控件是:开关按钮(SwitchButton). 开关按钮非常简单,实现方式也多种多样,比如常见的:使用两张不同的按钮图片,代表开和关,然后 ...

  5. 500G Python从入门到进阶的视频资料

    第一部分:Python资源500G百度网盘学习视频300+本电子书需要的小伙伴可以叫小编的Q群867067945 点击展开,查看完整图片 回复关键字:学习第二部分:Python就业指导 更多更详细的就 ...

  6. Parquet 源码解析

    date: 2020-07-20 16:15:00 updated: 2020-07-27 13:40:00 Parquet 源码解析 Parquet文件是以二进制方式存储的,所以是不可以直接读取的, ...

  7. Java学习的第十九天

    1.今天学了接口只能有抽象的常量和方法,接口为interface    承接接口是implements 接口的使用 接口中的方法必须是抽象的,没有构造方法 2.今天没有问题 3.明天学习第六章综合实例 ...

  8. 技术总监的故事告诉大家,要学会say【NO!】

    今天就给大家分享一个发生在我自己身上的事情吧. 1 2015年的时候,我和我的领导A,还有几个小伙伴正在做一个"紧急定制",这个任务是公司老大CEO和重要客户定下来的一个项目,背后 ...

  9. 简单谈谈Hilt——依赖注入框架

    今天继续Jetpack专题,相信不少的朋友都使用过Dagger,也放弃过Dagger,因为实在太难用了.所以官方也是为了让我们更好使用依赖注入框架,为我们封装了一个新的框架--Hilt,今天一起来看看 ...

  10. SAM学习笔记&AC自动机复习

    形势所迫,一个对字符串深恶痛绝的鸽子又来更新了. SAM 后缀自动机就是一个对于字符串所有后缀所建立起的自动机.一些优良的性质可以使其完成很多字符串的问题. 其核心主要在于每个节点的状态和$endpo ...