【问题标题】:In C#9, how do init-only properties differ from read-only properties?在 C#9 中,仅初始化属性与只读属性有何不同?
【发布时间】:2020-11-09 09:49:55
【问题描述】:

我一直在阅读 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}";
}

测试程序

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
    }
}

这会输出以下内容:

Test: This is a test object

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

那么我错过了什么?

【问题讨论】:

  • 由于我对此只有观察性的了解,因此我将避免发布答案,毫无疑问,有人会过来发布一个好的答案,但是就这样吧。 init 访问器与 set 访问器相同,只是编译器会阻止您在“允许的上下文”之外使用它,它来自构造函数、对象初始化器或新的 with 关键字。这也意味着反射将能够使用它设置值,因此例如反序列化将起作用。
  • 不同之处在于init-only 属性也可以在对象初始化器中使用,这意味着不再需要编写大量构造器来初始化所有可能的组合(这是其中之一)当今重要的不可变类型的主要痛苦)。
  • @LasseV.Karlsen 嗯,你是
  • 这可能会有所帮助Init-Only对象初始化器
  • 伙计……下面有很多很好的答案。很难选择哪一个是例外答案。

标签: c# readonly c#-9.0 init-only


【解决方案1】:

init 访问器在几乎所有领域的实现中都与set 访问器相同,除了它以某种方式标记,使得编译器不允许在一些特定的上下文。

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

不同之处在于,使用此标志的编译器(下文将详细介绍)仅允许您在一些特定上下文中为 C# 中的属性设置一个值(以下也将详细介绍)。

  • 来自类型的构造函数或派生类型
  • 来自对象初始化器,即。 new SomeType { Property = value }
  • 从带有新的with 关键字的构造中,即。 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; }
}

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

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;
    }
}

但你不能这样做:

var test = new Test();
test.Value = 42; // Gives compiler error

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


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

假设类型被声明为:

public class Test
{
    public int Value1 { get; set; }
    public int Value2 { get; init; }
}

那么为这两个属性生成的 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)
}

您可以看到 Value2 属性设置器(init 方法)已使用 modreq(System.Runtime.CompilerServices.IsExternalInit) 类型标记/标记(不确定这些是否正确,我确实说过我今天学到了这个)编译器这个方法不是你大叔的set accessor。

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

鉴于@canton7's cmets 关于这个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; }
}

产生这个输出:

这是一个 Json.net 示例:

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

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

【讨论】:

  • 啊,抱歉 LINQPad 截图的 4K 副本:P
  • modreq - 顾名思义 - 是必需的。它是方法签名的一部分,由运行时用于重载决议。如果您尝试使用 modreq 调用方法并且未在调用中指定它,它将无法解析。例如,modred 用于输出参数。我认为应该是TargetInvocationException。编辑:参考ECMA standard on the CLI
  • 很高兴知道,直到今天才知道modreq,所以我想知道编译器如何区分set 属性和init 属性。 LINQPad 有一个很好的 Diff 功能,例如 Util.Dif(p1, p2) 其中 p1p2PropertyInfo 用于两种类型的属性,它们仅在元数据令牌和属性名称上有所不同,但我现在看到了这比那更深。
  • 了解细节的赞!
【解决方案2】:

你可以写一个初始化体。就像一个固定的身体。除了它只会在初始化期间工作。

也可以从object initializers 或构造函数设置仅初始化属性。

初始化体示例:

    public string LastName
    {
        get => _lastName;
        init => _lastName = string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Shouldn't be null or whitespace",
                nameof(LastName))
            : value;
    }

来自下面第一个链接的示例。

另见:

【讨论】:

  • init 属性可以从对象初始值设定项以及构造函数中设置... 如果使用set'less 属性,它们只能是从初始化程序分配?还是仅来自构造函数? (我尝试了指向 SharpLab 的链接,但由于某种原因,第一个会产生错误,我不清楚它应该如何工作来说明原因。)
  • @KonradViltersten 我想你是想对canton7's answer 发表评论。无论如何,据我所知,SharpLab 的主分支(2022 年 3 月 18 日)有问题,请尝试将其更改为默认值。无论如何,第一个应该会导致错误(请参阅//Illegal 评论)。是的,你是对的,init 属性可以从对象初始化程序或构造函数中设置,我会更新我的答案(编辑:完成)。没有set 的属性不能从对象初始化器中设置,只能从构造器中设置。
  • 您在上面评论中的所有陈述都是完全正确的。我不知道我是如何设法发布到另一个我想要的答案的。脑残,我猜... :)
【解决方案3】:

想象一下你会有一个无参数的构造函数:

class Thingy {
    
    public Thingy(){
    }
    
    public string Name        { get; }
    public string Description { get; }
    
    public override string ToString()
        => $"{Name}: {Description}";
}

那么你不能这样做:

var test = new Thingy
 {
 Name = "Test",
 Description "Test"
 };

如果您使用 init 关键字编写类:

class Thingy {
    
    public Thingy(){
    }
    
    public string Name        { get; init; }
    public string Description { get; init; }
    
    public override string ToString()
        => $"{Name}: {Description}";
}

那么上面的代码就合法了。

【讨论】:

    【解决方案4】:

    不同之处在于init 属性可以从对象初始化器以及构造器中设置:

    public class C
    {
         public int Foo { get; init; }   
    }
    
    // Legal
    var c = new C()
    {
        Foo = 3,  
    };
    
    // Illegal
    c.Foo = 4;
    

    See SharpLab.

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

    public record C
    {
        public int Foo { get; init; }
    }
    
    var c = new C() { Foo = 3 };
    var d = c with { Foo = 4 };
    

    See SharpLab.

    在使用反射时,它们也显示为可写。这是一个深思熟虑的设计决定,允许基于反射的序列化程序反序列化为具有仅 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
    

    See SharpLab.

    【讨论】:

    • 我在你的答案和关于我什至没有考虑过的 Set Methods 的答案之间左右为难。这使它非常强大。但是问题是,这将如何应用于 afaik 必须支持无参数构造函数的结构? (我知道很多记录地址。)
    • @MarkA.Donohoe 它们对结构的应用与对类的应用相同。就像 getter-only 属性一样,init-only 属性可以从构造函数中设置,并且它们也将被初始化为其默认值。您不必设置仅初始化属性。如果一个结构有一个仅初始化属性,并且您调用了它的无参数构造函数,然后没有显式设置它,则仅初始化属性将保持其默认值。
    【解决方案5】:

    我认为这个问题的答案可以在这里找到: Official doc

    “在 C# 中构建不可变数据的底层机制自 1.0 以来没有改变。它们仍然存在: 将字段声明为只读。 声明仅包含 get 访问器的属性。

    这些机制在允许构造不可变数据方面是有效的,但它们这样做是通过增加类型的样板代码的成本并选择此类类型脱离对象和集合初始化器等功能。这意味着开发人员必须在易用性和不变性之间做出选择。”

    文档详细解释了这些差异。

    【讨论】:

      猜你喜欢
      • 2019-04-25
      • 2014-07-17
      • 1970-01-01
      • 1970-01-01
      • 2017-01-09
      • 1970-01-01
      • 1970-01-01
      • 2014-04-12
      • 1970-01-01
      相关资源
      最近更新 更多