为什么Microsoft建议使用具有可变值的只读字段?

时间:2010-05-10 17:18:37

标签: c# immutability

Design Guidelines for Developing Class Libraries中,微软说:

  

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

     

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

这简单地重述了readonly的行为,但没有解释为什么使用readonly是不好的。其含义似乎是许多人不理解“只读”的含义,并错误地认为只读字段是不可改变的。实际上,它建议使用“readonly”作为代码文档,指示深层不变性 - 尽管编译器无法强制执行此操作 - 并且不允许将其用于其正常功能:确保字段的值在之后不会发生变化该物体已经建成。

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

首先,为什么Microsoft建议不要使用readonly来引用可变类型?我也有兴趣知道:

  • 您是否在所有代码中都遵循此设计指南?
  • 当你在一段你没写过的代码中看到“readonly”时,你有什么期望?

7 个答案:

答案 0 :(得分:25)

如果某个字段是只读的,您可能会发现无法更改该值或与其有关的任何内容。如果我知道Bar是Foo的只读字段,我显然不会说

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

但我可以说呀

foo.Bar.Name = "Blah";

如果对象支持Bar实际上是可变的。微软只是通过建议只读字段由不可变对象支持来推荐这种微妙的,违反直觉的行为。

答案 1 :(得分:21)

完全赞同你,我有时会在我的代码中使用readonly来获取可变引用类型。

作为一个例子:我可能有一些privateprotected成员 - 例如,List<T> - 我在一个类的方法中使用它的所有可变荣耀(调用{ {1}},Add等)。我可能只是想制定一个保护措施,以确保无论如何,我总是处理相同的对象。这可以保护我和其他开发人员不做一些愚蠢的事情:即将成员分配给新对象。

对我而言,这通常是使用具有私有Remove方法的属性的首选替代方法。为什么?因为set表示,实例化后无法更改该值,即使是基类

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

readonly

然后我仍然可以在我的基类的代码中的任意点设置protected List<T> InternalList { get; private set; } 。 (这对我来说需要一个非常愚蠢的错误,是的;但它仍然是可能的。)

另一方面,这个:

InternalList = new List<T>();

使明白无误 protected readonly List<T> _internalList; 只能引用一个特定对象(构造函数中设置了_internalList的对象)。

所以我就在你身边。一个人应该避免在可变引用类型上使用_internalList的想法对我个人来说是令人沮丧的,因为它基本上预先假定对readonly关键字的误解。

答案 2 :(得分:7)

  

不要将可变类型的实例分配到readonly个字段。

我快速浏览了框架设计指南一书(第161-162页),它基本上陈述了您自己已经注意到的内容。 Joe Duffy还有一个额外的评论解释了指南的存在理由:

  

本指南试图保护您的目的是相信您已经暴露了一个深层不可变的对象图,而事实上它很浅,然后编写假设整个图形是不可变的代码。 < br /> - Joe Duffy

我个人认为关键字readonly被命名为错误。事实上它只指定引用的常量,而不指定引用对象的常量,容易造成误导的情况。

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

为了解决这种不幸的情况,制定了指南。虽然我认为它的建议是从人类的观点来看的(但并不总是很明显哪些类型是可变的,哪些不是没有查找它们的定义,而且这个词暗示了深刻的不变性),有时希望,当声明常量时,C#会提供类似于C ++提供的自由,你可以在指针或指向对象上定义const,或者在无论是什么都没有。

答案 3 :(得分:2)

微软有一些特殊的建议。另一个立即想到的不是在公共成员中嵌套泛型类型,如List<List<int>>。我尽量避免使用这些结构,但在我觉得使用合理时,请忽略新手友好的建议。

至于readonly字段 - 我试图避免使用公共字段,而是去寻找属性。我认为对此也提出了一些建议,但更重要的是,当某个字段在某个属性的情况下不起作用时(有时与数据绑定和/或可视化设计器有关),有时会出现这种情况。通过创建所有公共字段属性,我可以避免任何潜在的问题。

答案 4 :(得分:1)

最后,它们只是指导方针。我知道微软的人通常不遵循所有指南。

答案 5 :(得分:1)

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; 
然而,它是烟雾和镜子。不可变性由编译器验证,而不是由CLR验证。另一种托管语言可以修改它们。这是问题的根源,CLR只是不支持它。

答案 6 :(得分:0)

这在C#(简单的控制台应用程序)中是合法的

readonly static object[] x = new object[2] { true, false };
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
        x[0] = false;
        x[1] = true;

        Console.WriteLine("{0} {1}", x[0], x[1]); //prints "false true"
        Console.ReadLine();
    }

那行得通。但这没有意义。请记住,变量x为只读变量,并且未更改(即,x的引用确实未更改)。但这不是我们说“只读x”时的意思,不是吗?因此,请勿使用具有可变值的只读字段。这令人困惑且违反直觉。