init 访问器只能初始化时赋值,是真的吗?

前言

C# 提供的 init 关键字用于在属性中定义访问器方法,可以让属性仅能在对象初始化的时候被赋值,其他时候只能为只读属性的形式。

例如下面代码可以正常执行:.

public class Demo
{
    public string Name { get; init; }
}

var demo = new Demo { Name = "Demo" };

但是当我们想修改属性值时,就会报错:

demo.Name = "My IO";

init 访问器只能初始化时赋值,是真的吗?

看来只能初始化时赋值了,真的是这样吗?

原理

首先,我们来看看 init 最后会编译成什么。

反编译代码,发现生成的代码和普通属性完全一致:

public class Demo
{
    // Token: 0x17000001 RID: 1
    // (get) Token: 0x06000003 RID: 3 RVA: 0x00002085 File Offset: 0x00000285
    // (set) Token: 0x06000004 RID: 4 RVA: 0x0000208D File Offset: 0x0000028D
    public string Name { get; set; }
}

进一步使用 IL DASM 反汇编成中间语言 (IL) 代码:

.property instance string Name()
  {
    .get instance string ConsoleApp1.Demo::get_Name()
    .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) ConsoleApp1.Demo::set_Name(string)
  } // end of property Demo::Name

可以看到,属性还是会编译成get_XXXset_XXX方法,只是 set_Name(string) 方法有一个 modreq 修饰符,并使用了 IsExternalInit 类型。

查看官方文档[1],原来使用 modreq 特性是为了让编译器知道它会限制对属性的访问,更为关键的是,它不会在以下情况下受到保护

  • 成员反射
  • 使用 dynamic
  • 不识别 modreqs 的编译器

实现

既然如此,我们可以使用反射在任何时候为 init-only 属性赋值:

var demo = new Demo { Name = "Demo" };

typeof(Demo).GetProperty("Name").SetValue(demo, "My IO"); 
Console.WriteLine(demo.Name);

//输出
My IO

结论

今天,我们了解到,init 其实是 C# 编译器的功能,在运行时还是可以修改其值的。