阅读《CLR via C#》总结(第四天)

ch5——基元类型、引用类型和值类型

基元类型:编译器直接支持的数据类型称为基元类型。

FCL类型:Framework类库中存在的类型。

C#基元类型与对应的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字符(char不像在非托管c++中那样代表一个8位值)
float System.Single IEEE32位浮点值
double System.Double IEEE64位浮点值
bool System.Boolean true/false值
decimal System.Decimal 128位高精度浮点值,常用于不容许舍入误差的金融计算。128位中,1位是符号,96位是值本身(N),8位是比例因子(k).decimal实际值是±N*10^k,其中-28<=k<=0。其余位没有使用。
string System.String 字符数组
object System.Object 所有类型的基类型
dynamic System.Object 对于CLR,dynamic和object完全一致。但C#编译器允许使用简单的语法让dynamic变量参与动态调度。

引用类型和值类型 

 CLR支持两种类型:引用类型和值类型。引用类型总是从托管堆分配,C#的new操作符返回对象内存地址——即指向对象数据的内存地址。

使用引用类型必须注意以下四点:

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

值类型一般在线程栈上分配(虽然也可作为字段嵌入引用类型的对象中)。 在代表值类型实例的变量中不包含指向实例的指针,相反,变量中包含了实例本身的字段。值类型实例不受垃圾回收器的控制。

所有值类型从System.ValueType中派生,ValueType本身又从System.Object中派生。所有值类型都隐式密封,防止将值类型用作其他引用类型或值类型的基类型。

根据前一章的叙述可理解引用类型对象在堆上开辟空间,其包含类型对象指针和同步索引块以及一些自身的实例字段,其引用的地址会存储在栈上,值类型对象会在栈上开辟空间,那么当我们对一个引用类型做了复制操作(即类似于Object o1;Object o2=o1,则此时o2是o1的复制)会在栈中复制出一个新的对象对其引用类型在堆上的内存引用,此时我们对o1的更改的同时o2也会改动,因为二者此时引用的是同一个东西

相反,值类型对象复制后是两个不同的对象,二者之前不会有关联。

除非满足以下全部条件,否则不应将类型声明为值类型

  1. 类型具有基元类型的行为。(没有任何成员会修改类型的任何实例字段,即不可变类型,对于值类型,建议将全部字段标记为readonly)。
  2. 类型不需要从其他任何类型继承。
  3. 类型也不派生出其他任何类型。

因为实参默认以传值方式传递,返回也相同,都会对值类型实例中的字段进行复制,对性能造成损害,所以还必须满足以下条件

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

值类型变量赋值给另一个值类型变量,会执行逐字段的复制。引用类型变量赋值给另一个一引用类型的变量只复制内存地址。

值类型的装箱和拆箱 

 简单的说值类型到引用类型就是装箱,反之为拆箱。

那么我们来深入了解一下

struct Point{
    public Int32 x,y;
}
public sealed class Program{
    public static void Main(){
        ArrayList a = new ArrayList();
        Point p;
        for(Int32 i = 0; i<10;i++){
            p.x=p.y=i;
            a.Add(p);
        }
        ...
    }
}

在循环中可看到,add获取的是一个Object参数(获取托管堆上的一个对象引用或指针)来作为参数,但p是值类型,所以Point值类型在此必须转换成真正的,在堆中托管的对象,且必须获得该对象的引用。

装箱步骤:

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

 拆箱并不是直接倒过来,代价会低很多。

eg: Point p =(Point) a[0];

我们通过此方式获取第一个元素的引用

  1. 获取已装箱Point对象的各个Point字段的地址(拆箱)
  2. 将字段包含的值从堆复制到栈上的值类型实例中。
public static void Main(){
    Point p;
    p.x=p.y=1;
    Object o =p;
    
    p=(Point)o;
    p.x=2;
    o=p;
}

由上述代码可直观的看出,后三行代码仅为更改p的x的值进行了拆箱再封箱的操作,其复制对应用程序性能的影响非常大。

 

public static Void Main(){
    Int32 v = 5;
    Object o = v;
    v = 123;
    
    Console.WriteLine(v+", "+(Int32)o);
}

可以看出上述代码进行过3次装箱么?第一次装箱在Object o =v;此时对v进行装箱将引用放到o中,第二和第三次在Console.WriteLine()中,v 此时进行装箱,将指针留在栈上以进行Concat(连接)操作。第三次在o进行拆箱后再次对Int32进行装箱,将指针保留在栈上进行Concat操作。

 上述代码可简化为:

public static Void Main(){
    Int32 v = 5;
    Object o = v;
    v = 123;
    
    Console.WriteLine(v.ToString()+", "+o);
}

由于v.ToString方法会返回一个String类型对象,以及o本身就是Object类型对象,可避免后两次装箱及o对Int32的拆箱操作。

如果知道自己代码将会造成编译器反复对一个值类型进行装箱,可以改成手动方式进行装箱

public static void Main(){
    Int32 v = 5;
    Console.WriteLine("{0},{1},{2}",v,v,v);//这里对v进行了3次装箱
    //上述代码完全可以更改为:
    Int32 v = 5;
    Object o = v;
    Console.WriteLine("{0},{1},{2}",o,o,o);
}
    

 

你可能感兴趣的:(CLR,C#)