在 C#9 中,仅初始化属性与只读属性有何不同?

Mar*_*eIV 7 c# readonly c#-9.0 init-only

我一直在阅读 C#9 中的 init-only 属性,但我认为我们已经有了只能在构造函数中设置的只读属性。在那之后,它是不可变的。

例如,在这里的类中,NameDescription都可以在构造函数中赋值,但只能在构造函数中赋值,这正是 init-only 属性的描述方式。

示例类


class Thingy {
    
    public Thingy(string name, string description){
        Name        = name;
        Description = description;
    }
    
    public string Name        { get; }
    public string Description { get; }
    
    public override string ToString()
        => $"{Name}: {Description}";
}
Run Code Online (Sandbox Code Playgroud)

测试程序

using System;

class Program {

    public static void Main (string[] args) {
        
        var thingy = new Thingy("Test", "This is a test object");
        Console.WriteLine(thingy);
        // thingy.Name = “Illegal”; <— Won’t compile this line
    }
}
Run Code Online (Sandbox Code Playgroud)

这将输出以下内容:


class Thingy {
    
    public Thingy(string name, string description){
        Name        = name;
        Description = description;
    }
    
    public string Name        { get; }
    public string Description { get; }
    
    public override string ToString()
        => $"{Name}: {Description}";
}
Run Code Online (Sandbox Code Playgroud)

此外,如果我尝试修改NameDescription在构造函数运行后,它将无法编译。

那么我错过了什么?

ang*_*son 8

一个init访问是相同set实施访问的几乎所有领域,但它是在一定的方式,使得它的编译器不允许使用外的一些具体的上下文标记。

通过相同的我真的不意味着相同。创建的隐藏方法的名称是set_PropertyName,就像set访问器一样,使用反射你甚至无法区分它们,它们看起来是相同的(请参阅我下面关于此的注释)。

不同之处在于编译器使用此标志(更多内容见下文)将仅允许您在少数特定上下文中为 C# 中的属性设置值(下文也有更多内容)。

  • 从类型的构造函数或派生类型
  • 从对象初始值设定项,即。 new SomeType { Property = value }
  • 从带有 newwith关键字的构造中,即。var copy = original with { Property = newValue }
  • init另一个属性的访问器中(因此一个init访问器可以写入其他init访问器属性)
  • 从属性说明符,所以你仍然可以写 [AttributeName(InitProperty = value)]

在这些之外,基本上相当于正常的属性分配,编译器将阻止您写入带有编译器错误的属性,如下所示:

CS8852 仅初始化属性或索引器“Type.Property”只能在对象初始值设定项中分配,或者在实例构造函数或“init”访问器中的“this”或“base”上分配。

因此,鉴于这种类型:

public class Test
{
    public int Value { get; init; }
}
Run Code Online (Sandbox Code Playgroud)

您可以通过以下所有方式使用它:

var test = new Test { Value = 42 };
var copy = test with { Value = 17 };

...

public class Derived : Test
{
    public Derived() { Value = 42; }
}

public class ViaOtherInit : Test
{
    public int OtherValue
    {
        get => Value;
        init => Value = value + 5;
    }
}
Run Code Online (Sandbox Code Playgroud)

但你不能这样做:

var test = new Test();
test.Value = 42; // Gives compiler error
Run Code Online (Sandbox Code Playgroud)

因此,出于所有意图和目的,这种类型是不可变的,但它现在允许您更轻松地构造该类型的实例,而不会陷入这种不可变性问题。


我在上面说过,反射并没有真正看到这一点,并注意我今天才了解实际机制,所以也许有一种方法可以找到一些可以真正区分差异的反射代码。重要的部分是编译器可以看到差异,这就是。

鉴于类型声明为:

public class Test
{
    public int Value1 { get; set; }
    public int Value2 { get; init; }
}
Run Code Online (Sandbox Code Playgroud)

那么为这两个属性生成的 IL 将如下所示:

.property instance int32 Value1()
{
    .get instance int32 UserQuery/Test::get_Value1()
    .set instance void UserQuery/Test::set_Value1(int32)
}
.property instance int32 Value2()
{
    .get instance int32 UserQuery/Test::get_Value2()
    .set instance void modreq(System.Runtime.CompilerServices.IsExternalInit) UserQuery/Test::set_Value2(int32)
}
Run Code Online (Sandbox Code Playgroud)

您可以看到Value2属性设置器(init方法)已被标记/标记(不确定这些词是否正确,我确实说过我今天学到了这个),其modreq(System.Runtime.CompilerServices.IsExternalInit)类型告诉编译器此方法不是您叔叔的设置访问器。

这就是编译器如何知道将这个访问器方法与普通set访问器区别对待的方式。

鉴于@canton7对这个问题的评论,这个modreq构造也意味着如果你尝试在旧的 C# 编译器中使用用新的 C# 9 编译器编译的库,它不会考虑这个方法。这也意味着您将无法在对象初始值设定项中设置属性,但这当然只能在 C# 9 和更新的编译器中使用。


那么设置值的反射呢?好吧,事实证明反射将能够init很好地调用访问器,这很好,因为这意味着反序列化,您可能会认为它是一种对象初始化,仍然会按您的预期工作。

观察以下LINQPad程序:

void Main()
{
    var test = new Test();
    // test.Value = 42; // Gives compiler error
    typeof(Test).GetProperty("Value").SetValue(test, 42);
    test.Dump();
}

public class Test
{
    public int Value { get; init; }
}
Run Code Online (Sandbox Code Playgroud)

产生这个输出:

反射代码的输出

这是一个 Json.net 示例:

void Main()
{
    var json = "{ \"Value\": 42 }";
    var test = JsonConvert.DeserializeObject<Test>(json);
    test.Dump();
}
Run Code Online (Sandbox Code Playgroud)

这给出了与上面完全相同的输出。


can*_*on7 5

不同之处在于init属性可以从对象初始值设定项以及构造函数中设置:

public class C
{
     public int Foo { get; init; }   
}

// Legal
var c = new C()
{
    Foo = 3,  
};

// Illegal
c.Foo = 4;
Run Code Online (Sandbox Code Playgroud)

参见夏普实验室

如果您声明带有属性的记录init,编译器还允许您使用with表达式设置它们:

public record C
{
    public int Foo { get; init; }
}

var c = new C() { Foo = 3 };
var d = c with { Foo = 4 };
Run Code Online (Sandbox Code Playgroud)

参见夏普实验室

使用反射时它们也显示为可写。这是一个经过深思熟虑的设计决策,允许基于反射的序列化器反序列化为具有仅限 init 属性的对象,而无需进行修改。

public class C
{
    public int GetterOnly { get; }
    public int InitOnly { get; init; }
}

typeof(C).GetProperty("GetterOnly").CanWrite); // False
typeof(C).GetProperty("InitOnly").CanWrite); // True
Run Code Online (Sandbox Code Playgroud)

参见夏普实验室