为什么微软build议针对可变值的只读字段?

微软在“开发类库的devise指南”中说:

不要将可变types的实例分配给只读字段。

使用可变types创build的对象可以在创build后进行修改。 例如,数组和大多数集合是可变types,而Int32,Uri和String是不可变types。 对于包含可变引用types的字段,只读修饰符可以防止字段值被覆盖,但不保护可变types不被修改。

这只是简单地重申只读行为,而不解释为什么使用只读。 其含义似乎是,许多人不明白“只读”是什么,并错误地认为只读字段是不可变的。 实际上,build议使用“readonly”作为代码文档,指示深度不变性 – 尽pipe编译器无法强制执行此操作 – 并且不允许将其用于其正常function:确保字段的值不会在更改后该对象已被构build。

我对这个build议感到不自在,只能用“只读”来表示一些不同于编译器所理解的正常含义的东西。 我觉得它鼓励人们误解“只读”的意思,而且期望它意味着代码作者可能不想要的东西。 我觉得它排除了在可能有用的地方使用它 – 例如,为了显示两个可变对象之间的某些关系在其中一个对象的生命周期中保持不变。 假定读者不理解“只读”的含义的概念似乎也与微软的其他build议相矛盾,例如FxCop的“不要初始化不必要的”规则,该规则假设读者的代码是语言的专家并且应该知道(例如)bool字段会自动初始化为false,并阻止您提供显示为“是的,这已被有意识地设置为false;我不只是忘记初始化”的冗余。

所以,首先, 为什么微软build议不要使用readonly来引用可变types? 我也有兴趣知道:

  • 你在所有的代码中遵循这个devise指南吗?
  • 当你在一段你没有写的代码中看到“只读”时,你期望什么?

完全同意你的观点 ,而且我有时在我的代码中只使用可变引用types。

举个例子:我可能有一些private或者protected成员 – 比方说一个List<T> – 我在一个类的方法中使用了所有可变的荣耀(调用AddRemove等)。 我可能只想做一个保障措施,以确保无论如何, 我总是处理同一个对象 。 这可以保护我和其他开发人员免于做一些愚蠢的事情:即将成员分配给一个新的对象。

对我来说,这通常是使用私有set方法的属性的一个更好的select。 为什么? 因为readonly意味着实例化后的值不能被改变,即使是基类

换句话说,如果我有这个:

 protected List<T> InternalList { get; private set; } 

然后我仍然可以设置InternalList = new List<T>(); 在我的基类中的任何代码点。 (这是需要一个非常愚蠢的错误,是的,但仍然有可能。)

另一方面,这个:

 protected readonly List<T> _internalList; 

清楚地表明_internalList 只能引用一个特定的对象( _internalList在构造函数中设置的对象)。

所以我在你身边 对于我个人来说,不应该只使用可变引用types的想法是令人沮丧的,因为它基本上预示了对readonly关键字的误解。

如果一个字段是只读的,那么看起来很自然,你可能期望不能改变这个值或任何与它有关的东西 。 如果我知道Bar是Foo的只读字段,我显然不会说

 Foo foo = new Foo(); foo.Bar = new Baz(); 

但是我可以逃避

 foo.Bar.Name = "Blah"; 

如果对象后台栏实际上是可变的。 微软只是build议反对这种微妙的,违反直觉的行为,通过暗示只读字段由不可变对象支持。

不要将可变types的实例分配给readonly字段。

我在框架devise指南书(第161-162页)中快速浏览了一下,它基本上陈述了你自己已经注意到的东西。 乔·达菲还有一个额外的评论,解释了指南的存在理由:

这个指南试图保护你的是相信你已经暴露了一个深不可变的对象图,事实上它是浅的,然后编写假设整个图是不可变的代码。
– 乔·达菲

我个人认为readonly关键字命名不好。 事实上,它仅仅指定了引用的常量,而不是引用对象的常量,容易产生误导的情况。

我认为如果readonly引用的对象也是不可变的,而不仅仅是引用,因为这就是关键字所暗示的。

为了弥补这种不幸的情况,指南已经制定完成。 虽然我认为,从人类的angular度来看,它的build议是正确的(哪些types是可变的,哪些types不是没有查找它们的定义,并且这个词暗示着深刻的不变性),我有时希望,为了声明const,C#将提供一个类似于C ++所提供的自由,在那里你可以在指针,指向对象,或者两者都不定义const

微软有一些这样的奇特的build议。 另一个立即想到的是不要在公共成员中嵌套genericstypes,如List<List<int>> 。 我试图尽可能避免这些构造,但是当我觉得使用是合理的时候,忽略这个新手友好的build议。

至于只读字段 – 我尽量避免公开字段,而不是为了属性。 我认为也有一些build议,但更重要的是有些时候,当一个领域没有工作,而一个属性(主要是与数据绑定和/或视觉devise师)有关。 通过使所有的公共领域属性,我避免任何潜在的问题。

最后他们只是指导方针。 我知道,微软的人往往不遵循所有的指导方针。

C ++ / CLI语言支持您正在查找的语法:

 const Example^ const obj; 

第一个const使被引用的对象不可变,第二个使得引用不可变。 后者相当于C#中的readonly关键字。 试图逃避它会产生一个编译错误:

 Test^ t = gcnew Test(); t->obj = gcnew Example(); // Error C3892 t->obj->field = 42; // Error C3892 Example^ another = t->obj; // Error C2440 another->field = 42; 

然而,这是烟雾和镜子。 不变性由编译器validation,而不是由CLRvalidation。 另一种托pipe语言可以修改两者。 问题的根源在哪里,CLR就是不支持它。

如果你把一个可变types放入只读域,你就有一个根本的矛盾。 预计只读字段的内容将是不变的 。 所以,以下可能会导致不正确的使用:

 public Customer OrderCustomer { get; } 

没有setter,但Customer对象是可变的。 如果开发人员将该属性的只读属性解释为客户对象是不可变的,那么这个假设可能会导致问题。 代码将被编译并且系统可以运行,但是如果框架依赖于可变性,而其他代码则需要不变性,则可能会出现竞争条件,死锁和其他资源争用问题。