C# 9.0新特性——只初始化设置器
作者:未来的邀请 发布时间:2023-03-19 02:31:45
自C#1.0版本以来,我们要定义一个不可变数据类型的基本做法就是:先声明字段为readonly,再声明只包含get访问器的属性。例子如下:
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; } // 错误
}
作者:MarkKang
出处:https://www.cnblogs.com/markkang/
来源:https://www.cnblogs.com/markkang/p/14011089.html
猜你喜欢
- 这篇文章主要介绍了Spring框架实现AOP添加日志记录功能过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习
- 前言MongoDB是一款由C++编写的高性能、开源、无模式的常用非关系型数据库产品,是非关系数据库当 * 能最丰富、最像关系数据库的数据库。它
- 本文实例为大家分享了java图形用户界面实现菜单功能的具体代码,供大家参考,具体内容如下题目:编写一个图形用户界面,实现菜单的功能。有3个一
- 一 :问题背景问题:当查询接口较复杂时候,数据的获取都需要[远程调用],必然需要花费更多的时间。 假如查询文章详情页面,需要如下标注的时间才
- 一、lombok简介lombok 提供了使用注解的形式帮助简化消除java代码。在编写Java代码时,通过使用对应的注解,可以简化开发,同时
- 前言Spring常见的创建bean实例的方式有:1.通过bean的class属性创建实例 无参构造器带参构造器2.工厂方法静态工厂
- 1、堆的定义①、它是完全二叉树,除了树的最后一层节点不需要是满的,其它的每一层从左到右都是满的。注意下面两种情况,第二种最后一层从左到右中间
- 被覆盖比较好理解,类似于多态的实现,访问时通过类方法表来访问,你实际是什么类型,访问的方法就是那个类型的方法而不会是你的父类的方法。被隐藏是
- 摘要:在spring boot中 MVC这部分也有默认自动配置,也就是说我们不用做任何配置,那么也是OK的,这个配置类就是 WebMvcAu
- 概述在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的。也就是说,程序的流程对运行结果有直接的影响。所以,我们必须清楚每条
- 什么是线程池是一种基于池化思想管理线程的工具。池化技术:池化技术简单点来说,就是提前保存大量的资源,以备不时之需。比如我们的对象池,数据库连
- Java读取Properties文件的方法总结  
- 方式一:在所有mapper接口使用@Mapper注解@Mapper(将包中的所有接口都标注为DAO层接口)public interface
- 一、简介Lock关键字是Monitor的一种替换用法,lock在IL代码中会被翻译成Monitor. lock (obj) &nb
- 流程分析首先,使用mybatis的时候会定义mapper接口的基础包,一般我们会用@MapperScanner这个注解,来看下这个注解&nb
- Android内部没有控件来直接显示文档,跳转WPS或其他第三方文档App体验性不好,使用腾讯X5内核能很好的解决的这一问题。一、下载腾讯X
- 使用@Value取值出现的问题在springBoot项目中我们一般会把一些路径或者资源写在配置文件中,方便管理。但是取得时候有可能会出现一些
- 短网址应用已经在全国各大微博上开始流行了起来。例如QQ微博的url.cn,新郎的sinaurl.cn等。我们在QQ微博上发布网址的时候,微博
- Lambda表达式的进化之路为什么要使用Lambda表达式可以简洁代码,提高代码的可读性可以避免匿名内部类定义过多导致逻辑紊乱在原先实现接口
- 本文实例讲述了C#中实现子类调用父类的方法,分享给大家供大家参考之用。具体方法如下:一、通过子类无参构造函数创建子类实例创建父类Person