《C#本质论》 第9章 值类型

第9章 值类型

初学者主题:类型的分类
所有类型分为两个类别:引用类型和值类型。两者区别在于拷贝策略。

值类型

值类型的变量直接包含数据,换言之,变量名称直接和值的存储位置关联因此,将原始变量的值赋给另一个变量,会在新变量的位置创建原始变量值的内存拷贝。两个变量不可能引用同一个内存位置(除非其中一个或两个是out或ref参数,根据定义,这种参数是另一个变量的别名)。更改一个变量的值不会影响另一个变量。

设计规范

•避免创建消耗内存大于16字节的值类型。

•·避免创建消耗内存大于16字节的值类型。

•值类型的值一般只是短时间存在。通常作为表达式的一部分,或用于激活方法。在这些情况下,值类型的变量和临时值经常存储在称为栈的临时存储池中。

•临时池清理起来的代价低于需要垃圾回收的堆。不过,值类型要比引用类型更频繁地拷贝,会对性能造成一定影响。总之,不要觉得“值类型因为能在栈上分配所以更快”。

引用类型

相反,引用类型变量的值是对一个对象实例的引用。引用类型的变量存储的是引用(通常作为内存地址实现),要去那个位置找到对象实例的数据。因此,为了访问数据,“运行时”要从变量读取引用,进行“解引用”才能到达实际包含实例数据的内存位置。所以,引用类型的变量关联了两个存储位置:直接和变量关联的存储位置,以及由变量中存储的值引用的存储位置。

9.1 结构

除了string和object是引用类型,其他所有C#内建类型都是值类型。框架还提供了大量值类型,开发者甚至能定义自己的值类型。
定义自己的值类型使用和定义类及接口相似的语法,区别在于值类型使用关键字struct。

注意 虽然语言本身未作要求,但好的实践是使值类型不可变。换言之,值类型一旦实例化,就不能修改该实例。要修改应创建新实例。

设计规范
•要创建不可变的值类型(struct)。

9.1.1 初始化结构

除了属性和字段,结构还可包含方法和构造函数,但不可包含用户自定义的默认构造函数,相反,编译器自动生成默认构造函数的所有字段初始化为默认值。另外注意,不允许在结构声明中初始化字段。
如果不用new操作符来调用构造函数从而显式实例化结构,结构中的所有数据都隐式初始化为对应数据类型的默认值,但值类型中的所有数据都必须显式初始化来避免编译时错误。
设计规范

•要确保结构的默认值有效。总是可以获得结构的默认”全零“值。

9.1.2 使用default操作符

结构不能自定义默认构造函数,相反,编译器为所有值类型生成自动定义的默认构造函数。

9.1.3 值类型的继承和接口

设计规范

**•**要确保结构的默认值有效。封装并不能阻止访问默认的“全零”值。

高级主题:将new用于值类型

为引用类型使用new操作符,“运行时”会在托管堆上创建对象的新实例,将所有字段初始化为默认值,再调用构造函数,将对实例的引用以this的形式传递。new操作符最后返回对实例的引用,该引用被拷贝到和变量关联的内存位置。

相反,为值类型使用new操作符,“运行时”会在临时存储池中创建对象的新实例,将所有字段初始化为默认值,调用构造函数,将临时存储位置作为ref变量以this的形式传递。结果是值被存储到临时存储位置,然后可将该值拷贝到和变量关联的内存位置。

和类不同,结构不支持终结器。结构以值的形式拷贝,不像引用类型那样具有“引用同一性”,因此难以知道什么时候能安全执行终结器并释放结构占用的非托管资源。垃圾回收器知道在什么时候没有了对引用类型实例的“活动”引用,可在此之后的任何时间运行终结器。但“运行时”没有任何机制能跟踪值类型在特定时刻有多少个拷贝。

所有值类型都隐式密封,此外,除枚举之外的所有值类型都派生自System.ValueType。这意味着结构的继承链总是从object到System.ValueType到结构。
值类型也能实现接口。框架内建的许多值类型都实现了IComparableIForttable这样的接口
设计规范
如需比较相等性,要在值类型上重写相等性操作符(Equals(),==和!=)并考虑实现IEquatable接口。

9.2 装箱

我们知道值类型的变量直接包含它们的数据,而引用类型的变量包含对另一个存储位置的引用,但将值类型转换成它实现的某个接口或object会发生什么?结果必然是对一个存储位置的引用。该位置上包含引用类型的实例,但实际包含值类型的值,这种转换称为装箱 它具有一些特殊行为。从值类型的变量(直接引用其数据)转换为引用类型(引用堆上的一个位置)会涉及以下几个步骤。

1.首先在堆上分配内存。它将用于存放值类型的数据以及少许额外开销(SyncBlockIndex和方法表指针)。这些开销使对象看起来像引用类型的托管对象实例。

2.接着发生一次内存拷贝动作,当前存储位置的值类型数据拷贝到堆上分配好的位置。

3.最后,转换结果是对堆上的新存储位置的引用

相反的过程称为拆箱(unboxing)。具体是核实已装箱值的类型兼容于要拆箱成的值的类型,再拷贝堆中存储的值,结果是堆上存储的值的拷贝。

9.3 枚举

枚举是可由开发者声明的值类型,枚举的关键特征是在编译时声明一组具名常量值,这使代码易读。
**注意 ** 用枚举替代布尔值能改善可读性
设计规范
•考虑在现有枚举中添加新成员,但要注意兼容性风险。
•避免创建代表==“不完整值”==集合的枚举。
•避免在枚举中创建“保留给将来使用”的值。
•避免包含单个值的枚举
•要为简单枚举提供值0来代表无,注意若不显式初始化,0就是默认值。

9.3.1 枚举之间的类型兼容性

C#不支持不同枚举数组之间的直接转型,但CLR允许,前提是两个枚举具有相同的基础类型。

9.3.2 在枚举和字符串之间转换

枚举的好处是ToString()方法会输出枚举值标识符。

9.3.3 枚举作为标志使用

开发者许多时候不仅希望枚举值独一无二,还希望能对其进行组合以表示复合值。以System.IO.FileAttributes为例。如代码清单9.14所示,该枚举用于表示文件的各种特性:只读、隐藏、存档等。

一个好习惯是在标志枚举中包含值为0的None成员,因为无论是枚举类型的字段,还是枚举类型的一个数组中的元素,其初始默认值都是0。避免最后一个枚举值对应像Maximum(最大)这样的东西,因为Maximum可能被解释成有效枚举值。要检查枚举是否包含某个值,请使用System.Enum.IsDefined()方法。

设计规范

•要用FlagsAttribute标记包含标志的枚举。

•要为所有标志枚举提供等于0的None值。

•避免将标志枚举中的零值设定为“所有标志都未设置”之外的其他意思。

•考虑为常用标志组合提供特殊值。

•不要包含“哨兵”值(如Maximum),这种值会使用户困惑。

你可能感兴趣的:(C#本质论,c#,开发语言)