结构、枚举、装箱、拆箱
自定义值类型
如何利用结构来定义新的值类型,并使之具有与大多数预定义 类型相似的行为,这里的关键在于,任何
新定义的值类型都有它们自己的数据和方法。
一般用枚举来定义常量值集合。
1、值类型
所有值类型都派生自类System.ValueType。而所有类都派生自类System.Object。
值类型直接包含值。换言之,变量引用的位置就是值在内存中实际存储的位置。因此,
将第一个变量的值赋值给第二个变量,会在新变量的位置创建原始变量的值的一个内存副本。
所以改变第一个变量的值不会影响第二个变量的值,因为值类型会使用内存中的特定位置。
值类型需要的内存量会在编译时固定下来,不会在运行时改变。因为大小是固定的,所有值类型可以存储在称为
栈的内存区域中。
2、引用类型
引用类型也是一个变量,如上所述变量引用的位置就是值在内存中实际存储的位置,存储在栈中(但存储的数据是一个内存地址,大小是一个整形数据所占的字节数,而不是我们存储的实际数据),存储在这个变量中的内存地址号所指的内存,才是真正我们存储的实际数据)
,实际数据在堆中。这个内存地址号指向的是堆中的内存块。
运行时,从变量中读取内存位置,然后根据内存地址所指去堆中查找包含实际数据的内存。
所以在赋值给另一个变量时,复制的是一个内存地址号(是实际数据存储的内存地址,而不是实际数据,经过了一个间接跳转)。
在运行时,改变这个变量的数据而不是它的指向时(所谓改变指向就是重新指向一个新的地址),会改变实际数据。
当作为函数参数时,如果被声明为ref或out时,值类型和引用类型复制的统一是该变量的内存号,
所以如果是值类型,可以改变这个内存号中()的数据。也就实际数据(实参的数据)。
如果是引用类型,除了像以上可以改变实际数据以外,还可以改变传进来实参的指向,而且会取消对原来实际数据的指向(如果实际数据的指向只有此一个,这段内存将会在下一次垃圾回收时进行内存回收)。
注:内存号实际是一个整型数据(用来可以表示所有内存地址号的整型)32位,或者64位,一由处理器的位数决定。
1、结构(值类型)
注:虽然语言本身没有作要求,但作为一个良好的习惯,应该确保值类型是不可变的,换言之,一旦实例化好了一个值类型
,那个实例不能修改。如果需要修改,应该创建一个新的实例。
除了属性和字段,struct中还可以包含方法和构造器,但不能包含默认(无参数)的构造器。有的时候(比如在实例化一个数组的时候)
不会调用值类型的构造器,因为所有数组内存都转为用零来初始化。为了避免因为默认构造器只是偶尔调用而造成不一致,
C#完全禁止了用户显式定义默认构造器因为编译器会将声明时的实例字段赋值放到类型的构造器中进行,所以 C#相当于禁止了在声明时对实例字段进行赋值。
C#支持带参数的构造器。且需要在构造器中对所有字段进行初始化,否则会产生编译错误。
因为是值类型,所以没有终结器。垃圾回收只针对在堆中分配的内存。
default运算符的使用
default(数据类型)
如:default(int)
值类型都是密封的,所有值类型都派生自System.ValueType。这意味着struct的继承链问题从object到ValueType到struct。
值类型还能实现接口。
和类一样,可以在值类型中重写System.Object的virtual成员。
与引用类型重写的一个区别在于,对于值类型来说,GetHashCode()的默认实现昌将调用转发给struct的第一个非空字段。
此外,Equals()大量利用了反射机制。所以,假如一个值类型在集合中频繁使用,尤其是使用了码的字典类型的集合,那么值
类型应该同时包含对Equals()和GetHashCode()的重写。
1 struct Angle 2 { 3 4 public Angle(int hours, int minutes, int seconds) 5 { 6 _Hours = hours; 7 _Minutes = minutes; 8 _Seconds = seconds; 9 } 10 public int Hours 11 { 12 get 13 { 14 return _Hours; 15 } 16 } 17 private int _Hours; 18 public int Minutes 19 { 20 get 21 { 22 return _Minutes; 23 } 24 } 25 private int _Minutes; 26 public int Seconds 27 { 28 get 29 { 30 return _Seconds; 31 } 32 } 33 private int _Seconds; 34 public Angle Move(int hours, int minutes, int seconds) 35 { 36 return new Angle(Hours + hours, Minutes + minutes, Seconds + seconds); 37 } 38 } 39 40 class Coordinate 41 { 42 public Angle Latitude 43 { 44 get 45 { 46 return _Longitude; 47 } 48 set 49 { 50 _Longitude = value; 51 } 52 } 53 private Angle _Longitude; 54 55 public Angle Latitude 56 { 57 get 58 { 59 return _Latitude; 60 } 61 set 62 { 63 _Latitude = value; 64 } 65 } 66 private Angle _Latitude; 67 } 68
2、装箱
由于局部变量值类型(函数当中声明的变量)直接包含了它们的数据,而它们的接口和System.Object包含的是对它们的数据的引用。
因此必须认真考虑当一个值类型转成它实现的某个接口或者它的根基类object的时候,会发生什么。
这样的转型过程称为装箱。
从一个值类型转换成一个引用类型时,会涉及以下几个步骤。
步骤一:首先在堆中分配好内存,它将用于存放值类型的数据以及少许额外开销(一个SyncBlockIndex以及方法表指针)
步骤二:接着发生一次内存复制动作,栈上的值类型数据得到到堆上分配好的位置。
步骤三:最后,对象或接口引用得到更新,指向堆上的位置。
相反的过程称为拆箱。根据定义,CIL指令unbox只是对堆上的数据进行解引用(取消引用),并不包括从堆复制到栈的动作。
但在C#语言中,大多数时候都会紧接在拆箱之后发生一次复制动作。
装箱和拆箱会对性能和行为造成一些影响 。(应该多看CIL,在一个特定的代码片断中统计box/unbox指令的数量)
需要额外学习:待学习。 如何避免装箱和拆箱
3、枚举
枚举是开发者可以定义的一个类型。枚举的关键特征在于它标识了一个在编译时定义的所有可能值的集合,每个值都由一个名称来引用。
要引用一个枚举值,需要为其附加枚举名称前缀。
1 enum ConnectionState : short 2 { 3 Disconnected, 4 Connecting = 10, 5 Conntected, 6 Joined = Conntected, 7 Disconnecting 8 }
第一个枚举值默认为0,以后自动加1
枚举问题具有一个基础类型,这可能是int、uint、logn、ulong。但不能是char
默认值类型是Int.但可以使用继承语法来指定一个不同的类型。
枚举类型的性能完全取决于基础类型的性能。
注:在没有对应的枚举值的前提下也允许转型 ,优点在于枚举能在未来的API发布中添加新值,而且不会破坏早期的版本。
枚举和其他值类型稍有不同,因为枚举的继承链是从System.ValueType到System.Enum,再到enum。
3、1枚举之间的类型兼容性
C#不支持两个不同的枚举数组之间的直接转型,但是可以通过中间数据类型(数组)进行转换。
3、2 枚举和字符串之间的转换
输出枚举标识符。
3、3 枚举作为标志使用
可以使用移位来赋值。
或者 or 或者 and
或者 使用 FlagsAttribute: [Flags]放在枚举之前,这个标志指出多个枚举值组合使用。
枚举和结构仍需要强化学习。