在C#中将字段标记为“readonly”的好处是什么?

380

将成员变量声明为只读有哪些好处?它仅保护类生命周期内防止其他人更改其值,还是使用此关键字会导致任何速度或效率的提高?


9
良好的外部答案:http://www.dotnetperls.com/readonly - OneWorld
3
有趣。这本质上是C#版的这个Java问题https://dev59.com/c3VC5IYBdhLWcg3w9GA9尽管讨论不太激烈... 嗯... - RAY
7
值得注意的是,结构体类型的readonly字段相对于未被改变的可变字段来说,会增加一些性能开销。因为调用任何一个readonly值类型字段的成员都会导致编译器复制该字段并在其上调用该成员。 - supercat
2
更多关于性能惩罚的信息:http://codeblog.jonskeet.uk/2014/07/16/micro-optimization-the-surprising-inefficiency-of-readonly-fields/ - CAD bloke
14个回答

223

我认为readonly字段并不能提高性能,它只是一个检查,确保对象构造完成后该字段不能指向新值。

但是,“readonly”和其他类型的只读语义非常不同,因为它在CLR中通过运行时强制执行。readonly关键字编译成.initonly,这可以被CLR验证。

这个关键字的真正优势是生成不可变数据结构。根据定义,不可变的数据结构在构建完成后就无法更改。这使得在运行时推理数据结构的行为非常容易。例如,传递不可变的结构到另一个随机的代码部分也没有危险,因为它们永远无法更改,所以你可以对该结构进行可靠地编程。

Robert Pickering写了一篇有关不可变性好处的好博客文章。该文章可以在这里archive.org备份找到。


6
如果你阅读了这篇文章 https://dev59.com/zGkw5IYBdhLWcg3wfKhB ,你会发现只读成员可以被修改,这似乎是 .net 的一种不一致行为。 - Akash Kava
“使结构体‘X’只读”分析 - 避免使用结构体时出现微妙的错误和负面性能影响的最佳方法是尽可能地将其设置为只读。在结构体声明中使用readonly修饰符清晰地表达了设计意图(强调结构体是不可变的),并帮助编译器避免在上述许多情况下进行防御性复制。 - Gavin Williams

218
readonly关键字用于声明常量成员变量,但允许在运行时计算其值。这与使用const修饰符声明的常量不同,后者必须在编译时设置其值。使用readonly,您可以在声明中或包含该字段的对象的构造函数中设置字段的值。
此外,如果您不想重新编译引用该常量的外部DLL(因为它在编译时被替换),也可以使用它。

8
速度与效率的好处呢?有吗? - Artemious
5
需要翻译的内容: 请记住,如果您将readonly分配给某些东西(比如类),例如private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>();,那么您仍然可以使用_ready.SetResult(true),所以readonly仅适用于字段,而不一定适用于对象的属性或状态。const也不仅仅是“编译时”这么简单——它不能用于所有与readonly相同的东西…… const只能保存字符串、int、bool或null。例如,您无法使用const HttpClient hello5 = new HttpClient();,但可以使用readonly - NotoriousPyro
2
@NotoriousPyro,你不能使用const HttpClient hello5 = new HttpClient()的原因正是因为在运行时分配了一个新的HttpClient。这真的就像“编译时”一样简单。即使结构体也是在运行时分配的,也不能是const。 - Leonardo G

82

readonly没有明显的性能优势,至少我从未在任何地方看到过这样的提及。它只是用于像您所建议的一样防止初始化后进行修改。

因此,它有助于您编写更健壮、更易读的代码。这种方法的真正好处出现在团队合作或维护时。将某些东西声明为readonly类似于在代码中添加变量使用协定。可以将其视为添加文档,就像其他关键字如internalprivate一样,您正在表达“此变量在初始化后不应被修改”,而且您正在强制执行它。

因此,如果您创建一个类并将一些成员变量标记为readonly,则可以防止自己或另一个团队成员以后在扩展或修改类时犯错误。在我看来,这是一个值得拥有的好处(虽然可能会稍稍增加语言复杂度,正如doofledorfer在评论中提到的那样)。


1
而另一方面,它简化了语言。不否认您的利益陈述。 - dkretz
3
我同意,但我认为真正的好处是当有多个人在编写代码时。这就像在代码中有一个小的设计说明书,一个对其使用的契约。我应该把这个放在答案中,呵呵。 - Xiaofu
8
我认为这个回答和讨论实际上是最好的回答,点赞。 - Jeff Martin
@Xiaofu:你让我对readonly的概念产生了恒定的印象,哈哈哈,美丽的解释,没有人能够理解最愚蠢的思维。 - Jasmine
你在代码中保持着这个值不会在任何时候改变的意图。 - Andez

