为什么我不能在初始化器中初始化只读变量?

26

为什么我不能在初始化器中初始化只读变量?以下代码并没有按照预期运行:

class Foo
{
    public readonly int bar;
}

new Foo { bar=0; }; // does not work

这是CLR的一些技术限制导致的吗?

编辑

我知道new Foo { bar=0; }new Foo().bar=0;是相同的,但是“只读”是由CLR强制执行的,还是仅仅是编译器的限制?


3
根据您的最新编辑,我不知道您在问什么。是的,“readonly”由CLR在运行时执行。我不明白它为什么会是编译器的限制。其他回答解释了为什么您尝试做的事情没有任何意义。 - Cody Gray
@Cody Gray -- 这是“语言限制”,而不是“编译器限制”。编译器只是实现语言的工具。 - user166390
@pst:嗯,我说“我不明白这怎么可能是编译器的限制。”我同意你的观点。 - Cody Gray
11个回答

20

这个初始化器只是一种语法糖。当你写下:

new Foo { bar=0; };

(顺便说一下,这是语法错误,应该是这样的...)

new Foo { bar=0 }

实际上发生的是:

var x = new Foo();
x.bar = 0;

由于该属性是只读的,所以第二个语句是无效的。

编辑:根据你的编辑,问题有点不清楚。一个readonly属性是设计为不可设置的。它在对象构造时构建。这由编译器和运行时都强制执行。(尽管我没有测试过后者,因为需要一些技巧才能绕过前者。)

要记住,有两个“编译”阶段。将C#代码编译成IL代码时强制执行,将IL代码编译成机器代码时也强制执行。

这不是CLR的技术限制,考虑到明确的readonly声明,它正按照其预期方式工作。在对象构造后,您无法设置readonly属性。


1
这个答案越来越好了。我已经看过两次并想点赞,但意识到我已经点过赞了。 - Cody Gray
@Cody Gray:有人似乎不同意,给了一个踩。我相当确定这个问题是重复的,我想。很难判断提问者的意图。 - David
@JeppeStigNielsen:这就可以被归为“一些用来绕过编译器的巧妙手段了。”在许多情况下,这正是反射所能发挥的作用。例如,还可以使用反射访问私有成员。 - David
1
当然可以!但这很可能会导致对象失效,因为Foo的作者并没有预料到你会这样做。但也许提问者对此感兴趣,作为对你出色回答的评论。 - Jeppe Stig Nielsen
@DemurRumed:好发现,谢谢!我想当我写这篇文章的时候,我只是从原始文本中复制/粘贴了。我已经更新了答案。 - David
显示剩余2条评论

18

在初始化程序中设置readonly会引入无法在编译时强制执行的矛盾和复杂性。我想限制是为了避免歧义。关键是进行编译时验证。

想象一下:

class Foo
{
    public readonly int bar;
    Foo () {
      // compiler can ensure that bar is set in an invoked ctor
      bar = 0;
    }
}

// compiler COULD know that `bar` was set in ctor
// and therefore this is invalid
new Foo { bar = 0; }

现在,考虑以下内容:
class Foo
{
    public readonly int bar;
    Foo () {
      // imagine case where bar not set in ctor
    }
}

// compiler COULD know that `bar` is not bound yet
// therefore, this COULD be valid
new Foo { bar = 0; }

// but this COULD be proved to never be valid
new Foo();

假设上述两种情况被统一了(比如说,“通过编译器的魔法”),然而加入泛型:
T G<T> () where T : new
{
  // What in heck should happen *at compile time*?
  // (Consider both cases above.)
  // What happens if T (Foo) changes to include/not-include setting the
  // readonly variable in the ctor?
  // Consider intermediate code that invokes G<Foo>() and this other
  // code is NOT recompiled even though Foo is--
  //   Yet a binary incompatibility has been added!
  //   No thanks!
  return new T();
}
G<Foo>();

我认为我所概述的案例展示了使用“动态”readonly方法的一些复杂性,最终我相信这只是一种“选择性语言限制”(编译器实现语言)来强制/允许编译时验证。


8

这是现代C#的正确答案。 - phoenix
我希望这个回答能够向被采纳的回答上浮动一点。 - dacabdi
请注意,这些不再是简单的readonly _fields_,它们现在是_properties_,生成的IL现在将是一个函数调用而不是访问字段。请参见https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgZgAQCcCmBDGA9gHYQCe6AzsJgK4DGw6ASnjAPImkBiY2EMAWABQAb2HoJ6NFlbEy6MEUYA1XBBrYA3OMk6J0lvg5kefGAApFjAG5qNASj3oxQyW/Sr12dAF50tr21XSQBfYTChYWkqWgZmVmNSAAVMAgAHbExgUmEXN2krDztvEXQAc2xgTQUiMCr0CIiojGQAJnQAFWwqXKcCpXj8U35zR2CJPPcJW0waqlwiOm8/ImwAd0H2TmGLABZW+yCp6dxZgI1fOeAFpYA6Tw0j4+QAdn9ip4kItz6MQsMYCl0plsqMnJMpjMrjdluhVhsAYkgRksuRSg9YfsGp93FDzrDFPNFth7h8nPk3vicY1hEA=== - Erunehtar

5

readonly变量必须在构造函数中初始化,而属性初始化程序在对象构造之后执行,因此不可行。


3

因为初始化器等同于

var foo = new Foo();
foo.bar=0;

这是一次幕后重写。

2
你想要做的是这样的:
   class Foo
   {
        public readonly int bar;

        Foo(int b)
        {
             bar = b;  // readonly assignments only in constructor
        }
   }

   Foo x = new Foo(0);

C# 一直以来都以冗长和样板代码著称。我真的很期待他们引入记录类型。 - Joe Koberg

1

因为您指定了它为只读。这样指定某物为只读,然后期望写入语句起作用是没有意义的。


1
根据此页面,CLR 在处理初始化列表之前首先进行对象的默认构造,因此您将对bar赋值两次(一次在默认构造时,一次在处理初始化器时)。

0
这是实现readonly关键字的结果。下面的引用摘自readonlyMSDN参考

readonly关键字是一个修饰符,可用于字段上。当字段声明包括readonly修饰符时,只能在声明中或同一类中的构造函数中作为声明引入的字段进行赋值。


0

我知道这不是对发帖者问题的直接回答,但是C#的新版本现在允许从属性本身进行直接初始化,如下所示。

public int bar { get; set; } = 0;

再次声明,我知道这并没有解决上述已经识别(和解决)的只读问题。


欢迎来到Stack Overflow。如果您知道它并不能回答问题,但您想添加相关的附加信息,那么评论更为合适。 - belwood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接