可能是受到Java的影响,我在转向C#后,一直没有注意区分过值类型和引用类型,不论是编写新代码还是重构旧代码时,如果发现有一些信息需要独立出来时,一直都是以新建类的方式进行,很少考虑使用结构体(我到目前为止,对结构体的理解还停留在C语言的层次)。
C#,或者说.NET,是区分值类型和引用类型的,这一点和C++以及Java都有区别。C++中传参都是以“传值”的方式进行,这种方式效率很高,但是会产生“对象切割”的问题,即在如果基类对象的地方,如果传递了一个派生类的实例,那么程序只会截取派生类实例中包含的基类信息,而忽略派生类自己新追加的信息,对于虚方法,也只会调用基类的方法;Java为了解决这个问题,将传参都做成了“按引用”的方式,这样造成了效率比较低。
我们在编写C#代码的过程中,应该在新建一个类型时,就要明确这个类型应该是值类型,还是引用类型。如果初期考虑不周全,到了后面再进行修改,很可能会造成问题。
区分值类型和引用类型的一个原则:值类型用于存储数据,而引用类型用于定义行为。
其实,对于单纯存储数据的结构,是采用值类型,还是存储类型,也可以从类的职责划分角度来看,无论是《敏捷软件开发》还是《设计模式》中,都对类的划分有说明。作为一个类,应该有其自身的职责,这些职责是通过向外提供一组接口的方式来实现的,对于单纯的存储数据的操作,例如ORM框架或者MVC模式中用于保存和DB表映射关系的Model,它本身没有行为,只是一堆数据,这种情况下,使用类来存储是否合适呢?
值类型是直接存储在堆栈上,而引用类型是存储在堆中,对于值类型来说,用户拿到的就是值类型本身,而对于引用类型来说,用户拿到的是指向引用类型的一个引用,这里,可以理解为指针。
我们可以看以下的代码。
首先,通过类的方式来定义一个结构
1 public class RefForStoreValue : ICloneable
2 {
3 private string m_strName;
4 public string Name
5 {
6 get { return m_strName; }
7 set { m_strName = value; }
8 }
9
10 private string m_strSex;
11 public string Sex
12 {
13 get { return m_strSex; }
14 set { m_strSex = value; }
15 }
16
17 private string m_strAddress;
18 public string Address
19 {
20 get { return m_strAddress; }
21 set { m_strAddress = value; }
22 }
23
24 public override string ToString()
25 {
26 return string.Format("Name:{0}, Sex:{1}, Address:{2}", this.Name, this.Sex, this.Address);
27 }
28
29 public object Clone()
30 {
31 return this.MemberwiseClone();
32 }
33 }
然后,通过结构体的方式,来定义相同的结构
1 public struct ValueForStoreValue
2 {
3 private string m_strName;
4 public string Name
5 {
6 get { return m_strName; }
7 set { m_strName = value; }
8 }
9
10 private string m_strSex;
11 public string Sex
12 {
13 get { return m_strSex; }
14 set { m_strSex = value; }
15 }
16
17 private string m_strAddress;
18 public string Address
19 {
20 get { return m_strAddress; }
21 set { m_strAddress = value; }
22 }
23
24 public override string ToString()
25 {
26 return string.Format("Name:{0}, Sex:{1}, Address:{2}", this.Name, this.Sex, this.Address);
27 }
28 }
接下来是很对上述代码的测试程序
1 private static void TestRefType()
2 {
3 Console.WriteLine();
4 Console.WriteLine("Test Ref Type");
5
6 RefForStoreValue refValue = new RefForStoreValue();
7 refValue.Name = "AAA";
8 refValue.Sex = "F";
9 refValue.Address = "Beijing China";
10 Console.WriteLine(refValue.ToString());
11
12 Console.WriteLine();
13
14 RefForStoreValue refValue2 = refValue;
15 RefForStoreValue refValue3 = refValue.Clone() as RefForStoreValue;
16 refValue2.Name = "XXX";
17 refValue2.Sex = "NA";
18 refValue2.Address = "Moon";
19 Console.WriteLine(refValue.ToString());
20 Console.WriteLine(refValue2.ToString());
21 Console.WriteLine(refValue3.ToString());
22
23 }
24
25 private static void TestValueType()
26 {
27 Console.WriteLine();
28 Console.WriteLine("Test Value Type");
29
30 ValueForStoreValue valueValue = new ValueForStoreValue();
31 valueValue.Name = "BBB";
32 valueValue.Sex = "M";
33 valueValue.Address = "Shanghai China";
34 Console.WriteLine(valueValue.ToString());
35
36 Console.WriteLine();
37
38 ValueForStoreValue valueValue2 = valueValue;
39 valueValue2.Name = "XXX";
40 valueValue2.Sex = "NA";
41 valueValue2.Address = "Moon";
42 Console.WriteLine(valueValue.ToString());
43 Console.WriteLine(valueValue2.ToString());
44 }
最后,执行结果如下所示
这样,应该可以看出值类型和引用类型的区别了吧。
另外一点需要说明,在声明某一个类型的数组时,在分配存储空间方面,值类型是一次完成所有数组元素的分配工作,而引用类型,第一次只是分配了指向这个数组的引用,至于数组中的每一个元素,都是null。
在创建一个类型时,如果以下问题的回答都是“是”,那么我们就可以将其定义为值类型:
- 该类型的主要职责是否用于数据存储?
- 该类型的公有接口是否完全由一些数据成员存取属性所定义?
- 是否确信该类型永远不可能有子类?
- 是否确信该类型永远都不可能具有多态行为?
如果对于上述问题的答案不太确定,那我们还是将其定义为引用类型吧。