66

说得具体点:

如果你在dll A中使用const,并且dll B引用了该const,那么该const的值将编译到dll B中。 如果您重新部署dll A,并为该const设置新值,则dll B仍将使用原始值。

如果您在dll A中使用readonly,并且dll B引用了该readonly,那么该readonly将始终在运行时查找。 这意味着如果您重新部署dll A并为该readonly设置新值,则dll B将使用该新值。


7
这是一个很好的实际例子,可以帮助理解其中的区别。谢谢。 - Shyju
另一方面,“const”可能比“readonly”具有更好的性能优势。以下是一个带有代码的更深入的解释: https://www.dotnetperls.com/readonly - Dio Phung
3
我认为这个答案中缺少了一个最重要的实用术语:在运行时将计算出的值存储到“readonly”字段中的能力。您无法在“const”中存储“new object();”,因为您无法在编译时将非值类型(如引用)混合到其他程序集中而不改变其标识。 - binki

22

可能存在一种情况,即编译器可以基于readonly关键字的存在进行性能优化。

只有在只读字段还被标记为静态时才适用。在这种情况下,JIT编译器可以假设这个静态字段永远不会发生改变。 JIT编译器可以在编译类的方法时考虑到这一点。

一个典型的例子:你的类可能有一个静态只读的IsDebugLoggingEnabled字段,在构造函数中初始化(例如,基于配置文件)。一旦实际的方法被JIT编译,当调试日志未启用时,编译器可以省略整个代码部分。

我没有检查当前版本的JIT编译器是否实际实现了这种优化,所以这只是猜测。


这个有来源吗? - Sedat Kapanoglu
7
目前的JIT编译器实际上已经实现了这一点,并且自CLR 3.5以来就一直如此。https://github.com/dotnet/coreclr/issues/1079 - mirhagk
1
无法对只读字段进行任何优化,因为只读字段实际上是可读写的。它们只是编译器提示,大多数编译器都会尊重它们,并且只读字段的值可以通过反射轻松地被覆盖(尽管在部分受信任的代码中不行)。 - Christoph

11

请记住,readonly仅适用于值本身,因此如果您正在使用引用类型,则readonly仅保护引用不被更改。实例的状态不受readonly保护。


4

5
请注意,如果在C#7.2中字段的类型为“readonly struct”,那么将该字段变为非只读的好处将消失。 - Jon Skeet

1

为了回答这个问题,我们需要添加一个基本方面:

通过省略set运算符,可以将属性表示为只读。因此,在大多数情况下,您不需要向属性添加readonly关键字:

public int Foo { get; }  // a readonly property

与此相反:字段需要使用readonly关键字来实现类似的效果:
public readonly int Foo; // a readonly field

因此,将字段标记为readonly的一个好处是可以实现与没有set操作符的属性相似的写保护级别 - 而无需将字段更改为属性(如果出于任何原因需要),从而使内容更加易于理解。请注意,保留html标签。

这两者之间的行为有区别吗? - petrosmm

1

不要忘记,有一种解决方法可以使用out参数在任何构造函数之外设置readonly字段。

虽然有点混乱,但是:

private readonly int _someNumber;
private readonly string _someText;

public MyClass(int someNumber) : this(data, null)
{ }

public MyClass(int someNumber, string someText)
{
    Initialise(out _someNumber, someNumber, out _someText, someText);
}

private void Initialise(out int _someNumber, int someNumber, out string _someText, string someText)
{
    //some logic
}

详细讨论请查看:http://www.adamjamesnaylor.com/2013/01/23/Setting-Readonly-Fields-From-Chained-Constructors.aspx

这里介绍了如何从构造函数链中设置只读字段的方法。

6
这句话的意思是:字段仍然在构造函数中被赋值,这是无法绕过的事实。无论这些值是来自单个表达式、分解的复杂类型,还是通过 out 的引用调用赋值,都不重要。 - user2864740
2
这甚至没有试图回答问题。 - Sheridan

0

readonly 标记的另一个有趣用法是保护单例中的字段不被初始化。

例如,在 csharpindepth 中的代码:

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy =
        new Lazy<Singleton>(() => new Singleton());

    public static Singleton Instance { get { return lazy.Value; } }

    private Singleton()
    {
    }
}

readonly 在保护 Singleton 字段不被初始化两次方面发挥了小作用。另一个细节是,对于上述情况,您不能使用 const,因为 const 强制在编译时创建,但是 singleton 在运行时创建。


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