《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操作符返回对象内存地址——即指向对象数据的内存地址。

  1. 内存必须从托管堆分配。
  2. 堆上分配的每个对象都有一些额外成员,这些成员必须初始化。
  3. 对象中的其他字节(为字段而设)总是设为零。
  4. 从托管堆分配对象时,可能强制执行一次垃圾回收。

用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"

装箱和拆箱

将值类型转换为引用类型需要用装箱机制。其装箱的流程为:

  1. 在托管堆中分配内存。分配的内存量是值类型各个字段所需的内存量,还要加上两个额外成员(类型对象指针和同步索引块)。
  2. 值类型的字段复制到新分配的堆内存
  3. 返回对象地址。

将引用类型转换为值类型需要用拆箱机制。其拆箱的流程为:

  1. 如果包含“对已装箱值类型实例的引用”的变量为null,抛出NullReferenceException异常。
  2. 如果引用的对象不是所需值类型的已装箱实例,抛出InvalidCastException异常。
  3. 获取已装箱对象中的各个字段的地址。这部叫做拆箱(unboxing)。
  4. 将字段包含的值从堆复制到基于栈的值类型实例中。

拆箱的代价比装箱低很多。少了堆内存请求分配和字段复制。

注意
  • 值类型重写虚方法(例如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方法调用,因为编译器推断不了要使用的类型。