C引用类型.docx

上传人:b****2 文档编号:24396684 上传时间:2023-05-27 格式:DOCX 页数:13 大小:20.72KB
下载 相关 举报
C引用类型.docx_第1页
第1页 / 共13页
C引用类型.docx_第2页
第2页 / 共13页
C引用类型.docx_第3页
第3页 / 共13页
C引用类型.docx_第4页
第4页 / 共13页
C引用类型.docx_第5页
第5页 / 共13页
点击查看更多>>
下载资源
资源描述

C引用类型.docx

《C引用类型.docx》由会员分享,可在线阅读,更多相关《C引用类型.docx(13页珍藏版)》请在冰豆网上搜索。

C引用类型.docx

C引用类型

C#引用类型详细剖析

本文介绍了C#引用类型和值类型的区别的第二部分,包括值类型和引用类型在内存中的部署、正确使用值类型和引用类型等。

C#引用类型和值类型的区别——值类型和引用类型在内存中的部署

经常听说,并且经常在书上看到:

值类型部署在栈上,引用类型部署在托管堆上。

实际上并没有这么简单。

MSDN上说:

托管堆上部署了所有引用类型。

这很容易理解。

当创建一个应用类型变量时:

1.object reference = new object(); 

关键字new将在托管堆上分配内存空间,并返回一个该内存空间的地址。

左边的reference位于栈上,是一个引用,存储着一个内存地址;而这个地址指向的内存(位于托管堆)里存储着其内容(一个System.Object的实例)。

下面为了方便,简称引用类型部署在托管推上。

再来看值类型。

《C#语言规范》上的措辞是“结构体不要求在堆上分配内存(However,unlikeclasses,structsarevaluetypesanddonotrequireheapallocation)”而不是“结构体在栈上分配内存”。

这不免容易让人感到困惑:

值类型究竟部署在什么地方?

数组

考虑数组:

2.int[] reference = new int[100]; 

根据定义,数组都是引用类型,所以int数组当然是引用类型(即reference.GetType().IsValueType为false)。

而int数组的元素都是int,根据定义,int是值类型(即reference[i].GetType().IsValueType为true)。

那么引用类型数组中的值类型元素究竟位于栈还是堆?

如果用WinDbg去看reference[i]在内存中的具体位置,就会发现它们并不在栈上,而是在托管堆上。

实际上,对于数组:

3.TestType[] testTypes = new TestType[100]; 

如果TestType是值类型,则会一次在托管堆上为100个值类型的元素分配存储空间,并自动初始化这100个元素,将这100个元素存储到这块内存里。

如果TestType是引用类型,则会先在托管堆为testTypes分配一次空间,并且这时不会自动初始化任何元素(即testTypes[i]均为null)。

等到以后有代码初始化某个元素的时候,这个引用类型元素的存储空间才会被分配在托管堆上。

类型嵌套

更容易让人困惑的是引用类型包含值类型,以及值类型包含引用类型的情况:

4.public class ReferenceTypeClass 

5.{ 

6.    private int _valueTypeField; 

7.    public ReferenceTypeClass() 

8.     { 

9.         _valueTypeField = 0; 

10.     } 

11.    public void Method() 

12.     { 

13.        int valueTypeLocalVariable = 0; 

14.     } 

15.} 

16.ReferenceTypeClass referenceTypeClassInstance = new ReferenceTypeClass();//Where is _valueTypeField?

 

17.referenceTypeClassInstance.Method();//Where is valueTypeLocalVariable?

 

18. 

19.public struct ValueTypeStruct 

20.{ 

21.    private object _referenceTypeField; 

22.    public void Method() 

23.     { 

24.         _referenceTypeField = new object(); 

25.        object referenceTypeLocalVariable = new object(); 

26.     } 

27.} 

28.ValueTypeStruct valueTypeStructInstance = new ValueTypeStruct(); 

29.valueTypeStructInstance.Method();//Where is _referenceTypeField?

And where is referenceTypeLocalVariable?

 

单看valueTypeStructInstance,这是一个结构体实例,感觉似乎是整块扔到栈上的。

但是字段_referenceTypeField是引用类型,局部变量referenceTypeLocalVarible也是引用类型。

referenceTypeClassInstance也有同样的问题,referenceTypeClassInstance本身是引用类型,似乎应该整块部署在托管堆上。

但字段_valueTypeField是值类型,局部变量valueTypeLocalVariable也是值类型,它们究竟是在栈上还是在托管堆上?

规律是:

引用类型部署在托管堆上;值类型总是分配在它声明的地方:

作为字段时,跟随其所属的变量(实例)存储;作为局部变量时,存储在栈上。

我们来分析一下上面的代码。

对于引用类型实例,即referenceTypeClassInstance:

从上下文看,referenceTypeClassInstance是一个局部变量,所以部署在托管堆上,并被栈上的一个引用所持有;值类型字段_valueTypeField属于引用类型实例referenceTypeClassInstance的一部分,所以跟随引用类型实例referenceTypeClassInstance部署在托管堆上(有点类似于数组的情形);

