《CLR via C#》 基元类型、引用类型和值类型
Type基础
C#中有三种类型分别是基元类型、引用类型和值类型
基元类型
编辑器直接支持的数据类型称为基元类型(primitive type)。基元类型直接映射到Framework类库(FCL)中存在的类型。
C#基元类型 | FCL类型 | 符合公共语言规范(CLS) | 说明 |
---|---|---|---|
sbyte | System.SByte | 否 | 有符号8位 |
byte | System.Byte | 是 | 无符号8位 |
short | System.Int16 | 是 | 有符号16位 |
ushort | System.UInt16 | 否 | 无符号16位 |
int | System.Int32 | 是 | 有符号32位 |
uint | System.UInt32 | 否 | 无符号32位 |
long | System.Int64 | 是 | 有符号64位 |
ulong | System.UInt64 | 否 | 无符号64位 |
char | System.Char | 是 | 16位Unicode字符 |
float | System.Single | 是 | IEEE 32位浮点值 |
double | System.Double | 是 | IEEE 64位浮点值 |
bool | System.Boolean | 是 | true/false |
decimal | System.Decimal | 是 | 128位高精度浮点值,常用于不容许误差的金融计算 |
string | System.String | 是 | 字符数组 |
object | System.Object | 是 | 所有类型的基类 |
dynamic | System.Object | 是 | 对于CLR,dynamic和object完全一致。但C#编译器允许使用简单的语法让dynamic变量参与动态调度 |
C#编译器非常熟悉基元类型,会在编译代码的时候引用自己的规则。
- 编译器能执行基元类型之间的隐式或显示转型。但是只有在数据不会发生丢失的时候,C#才允许隐式转换。
1Int32 i = 5; // 整数字面值默认解释为Int32 2Int64 l = i; // Int32隐式转为Int64 3Single s = i; // Int32隐式转为Single 4Byte b = (Byte) i; // Int32显示转为Byte 5Int16 v = (Int16) s; // Single显示转为Int16
- 浮点值转证书C#编译器总是对浮点值进行接端,不向上取整。
- 基元类型也可写成字面值(literal)。字面值被看成是类型本身的实例。
1123.ToString();
- 表达式由字面值构成,编译器则可在编译期间就完成表达式求值。
1Boolean found = false; // 生成的代码将found设为0 2Int32 x = 100 + 20 + 3; // 生成的代码将x设为123 3String s = "a " + "bc"; // 生成的代码将s设为"a bc"
checked和unchecked基元类型操作
C#允许程序员自己决定是否开启处理溢出检查。溢出检查默认关闭。可以通过/checked+编译器开关来全局替换是否生成检查版本的指令。如果想在特定代码区域控制溢出检查,则可用checked和unchecked操作符或语句
1Byte b = 100;
2b = checked((Byte) (b + 200)); // 抛出OverflowException异常
3
4checked { // 开始checked块
5 Byte b = 100;
6 b = (Byte) (b + 200); // 抛出OverflowException异常
7} // 结束checked块
引用类型
引用类型总是从托管堆分配,C#的new操作符返回对象内存地址——即指向对象数据的内存地址。
- 内存必须从托管堆分配。
- 堆上分配的每个对象都有一些额外成员,这些成员必须初始化。
- 对象中的其他字节(为字段而设)总是设为零。
- 从托管堆分配对象时,可能强制执行一次垃圾回收。
用class声明的都是引用类型
值类型
值类型的实例一般在线程栈上分配(也可作为字段嵌入引用类型的对象中)。值类型实例在线程栈中,所以不受垃圾回收器控制。
所有结构都是抽象类型System.ValueType的直接派生类。而后者从System.Object派生。所有枚举都从System.Enum派生,而后者从System.ValueType派生。所有值类型都隐式密封,防止将值类型用作其他引用类型或值类型的基类型。
用struct声明的都是值类型
1struct vector2 { int x, y; }
2vector2 v1 = new vector2(); // 在线程栈中分配一个vector2的实例,并确保其中所有字段被初始化为零
3vector2 v2; // 跟上方一样,但C#不认为所有字段被初始化为零
4int a = v1.x; // 无问题
5int b = v2.x; // 报错 error CS0170: 使用了可能未赋值的字段"x"
装箱和拆箱
将值类型转换为引用类型需要用装箱机制。其装箱的流程为:
- 在托管堆中分配内存。分配的内存量是值类型各个字段所需的内存量,还要加上两个额外成员(类型对象指针和同步索引块)。
- 值类型的字段复制到新分配的堆内存
- 返回对象地址。
将引用类型转换为值类型需要用拆箱机制。其拆箱的流程为:
- 如果包含“对已装箱值类型实例的引用”的变量为null,抛出NullReferenceException异常。
- 如果引用的对象不是所需值类型的已装箱实例,抛出InvalidCastException异常。
- 获取已装箱对象中的各个字段的地址。这部叫做拆箱(unboxing)。
- 将字段包含的值从堆复制到基于栈的值类型实例中。
拆箱的代价比装箱低很多。少了堆内存请求分配和字段复制。
注意
- 值类型重写虚方法(例如Equals, GetHashCode或ToString)如果在其中调用的基类型的实现,则会在调用时进行装箱。以便能够通过this指针将对一个堆对象的引用传给基方法。
- 调用非虚的、继承的方法时(如GetType或MemberwiseClone),无论如何都有对类型进行装箱。因为这些方法由System.Object定义,要求this实参是指向对对象的指针。
- 将值类型的未装箱实例转换为类型的某个接口时要对实例进行装箱。这是因为接口变量必须包含对堆对象的引用。
- C#一般无法修改已装箱值类型中的字段。有一种例外就是利用接口来修改。(尽量还是不要使用,破坏C#值类型的不可变"immutable"性)
1interface IChange { 2 void Change(int x, int y); 3} 4 5struct Point : IChange { 6 public int x, y; 7 public void Change(int x, int y) { 8 this.x = x; 9 this.y = y; 10 } 11} 12 13public static void Mian() { 14 Point p = new Point(); 15 p.x = 1; 16 p.y = 1; 17 18 // 一次装箱 19 Object o = p; 20 21 // 一次拆箱。Change修改的是拆箱后的临时值类型。o内部的值类型还是(1,1) 22 ((Point) o).Change(2, 2); 23 24 // 一次装箱。Change修改的是装箱后临时的引用类型。然后丢弃它。不影响p。 25 ((IChange) p).Change(3, 3) 26 27 // o内部的值类型字段x和y被修改为(4,4) 28 ((IChange) o).Chage(4, 4); 29}
值类型和引用类型的区别
- 值类型对象有两种表示形式:未装箱和已装箱。而引用类型总是已装箱。
- 由于不能将值类型作为基类型来定义新的值类型或引用类型,所以不应该在值类型中引入任何虚方法。所有的方法也不能时抽象的,所有的方法都隐式密封(不可重写)。
- 引用类型的变量包含堆中对象的地址。引用类型的变量创建时默认初始化为null。而值类型的变量总是包含其基础类型的一个值,而值类型的所有成员变量都初始化为0。
- 将值类型变量赋值给另一个值类型变量会逐字段复制。而引用类型只会复制内存地址。
C#模拟Union
特性[StructLayout(LayoutKind.Explicit)]
用于告知编辑器此struct显示指定每个字段的偏移量。
特性FieldOffset(Int32 val)
用于指出字段距离实例启始出的偏移量(字节为单位)
注意:引用类型与值类型互相重叠是不合法的。
1// 此struct中m_b与m_x互相重叠
2[StructLayout(LayoutKind.Explicit)]
3struct ValType {
4 [FieldOffset(0)]
5 private readonly Byte m_b;
6 [FieldOffset(0)]
7 private readonly Int16 m_x;
8}
Equals函数的同一性(identity)或相等性(equality)判断
同一性指两个引用是否指向同一个对象,最好调用’ReferenceEquals’来判断,或先把两个操作数转为Object在调用==操作符。
相等性指两个实际的值是否相同,或引用的对象的值是否相同。
dynamic基元类型
代码使用dynamic表达式或变量调用成员时,编译器生成特殊IL代码来描述所需的操作。这种特殊的代码称为payload(有效载荷)。在运行时,payload代码根据dynamic表达式或变量引用的对象的实际来决定具体执行的操作。
如果字段、方法参数或方法返回值的类型是dynamic,编译器会将该类型装欢为System.Object,并在元数据中向字段、参数或返回类型应用System.Runtime.CompilerServices.DynamicAttribute的实例。如果局部变量被指定为dynamic,则变量类型也会成为Object,但不会向局部变量应用DynamicAttribute,因为它限制在方法内部受用。dynamic其实就是Object。
值类型赋值给dynamic需要装箱,因为dynamic本质就是Object。
1dynamic d = 123; // Int32隐式转换到dynamic(装箱)
2Int32 i = d; // dynamic隐式转换到Int32(拆箱)
不能定义对dynamic进行扩展的扩展方法。不能将lambda表达式或匿名方法作为实参传给dynamic方法调用,因为编译器推断不了要使用的类型。