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
就所有意图而言,这种类型是不可变的,但现在它允许你更轻松地构造类型的实例,而不会遇到不可变性问题。
我之前说过反射实际上看不到这一点,并且注意到我今天才了解到实际机制,因此也许存在一种找到某些反射代码并能够实际区分差异的方法。重要的部分是编译器可以看到区别,在这里它是:
由于该类型被声明为:
public class Test
{
public int Value1 { get; set; }
public int Value2 { get; init; }
}
那么这两个属性所生成的中间语言代码将如下所示:
.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
属性的setter(即init
方法)已被标记为modreq(System.Runtime.CompilerServices.IsExternalInit)
类型,告诉编译器此方法不是普通的set访问器。
这是编译器知道如何将此访问器方法与普通的set
访问器方法区别对待的方式。
基于@canton7在该问题上的评论,这个modreq
结构还意味着如果您尝试在旧版本的C#编译器中使用使用新版C# 9编译器编译的库,则编译器将不会考虑此方法。它还意味着您将无法在对象初始化程序中设置属性,但这当然仅适用于C# 9及更高版本的编译器。
那么对于设置值的反射怎么办?好吧,事实证明,反射将能够很好地调用init
访问器,这很好,因为这意味着反序列化(您可以认为它是一种对象初始化)仍将按预期工作。
请注意以下LINQPad程序:
void Main()
{
var test = new Test();
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();
}
它的输出与上面完全相同。
init
访问器与set
访问器相同,除了编译器将阻止您在“允许的上下文”之外使用它,这些上下文包括构造函数、对象初始化程序或新的with
关键字。这也意味着反射将能够使用它设置值,因此例如反序列化将工作。 - Lasse V. Karlsenmodreq
修改器,今天学到了新东西,这也是我觉得自己没有资格发表实际答案的原因之一(尽管我现在看到这里的答案忽略了技术细节),就是我看不出一个set
访问器和一个init
访问器之间实际上区别在哪里,我使用反射检查了属性定义和方法定义,没有任何迹象表明有这种init
差异,但现在我知道在 IL 级别下有一些不用常规反射即可获取的内容。 - Lasse V. Karlsenmodreq
。 (https://sharplab.io/#v2:EYLgtghgzgLgpgJwDQxASwDZICYgNQA+AAgEwCMAsAFDVEDMABKQwMLUDe1D3TjaAdjAYAxAPaiG7BgHM4MANwMBaBQwC+1DTSoB6HQwCS0/qIRwGAWgsN+cONnsMAZqYYBlABYQEABwAyEMAMABTQSjBQDABuiFBoovwMok4MMB7mCACugmhg5hgQ0pHAcB4C2KnpDADGomA+mIjRsfH8AJTU/BB5UD4Q1eZEZAB0AErZMLlwwyx1DRiIbohRaANQHFw89ExkAGxMJIZQAKIAHvAIXRgG/CqS6ppAA=)。 - canton7