valueTypeLocalVariable是值类型局部变量,所以部署在栈上。

而对于值类型实例,即valueTypeStruct:

根据上下文,值类型实例valueTypeStructInstance本身是一个局部变量而不是字段,所以位于栈上;其引用类型字段_referenceTypeField不存在跟随的问题,必然部署在托管堆上,并被一个引用所持有(该引用是valueTypeStruct的一部分,位于栈);其引用类型局部变量referenceTypeLocalVariable显然部署在托管堆上,并被一个位于栈的引用所持有。

所以,简单地说“值类型存储在栈上,引用类型存储在托管堆上”是不对的。

必须具体情况具体分析。

C#引用类型和值类型的区别——正确使用值类型和引用类型

这一部分主要参考《EffectiveC#》,并非本人原创,希望能让你加深对值类型和引用类型的理解。

辨明值类型和引用类型的使用场合C#中,我们用struct/class来声明一个类型为值类型/引用类型。

考虑下面的例子:

30.TestType[] testTypes = new TestType[100]; 

如果TestTye是值类型,则只需要一次分配,大小为TestTye的100倍。

而如果TestTye是引用类型,刚开始需要100次分配,分配后数组的各元素值为null,然后再初始化100个元素,结果总共需要进行101次分配。

这将消耗更多的时间,造成更多的内存碎片。

所以,如果类型的职责主要是存储数据,值类型比较合适。

一般来说,值类型(不支持多态)适合存储供C#应用程序操作的数据,而引用类型(支持多态)应该用于定义应用程序的行为。

通常我们创建的引用类型总是多于值类型。

如果以下问题的回答都为yes,那么我们就应该创建为值类型:

该类型的主要职责是否用于数据存储?

该类型的共有借口是否完全由一些数据成员存取属性定义?

是否确信该类型永远不可能有子类?

是否确信该类型永远不可能具有多态行为?

将值类型尽可能实现为具有常量性和原子性的类型

具有常量性的类型很简单:

如果构造的时候验证了参数的有效性,之后就一直有效;省去了许多错误检查,因为禁止更改;确保线程安全,因为多个reader访问到同样的内容;可以安全地暴露给外界,因为调用者不能更改对象的内部状态。

具有原子性的类型都是单一的实体,我们通常会直接替换一个原子类型的整个内容。

下面是一个典型的可变类型:

31.public struct Address 

32.{ 

33.    private string _city; 

34.    private string _province; 

35.    private int _zipCode; 

36.    public string City 

37.     { 

38.        get { return _city; } 

39.        set { _city = value; } 

40.     } 

41.    public string Province 

42.     { 

43.        get { return _province; } 

44.        set 

45.         { 

46.             ValidateProvince(value); 

47.             _province = value; 

48.         } 

49.     } 

50.    public int ZipCode 

51.     { 

52.        get { return _zipCode; } 

53.        set 

54.         { 

55.             ValidateZipCode(value); 

56.             _zipCode = value; 

57.         } 

58.     } 

59.} 

下面创建一个实例:

60.Address address = new Address(); 

61.address.City = "Chengdu"; 

62.address.Province = "Sichuan"; 

63.address.ZipCode = 610000; 

然后更改这个实例:

64.address.City = "Nanjing"; //Now Province and ZipCode are invalid 

65.address.ZipCode = 210000; //Now Province is still invalid 

66.address.Province = "Jiangsu"; 

可见,内部状态的改变意味着可能违反对象的不变式(invariant),至少是临时的违反。

如果上面是一个多线程的程序,那么在City更改的过程中,另一个线程可能看到不一致的数据视图。

如果不是多线程的程序,也有问题:

当ZipCode的值无效而抛出异常时,对象仅作了一部分改变,因此处于无效的状态,为了修复这个问题,需要在Address中添加相当多的内部校验代码;

为了实现异常安全,我们需要在所有改变多个字段的客户代码处放上防御性的代码;

线程安全也要求我们在每一个属性的访问器上添加线程同步检查。

显然,这是一个相当可观的工作量。

下面我们把Address实现为常量类型:

67.public struct Address 

68.{ 

69.    private string _city; 

70.    private string _province; 

71.    private int _zipCode; 

72.    public Address (string city, string province, int zipCode) 

73.     { 

74.         _city = city; 

75.         _province = province; 

76.         _zipCode = zipCode; 

77.         ValidateProvince(province); 

78.         ValidateZipCode(zipCode); 

79.     } 

80.    public string City 

81.     { 

82.        get { return _city; } 

83.     } 

84.    public string Province 

85.     { 

86.        get { return _province; } 

87.     } 

88.    public int ZipCode 

89.     { 

90.        get { return _zipCode; } 

91.     } 

92.} 

如果要改变Address,不能修改现有的实例,只能创建一个新的实例:

93.Address address = new Address("Chengdu", "Sichuan", 610000);//create a instance 

94.address = new Address("Nanjing", "Jiangsu", 210000);//modify the instance 

address将不存在任何无效的临时状态。

那些临时状态只存在于Address的构造函数执行过程中。

