C#值类型和结构类型
条款讨论的是类型设计时候的tradeoff——是将类型设计为结构还是类。Bill Wagner先生给出了一个原则“C#值类型用于存储数据,引用类型用于定义行为(value types store values and reference types define behavior)”。
如何判断这个原则的适用性,Bill Wagner也给出了一个方法,那就是首先回答下面几个问题:
1.该类型的主要职责是否用于数据存储?
2.该类型的公有接口是否都是一些存取属性?
3.是否确信该类型永远不可能有子类?
4.是否确信该类型永远不可能具有多态行为?
如果所有问题的答案都是yes,那么就应该采用C#值类型。这样的判断确实有很好的理由支撑,但是我个人认为“将这4个问题回答为yes”还不足以构成采 用C#值类型的全部理由。因为在很多项目实践中,我发现C#值类型带来的性能问题不可小视。C#值类型带来的性能问题主要有两个:
1.由于C#值类型实例在栈和托管堆之间的转换而导致的box/unbox,以及由此带来的托管堆上的垃圾。
2.C#值类型默认情况下采用的是值拷贝语义,如果是比较大的C#值类型,在传递参数和函数返回值时,同样会带来性能问题。
关于第1条,Bill Wagner在本条款中提到了“引用类型会给垃圾收集器带来负担”这个表面看似正确的判断。但是由于box/unbox的效应,有些情况下,反倒是C#值 类型给垃圾收集器带来了更多的负担。比如将一些C#值类型放到一个集合中,然后又频繁地对其进行读写操作。如果碰到这种情况,我想“放弃结构而采用类”未 尝不是一种更好的做法。事实上,将一个用作数据存储的C#值类型(比如System.Drawing.Point)添加到一个集合 (System.Collections.ArrayList)中是一个太常见不过的操作。不过,C# 2.0中新引入的泛型技术对box/unbox的问题有极大的改善。
关于第2条,Scott Meyers先生在Effective C++的第22条“尽量使用pass-by-reference(传址),少用pass-by-value(传值)”中讲的比较清楚。虽然由于C# C#结构类型具有默认的深拷贝语义,没有拷贝构造器的调用。而且C#结构类型也没有子类,因此在某种程度上来讲不具有多态性,也就没有C++对象传值时可 能出现的切割(slicing)效应。但是值拷贝的成本仍然不小。尤其是在这个C#值类型比较大的情况下,问题就比较严重。实际上,在.NET框架的 Design Guidelines for Class Library Developers文档中,在说明什么时候应该使用C#结构类型的时候,其中提到了一项原则(还有其他一些并行原则)——类型实例数据的大小要小于16 个字节。该文档主要是从类型的运行效率层面来考虑的,而Bill Wagner先生这里的条款主要是从类型的设计层面来考虑的。
从上述两条讨论来看,我个人倾向于对C#结构类型采取更为保守的设计策略。而对于类则可以积极大胆地使用。因为“将C#结构类型不适当地设计为类”带来的 不良后果要远远小于“将类不适当地设计为C#结构类型”所带来的不良后果。就目前的经验来看,我甚至认为只有和非托管互操作打交道的情况才是使用C#结构 类型最充足的理由,其他情况都要“三思而后行”。当然,在C# 2.0中引入泛型技术之后,box/unbox将不再是一个沉重的负担,应付一些非常轻量级的场合,C#结构类型依然有自己的一席之地。