CLR基础 - 基元类型、值类型和引用类型

编程语言的基元类型

  某些数据类型如此常用,以至于许多编辑器允许代码以简化语法来操纵它们。没简化之前的代码如下所示:

System.Int32 a = new System.Int32();

简化之后的代码如下:

int a =0;

编译器直接支持的数据类型称为 基元类型。基元类型直接映射到 Framework 类库( FCL)中存在的类型。表 5-1 列出了FCL 类型在 C# 中有的基元类型。只要符合公共语言规范(CLS)的类型,其他语言都提供了类似的基元类型。

CLR基础 - 基元类型、值类型和引用类型

 

checkedunchecked 

C#允许程序员自己决定如何处理溢出。溢出检查默认关闭。如果想让C# 编译器控制溢出的一个办法是使用 /checked+ 编译器开关。这样生成的代码会稍慢一些,因为CLR 会检查这些运算,判断是否发生溢出。如果发生溢出则抛出 OverflowException 异常。

除了全局性的打开和关闭溢出检查,程序员还可以在代码的特定区域控制溢出检查。如下所示使用 了checked 和 unchecked 操作符来提供这种灵活性。

UInt32 invalid = unchecked((UInt32) (-1));

 

引用类型和值类型

引用类型从托管堆中分配,C#的new 操作符返回对象内存地址—— 即指向对象数据的内存地址。使用引用类型注意以下4 点:

  • 内存必须从托管堆中分配

  • 堆上的每个对象都有一些额外成员,这些成员必须初始化。

  • 对象中的其他字节总是设为零。

  • 从托管堆分配对象时,可能强制执行一次垃圾回收。

 

为提升简单和常用的类型的性能,CLR 提供了名为“值类型”的轻量级类型。值类型的实例一般在线程栈上分配,(注意 也能作为字段嵌入到引用类型的对象中)。

值类型的实例变量中保存的就是实例本身的字段。值类型的实例不受垃圾回收的控制,缓解了托管堆的压力,并减少了应用程序生存期内的垃圾回收次数。

 

  • 任何称为 “类” 的类型都是引用类型。

  • 所有的值类型都称为 结构 或 枚举。

  • 所有结构都是抽象类型 System.ValueType 的直接派生类,System.ValueType 又是System.Object 派生。

  • 所有的枚举都是 System.Enum 抽象类型派生,System.Enum 又是System.ValueType派生。

  • 定义值类型时不能继承基类,只能实现接口。值类型都是隐士密封。

 

下面的例子说明值类型在定义时使用 new 和不适用new 的区别。

SomeVal v1; // SomeVal 是值类型,它在栈上分配,并且也会初始化为 0,但是在使用时会提示使用为赋值的字段。
SomeVal v1 = new SomeVal(); //SomeVal 中的字段会被初始化为 0.使用时也不会报错。

 

什么时候该用值类型而不是引用类型呢?

  • 类型具有基元类型的行为

  • 不需要从其他任何类型继承

  • 也不派生出其他任何类型

  • 类型的实例较小(16字节 或更小)

  • 类型的实例较大(大于 16 字节),但不作为方法实参传递,也不从方法返回。

 

值类型的局限性

  • 值类型对象有两种表示形式:未装箱和已装箱

  • 值类型的基类 System.ValueType ,重新实现了 Object 的 Equals 方法,在两个对象的字段值完全匹配的前提下返回 true。

  • 值类型中不能引入任何新的虚方法,所有方法不能是抽象的,所有方法都隐士密封。

  • 值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为0。

  • 未装箱的值类型不在堆上分配,一旦定义了该类型的一个实例方法不在活动,为它们分配的存储就会被释放。

 

值类型的装箱和拆箱

将值类型转换成引用类型要使用装箱机制,下面是值类型的实例进行装箱时发生的事情,看完后你一定不会让它发生在你的代码中。

  • 在托管堆中分配内存,分配的内存量是值类型各字段所需要的内存量,还有类型对象指针和同步块索引。

  • 值类型的字段复制到新分配的堆内存中。

  • 返回对象地址。

拆箱过程的代价要小得多,拆箱实际就是获取指针的过程,将堆中已装箱的对象的各个字段复制到基于栈的值类型实例中。

拆箱过程中常出现的问题,就是拆箱之后值类型原来是 int32 还是 int64,还是其他的什么。

CLR基础 - 基元类型、值类型和引用类型

  • 未装箱的值类型没有同步索引块,所以不能使用 System.Threading.Monitor 。

下面是难以觉察到发生装箱的情况。

  • 如果值类型中重写基类的虚方法,如System.ValueType 的 Equals,在这之中调用方法在基类中的实现,这时会产生装箱。this指针将对一个堆对象的引用传给基类方法。

  • 在调用非虚的、继承的方法时(比如 GetType),无论如何都会对值类型进行装箱。因为这些方法由System.Object 定义,要求this 实参指向堆对象的指针。

  • 将值类型的未装箱实例转型为类型的某个接口时要堆实例进行装箱。因为接口变量必须包含堆对象的引用。

 

C# 不允许对已经装箱的值类型中的字段进行更改。不过可以用接口欺骗C#。

看下面的列子:

CLR基础 - 基元类型、值类型和引用类型

CLR基础 - 基元类型、值类型和引用类型

 

CLR基础 - 基元类型、值类型和引用类型

 

对象相等性和一致性

Object 的 Equals 方法是像下面这样实现的:

CLR基础 - 基元类型、值类型和引用类型

Object 的Equals 方法的默认实现实际是同一性,而非相等性。

下面展示了 Equals 方法应该如何实现。

CLR基础 - 基元类型、值类型和引用类型

 

对于同一性检查调用 Object的静态方法 ReferenceEquals,而不能使用 C#的 == 操作符(除非把两个操作数都转为 Object)。ReferenceEquals原型如下:

CLR基础 - 基元类型、值类型和引用类型

 

System.ValueType 就重写了 Object 的Equals 方法,并进行了正确的实现来执行值的相等性检查。ValueType 的Equals 内部是这样实现的:

CLR基础 - 基元类型、值类型和引用类型

由于 CLR 反射机制慢,定义自己的值类型时应重写 Equals 方法来提供自己的实现,从而提高自己类型的实例进行值相等性比较的性能。自己的实现不调用 base.Equals。

自己实现时要注意:

CLR基础 - 基元类型、值类型和引用类型

 

 

 

上一篇:C#学习笔记(一)


下一篇:都说B站难爬?这也不难!