这样一来,Address是异常安全的,也是线程安全的。

确保0为值类型的有效状态

.NET的默认初始化机制会将引用类型设置为二进制意义上的0,即null。

而对于值类型,不论我们是否提供构造函数,都会有一个默认的构造函数,将其设置为0。

一种典型的情况是枚举:

95.public enum Sex 

96.{ 

97.     Male = 1; 

98.     Female = 2; 

99.} 

100. 

然后用做值类型的成员:

101.public struct Employee 

102.{ 

103.    private Sex _sex; 

104.    //other 

105.} 

106. 

创建Employee结构体将得到一个无效的Sex字段:

107.Employee employee = new Employee (); 

employee的_sex是无效的,因为其为0。

我们应该将0作为一个为初始化的值明确表示出来:

108.public Sex 

109.{ 

110.     None = 0; 

111.     Male = 1; 

112.     Female = 2; 

113.} 

如果值类型中包含引用类型,会出现另一种初始化问题:

114.public struct ErrorLog 

115.{ 

116.    private string _message; 

117.    //other 

118.} 

119. 

然后创建一个ErrorLog:

120.ErrorLog errorLog = new ErrorLog (); 

errorLog的_message字段将是一个空引用。

我们应该通过一个属性来将_message暴露给客户代码,从而使该问题限定在ErrorLog的内部:

121.public struct ErrorLog 

122.{ 

123.    private string _message; 

124.    public string Message 

125.     { 

126.        get 

127.         { 

128.            return (_message !

 = null) ?

 _message :

 string.Empty; 

129.         } 

130.        set { _message = value; } 

131.     } 

132.    //other 

133.} 

134. 

尽量减少装箱和拆箱

装箱指把一个值类型放入一个未具名类型的引用类型中,比如:

135.int valueType = 0; 

136.object referenceType = i;//boxing 

拆箱则是从前面的装箱对象中取出值类型:

137.object referenceType; 

138.int valueType = (int)referenceType;//unboxing 

装箱和拆箱是比较耗费性能的,还会引入一些诡异的bug,我们应当避免装箱和拆箱。

装箱和拆箱最大的问题是会自动发生。

比如:

139.Console.WriteLine("A few numbers:

 {0}, {1}.", 25, 32); 

其中,Console.WriteLine()接收的参数类型是(string,object,object)。

因此,实际上会执行以下操作:

140.int i = 25; 

141.obeject o = i;//boxing 

然后把o传给WriteLine()方法。

在WriteLine()方法的内部,为了调用i上的ToString()方法,又会执行:

142.int i = (int)o;//unboxing 

143.string output = i,ToString(); 

所以正确的做法应该是:

144.Console.WriteLine("A few numbers:

 {0}, {1}.", 25.ToString(), 32.ToString()); 

25.ToString()只是执行一个方法并返回一个引用类型,不存在装箱/拆箱的问题。

另一个典型的例子是ArryList的使用:

145.public struct Employee 

146.{ 

147.    private string _name; 

148.    public Employee(string name) 

149.     { 

150.         _name = name; 

151.     } 

152.    public string Name 

153.     { 

154.        get { return _name; } 

155.        set { _name = value; } 

156.     } 

157.    public override string ToString() 

158.     { 

159.        return _name; 

160.     } 

161.} 

162.ArrayList employees = new ArrayList(); 

163.employees.Add(new Employee("Old Name"));//boxing 

164.Employee ceo = (Employee)employees[0];//unboxing 

165.ceo.Name = "New Name";//employees[0].ToString() is still "Old Name" 

166. 

上面的代码不仅存在性能的问题,还容易导致错误发生。

在这种情况下,更好的做法是使用泛型集合:

167.List< Employee> employees = new List< Employee>(); 

由于List是强类型的集合,employees.Add()方法不进行类型转换,所以不存在装箱/拆箱的问题。

C#引用类型和值类型的区别——总结

C#中,变量是值还是引用仅取决于其数据类型。

C#的值类型包括:

结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型。

C#的引用类型包括:

数组,用户定义的类、接口、委托,object,字符串。

数组的元素,不管是引用类型还是值类型,都存储在托管堆上。

引用类型在栈中存储一个引用,其实际的存储位置位于托管堆。

为了方便,本文简称引用类型部署在托管推上。

值类型总是分配在它声明的地方:

作为字段时,跟随其所属的变量(实例)存储;作为局部变量时,存储在栈上。

值类型在内存管理方面具有更好的效率,并且不支持多态,适合用作存储数据的载体;引用类型支持多态,适合用于定义应用程序的行为。

应该尽可能地将值类型实现为具有常量性和原子性的类型。

应该尽可能地确保0为值类型的有效状态。

应该尽可能地减少装箱和拆箱。

关键字new将在托管堆上分配内存空间,并返回一个该内存空间的地址。

左边的reference位于栈上,是一个引用,存储着一个内存地址;而这个地址指向的内存(位于托管堆)里存储着其内容(一个System.Object的实例)。

 

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 求职职场 > 社交礼仪

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1