C# 基础-CLR-类型【0】

 

  这篇文章之前发布在我的个人博客上了,http://www.vsmvp.com/Post.aspx?id=13

学习Jeffrey Richter的CLR via C#已经有两个月的时间,每天早上起床看一个小时,目前也就看到了14章。发现看到后面前面的内容基本上就差不多忘记了,所以打算写个类似读书笔记的东西,记下干货方便自己日后查阅。可能有人不知道CLR via C#这本书名是什么意思,根据我这几个月的理解,应该是通过C#了解CLR的意思

 
值类型和引用类型

        言归正传,CLR支持两种类型,值类型引用类型。下图是MSDN对值类型和引用类型的经典描述


需要注意的是,引用类型都是从托管堆上分配内存的,使用New关键词返回创建的内存地址。使用引用是,需要考虑一下事实:
  • 引用类型的内存必须从托管堆上分配
  • 堆上分配的每个对象都有一些额外的成员【额外成员包括一些同步索引块、对象指针等必须的东西】,这些成员必须初始化
  • 对象中的其他字节总是零
  • 从托管堆上分配一个对象实例时,可能强制执行一次垃圾回收
每次使用引用类型都会从托管堆上分配一些额外参数,还会导致GC的负担过重,这些肯定会影响程序的性能。CLR还提供一种轻型、高效的数据结构:值类型。值类型通常分配在线程栈上【如果值类型嵌入在引用类型里,则也分配在托管堆上】,变量的本身就包含实例的字段,所以值类型不需要指针,线程结束就会被随即释放,所以不会给GC造成压力。看个简单的例子:

public static void Go() {
SomeRef r1 = new SomeRef(); // Allocated in heap
SomeVal v1 = new SomeVal(); // Allocated on stack
r1.x = 5// Pointer dereference
v1.x = 5// Changed on stack
Console.WriteLine(r1.x); // Displays "5"
Console.WriteLine(v1.x); // Also displays "5"
// The left side of Figure 5-2 reflects the situation
// after the lines above have executed.
SomeRef r2 = r1; // Copies reference (pointer) only
SomeVal v2 = v1; // Allocate on stack & copies members
r1.x = 8// Changes r1.x and r2.x
v1.x = 9// Changes v1.x, not v2.x
Console.WriteLine(r1.x); // Displays "8"
Console.WriteLine(r2.x); // Displays "8"
Console.WriteLine(v1.x); // Displays "9"
Console.WriteLine(v2.x); // Displays "5"
// The right side of Figure 5-2 reflects the situation 
// after ALL the lines above have executed.
}
 
值类型那么牛逼是吧,那么是类型的适用条件是什么呢?通常情况下必须同时满足一下情况才可以将一个类型声明为值类型:
 
  • 类型不需要从其他类型中继承
  • 类型不需要被其他类型继承
  • 类型的实例字段不会给成员更改【建议将值类型的字段都设置为readonly】
满足以上三个条件的同时还必须满足一下任意一个条件:
  • 类型的实例比较小。16字节或这一下
  • 类型的实例比较大,但是不作为方法的实参传递,也不从方法返回。
值类型简单高效,但也有缺点or局限:
  • 值类型对象的两种表示形式:未装箱装箱。对值类型的装箱操作是非常消耗性能的。
            【引用类型总是处于装箱的形式,所以装箱针对值类型来说的,后面详细介绍装箱、拆箱
  • 值类型不能被其他类型(包括值类型和引用类型)继承。所以值类型里面不可以有新的虚方法,方法不能是抽象的,都必须是密闭的。
  • 值类型总是有默认值,不能为null
  • 将一个值类型的变量赋给另一个值类型的变量是,会执行一次逐个字段的赋值。而引用类型的变量赋给另一个引用类型的变量时,只复制内存地址
  • 基于上一条,值类型的变量值不会受到另一个值类型变量的影响。应用类型如果同时指向同一个托管堆,那么任何一个引用变量的改变都会影响其他的变量。
  • 线程栈上的值类型,实例不可达时便会给自动释放,所以不会收到Finalize的通知。
值类型的装箱和拆箱
 
前面说了,装箱和拆箱都是针对值类型而言的,因为引用类型本身就处于“装箱”状态。将一个值类型转换成引用类型,这个就是装箱操作 。
 
装箱的步骤:
  • 在托管堆中分配好内存。【这里分配的内存总理是值类型各个字段需要的内存量加上托管堆上对象都有的额外成员(类型对象指针和同步索引块)需要的内存量】
  • 值类型的字段复制到新分配的托管堆内存中
  • 返回现分配的托管堆上对象的地址。值类型现在是一个引用类型了。
装箱转换允许将 value-type 隐式转换为 reference-type。存在下列装箱转换:

        ·         从任何 value-type  object 类型。

        ·         从任何 value-type  System.ValueType 类型。

        ·         从任何 non-nullable-value-type  value-type 实现的任何 interface-type

        ·         从任何 nullable-type 到由 nullable-type 的基础类型实现的任何 interface-type

        ·         从任何 enum-type  System.Enum 类型。

        ·         从任何具有基础 enum-type  nullable-type  System.Enum 类型。

 
 
过程很复杂,C#编译器会自动生成IL去完成这些操作,只需要知道原理就OK了。装箱现在懂了,那么拆箱是什么个情况呢?大多数初学者都会想当然的认为拆箱是装箱的逆向,其实不然。拆箱操作其实只有一步,就是获取托管堆上要拆箱的对象的地址。紧接着将该对象的各个字段的值复制到线程栈的实例中。显然拆箱的代价比装箱低得多。
 

取消装箱转换允许将 reference-type 显式转换为 value-type。存在以下拆箱转换:

·          object 类型到任何 value-type

·          System.ValueType 类型到任何 value-type

·         从任何 interface-type 到实现了该 interface-type 的任何 non-nullable-value-type

·         从任何 interface-type 到其基础类型实现了该 interface-type 的任何 nullable-type

·          System.Enum 类型到任何 enum-type

·          System.Enum 类型到任何具有基础 enum-type  nullable-type

 
看了装箱拆箱的例子:
 
public static void Main6() {
Int32 v = 5
Object o = v; 
= 123
Console.WriteLine(v + ", " + (Int32)o); 
}
猜猜一共发生过几次装箱操作,答案是three!如果你可以详细的解释以上代码的装箱操作,那么你对装箱拆箱操作就很熟了。
posted @ 2011-11-01 09:37  BangQ  阅读(782)  评论(2编辑  收藏  举报