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. 【树形DP】NOI2003 逃学的小孩

    题目大意 题目链接 PS:可能出题人为了提高难度故意加了很多废话--实际上题目是很简单的 在一棵树上找3个点A.B.C,使AB+BC最大,且满足AC>AB. 样例输入 4 31 2 12 3 1 ...

  2. redhat系统服务器重启后提示An error occurred during the file system check.

    问题描述 浪潮一台NF8480M3外观红灯报警,鉴于无法登陆带外,只能对服务器进行断电重启操作 问题现象 重启后进入开机过程并报错,报错如下内容及图片如下所示,正常来说进入此界面后直接输入root密码 ...

  3. Thumbnailator处理图片

    读取源图 of(String... files) of(File... files) of(InputStream... inputStreams) of(URL... urls) 输出文件 toFi ...

  4. Tomcat6.0 支持 https

    环境信息 Linux系统 + Tomcat  (程序页面可以运行前提下) 条件:安装了JDK 查看指定版本信息 1 进入$JAVA_HOME/bin目录     (一般是这个目录  /usr/java ...

  5. 如何在construct3上开发游戏&游戏展示

    前言 为了更快体验做出游戏的快乐,我们可以直接采用construct3 提供的游戏模板.这里我用的是基础模板中的塔防游戏.我们在这个的基础上加进来"植物大战僵尸"的一些元素,包括内 ...

  6. 编译安装tree命令

    查看当前的tree [12:33:33 root@C8[ ~]#rpm -qi tree Name : tree Version : 1.7.0 Release : 15.el8 Architectu ...

  7. git学习(十一) idea git pull 解决冲突

    测试如下: 先将远程的代码修改,之后更新: 之后将工作区修改的代码(这里修改的代码跟远程修改的位置一样)提交到本地,之后拉取远程的代码,会发现有冲突: Accept Yours 就是直接选取本地的代码 ...

  8. Zotero导入Markdown here插件

    1. 下载Markdown Here源码包 网址:https://github.com/adam-p/markdown-here 2. 创建.xpi后缀文件 将文件夹 中的这几个文件放入同一个文件夹中 ...

  9. 【论文阅读】An Anchor-Free Region Proposal Network for Faster R-CNN based Text Detection Approaches

    懒得转成文字再写一遍了,直接把做过的PPT放出来吧. 论文连接:https://link.zhihu.com/?target=https%3A//arxiv.org/pdf/1804.09003v1. ...

  10. 走在深夜的小码农 Seventh Day

    Css3 Seventh Day writer:late at night codepeasant 学习大纲: 1. 定位(position) 介绍 1.1 为什么使用定位 我们先来看一个效果,同时思 ...