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

一、背景与动机

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

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

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

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

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

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结构定义就能够简化成下面的样子:code

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访问器的属性和索引器在下面状况是被认为可设置的。这几个能够进行设置的时机,在这里统称为对象的构造阶段。除过这个构造阶段以外,其余的后续赋值操做是不容许的。get

  • 在对象初始化器工做期间
  • 在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; } // 错误
}

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

码客风云
相关文章
相关标签/搜索