回到顶部

c#类型基础

一:所有类型都是从System.Objcet派生

“运行时”要求每个类型最终都是从system.Object类型派生。换言之,以下两个类型定义是完全一致的:

//隐式派生自Object

class Employee{

}

//显式派生Object

class Employee: System.Object{

}

由于所有类型都是派生自Objcet类型,所以可以保证的是每个类型的每个对象都有一组最基本的方法。Equals(),GetHashCode(),ToString(),GetType()。

CLR(公共语言运行时)要求所有对象都用new操作符来创建。下面代码演示如何创建一个Employee对象:

Employee e=new Employee(“ConstructorParam1”);

以下是New 操作所做的事情:

1.它计算类型及其所有基类型(直到Objcet)中定义的所有实例字段需要的字符数。堆上每个对象都需要一些额外的成员——即“类型对象指针”和“同步块索引”。这些成员由CLR用于管理对象。这些额外成员的字节数会计入对象的大小。

2.它从托管堆中分配指定类型要求的字节数,从而分配对象的内存,分配的所有字节数都设为0.

3.它初始化对象的“类型对象指针”和“同步块索引”。

4.调用类型的实例构造器,向其传入在对new的调用中指定的任何实际参数(上例就是“ConstructorParam1”)。大多数编辑器都在构造器中自动生成代码来调用一个基类构造器。每个类型的构造器在调用时,都要负责初始化有这个类型定义的实例字段。最终调用的是System.Objcet的构造器,该构造器只是简单地返回。不会做任何事情。

new执行了所有这些操作后,会返回指向新建对象一个引用(或指针)。在前面的示例代码中,这个引用会保存到变量e中。

 总结:有关层次结构下的类是在定义自己的构造函数时,会发生什么事情呢。实际上,在创建派生类的实例时,会有多个构造函数起到作用。要实例化的类的构造函数本身不能初始化类,还必须调用基类的构造函数。编译器首先试图找到实例化的类的构造函数。上面示例中的构造函数Employee。它首先要找到它的基类的构造函数Sytem.Objcet运行默认的构造函数,诚然。它什么都没做,它也没有基类。所以它执行完后,将控制权移交给构造函数Employee。然后该构造函数做它应该做的事情,初始化该类下的各个成员。这样的流程最后的结果就是派生类的实例可以调用基类下的成员、方法。所以我们也明白,基类的构造函数总是最先调用的。

二:类型转换

CLR最重要的特性之一就是类型安全性。在运行时,CLR总是知道一个对象是什么类型。调用GetType()。总是知道一个对象确切的类型是什么。由于这个方法是非虚方法,所以一个类型不可能伪装成另一个类型。例如,Employee类型不可以重写GetType()。

开发人员经常需要将一个类型转换为另外一个类型。CLR允许将一个对象转换为它的类型或者它的任何基类型。C#中,将一个对象转换为它的任何基类型都被认为是一种安全的隐式转换。然而,将对象转换为它的派生类型时,c#要求这样的转换只能进行显示转换。因为这样的转换存在运行时失败。

 总结:1.c#中,将一对象转换为基类时,不需要任何特殊操作即可完成--隐式转换过。2.将一对象转换为它的派生类时,必须显示转换。运行时存在失败。

下面的代码将演示向基类和派生类型的转换:

//该类型隐式派生自System.Object

class Employee{

}

static void Main(string[] args)
       {
           //不需要转类型,因为new返回一个Employee对象。
           Object o = new Employee();
           //需要转类型,因为Employee派生自System.Objcet
           //这里进行显示转换
           Employee e = (Employee)o;
       }

 

所以该代码在编译时不会出错。在显示转换那里。在运行时,CLR核实到o引用的是一个Employee对象。所以该显示转换也正好允许执行。

而下面的就会在运行时候报错了。

static void Main(string[] args)
       {
           //不需要转类型,?因为new返回一个Employee对象。
           Object o = new Employee();
           //需要转类型,因为Employee派生自System.Objcet 
       //这里进行显示转换
           Object d = new DateTime();
           //运行时,报错
           Employee e = (Employee)d;
       }

所以如果CLR允许这样的转型,就无类型安全可言了。

使用C#的is和as操作符来转型

在C#语言中进行类型转换的另一种方式是使用is操作符,is检查一个对象是否兼容于指定的类型,并返回一个bool类型。注意:is操作符永远不会抛出异常。

  Object o = new Employee();
            bool b1 = (o is object);//true
            bool b2 = (o is Employee);//true

如果对象引用的是null,is操作符总是返回false,因为没有可检查其类型的对象。

is操作符通常像下面那样使用:

if (o is Employee)
          {
              Employee e = (Employee)o;
          }

在这段代码中CLR实际会检测两次对象的类型。首先核实o是否兼容于Employee,然后if内部转型时CLR会再次核实o是否引用一个Employee对象。

这样无疑会对性能造成一定的影响,这是因为CLR首先必须判断变量o引用的对象的实际类型。然后,CLR遍历继承层次结构。用每个基类型去核对指定的类型。且由于这是一个比较常用的编程模式,所以C#便引入了as关键字。目的就是简化写法。提升性能。

Employee e = o as Employee;
           if (e != null)
           { 
           //...
           }

所以看到这里似乎明白了一些事情。

 总结:as操作符的工作方式和强制类型转换一样。只是它不会抛出异常,相反,如果不能转型,将返回一个NULL。

最后来一个小测试

 internal class B { }//基类
    internal class D : B { }//派生类

 针对每一行代码都用小勾对注明该代码是成功、还是编译时错误还是运行时错误。下图所示。

image

 

编程语言的基元类型

某些数据类型如此常用,以至于许多编译器允许代码以简化的方式来操作它们。例如。可是使用以下语法来分配一个整数:

Int32 a = new System.Int32();        

不过它相对于常用,且比较繁琐复杂,所以c#允许换用如下的语法:

int b = 0;

这种语法不仅增强了代码的可读性,而且生成的IL代码也是完全一致的。这种编译器直接支持的数据类型称为:基元类型。基元类型是直接映射到Framework类库中(FCL)存在的类型。下面4行代码都可以正确编译。并生成相同的IL代码:

int a = 0;//最方便的语法
     System.Int32 b = 0;//方便的语法
     int c = new int();//不方便的语法
     Int32 d = new System.Int32();//最不方便的语法

当然FCL在c#中对应的基于类型有很多。就不往下举例了。

一:引用类型和值类型

CLR支持两种类型:引用类型值类型。引用类型是从托管堆上分配的,c#的new运算符会返回对象的内存地址——也就是指向该对象数据的内存地址。引用类型有如下特性:

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

如果所有类型都是引用类型,那么应用程序的性能明显下降。如果每次使用int32类型都进行一次内存分配,性能将会大受影响,毕竟int类型频率用的非常高。所以。CLR提供了一种名为“值类型”的轻量级。值类型的实例一般在线程栈上分配。在代表值类型的实例变量中,并不包含一个指向实例的指针。相反,变量中包含了实例本身的字段。由于变量已经包含了实例的一个字段,所以为了操作实例中的字段,不再需要提领一个指针。值类型的实例不受垃圾回收的控制。一下代码演示引类型和值类型的区别:

SomeRef r1 = new SomeRef();//在堆上分配
            SomeVal v1 = new SomeVal();//在栈上分配
            r1.x = 5;                  //提领指针
            v1.x = 5;                 //在栈上修改
            Console.WriteLine("r1.x的值:" + r1.x);
           Console.WriteLine("v1.x的值:" + v1.x);

            Console.WriteLine("-----------");
            SomeRef r2 = r1;          //复制指针
              SomeVal v2 = v1;         //复制并重新分配成员
              r1.x = 8;
            v1.x = 8;
            Console.WriteLine("r1.x的值:" + r1.x);//8
            Console.WriteLine("r2.x的值:" + r2.x);//8
            Console.WriteLine("v1.x的值:" + v1.x);//8
            Console.WriteLine("v2.x的值:" + v2.x);//5
            Console.ReadLine();

 

看看运行效果图:

image

在上述代码中,SomeVal类型是用struct来申明的。而不是常用的class在C#中,用struct来声明的类型是值类型,用class来声明的类型是引用类型。

上述代码中有这样的一行:

SomeVal v1=new SomeVal();

因为这一行代码的写法,似乎是要在托管堆上分配一个SomeVal的实例。实际上根据前面提到的类型安全性c#编译器知道SomeVal是一个值类型。所以会在线程栈上分配一个SomeVal的实例,C#还会确保值类型中的所有字段都初始化为零。所以上述代码还可以这样写:

SomeVal v1;

这行代码在IL代码中发现也会在线程栈上分配一个实例。并且将字段初始化为零。唯一的区别在于new操作符,C#会认为实例已经完成初始化,一下代码更能清楚的说明问题:

SomeVal v1=new SomeVal();

int a=v1.x;

//下面不编译失败。

SomeVal v1;

int a=v1.x;//使用了未复制的字段x。

设计自己的字段时,要考虑是否应该讲一个类型定义为值类型.而不是定义为引用类型。某些时候值类型能提供更好的性能。具体来说,满足以下条件,可以考虑使用值类型。

  1. 类型具有基元类型的行为。换言之,这是一个十分简单的类型,其中没有成员会修改类型的任何实例字段。若一个类型没有提供会更改其字段的成员。就是说该类型不可变。对于这样的类型来说,我们都建议将他标记为readonly
  2. 类型不需要从其他任何类型继承。
  3. 类型也不会派生出其他任何类型。
  4. 类型的实例较小(约为16个字节);类型的实例较大的话,不作为方法的实参传递,也不从方法返回.

因为类型实例的大小在传值时候会有性能损失,值类型的实例在传值时,是对值类型中的实例进行复制.下面看看值类型和引用类型的区别.

  1. 值类型是从System.ValueType派生的。该类型提供了与System.Object定义相同的方法。然而,System.ValueType重写了Equals方法,能在两个对象的字段完全匹配的前提下放回true。除此之外,System.ValueType还重写了GetHashCode方法,生成哈希吗的时候,这个重写的方法所用的算法会将对象的实例字段中的值考虑在内,由于这个默认实现存在性能问题,所以在定义自己的值类型时,应该重写Equal和GetHashCode方法,并提供他们的显示实现。
  2. 由于不能将一个值类型作为基类型定义一个新的类型或者一个新的引用类型,所以不应该在值类型中引入任何新的虚方法。所有方法都不能使抽象的,而且所有方法都隐式的为密封方法。
  3. 引用类型的变量包含的是堆上的一个对象的地址。默认情况下,在创建一个引用类型的变量时,它被初始化为null,表明引用类型的变量当前不指向一个有效的对象。试图使用一个null的引用变量,会抛出一个异常,相反,值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为零。由于值类型的变量不是指针,所以在访问一个值类型时,不可能抛出异常,CLR确实提供了一个特殊的特性,能为值类型添加“可空性”。这个特性称为可空类型。
  4. 将一个值类型的变量赋给另一个值类型的变量时,会执行一次逐字段的复制。将引用类型的变量赋给另一个引用类型的变量时候,只是复制内存地址。(所以两个或多个引用类型的变量能引用堆中的同一对象,对这个对象的操作将会影响别的变量引用的对象,而值类型的则不会。)
  5. 由于未装箱的值类型不再堆上分配,所以一旦定义了该类型的一个实例的方法不再处于活动状态,为他们分配的存储就会被释放。这意味着值类型的实例在其内存被回收时,不会通过finalize方法接受到一个通知。

二:值类型的装箱和拆箱

值类型是比引用类型更“轻型”的一种类型,因为他们不作为对象在托管堆中分配,不会被垃圾回收,也不通过指针来引用。但是在许多情况下,都需要获取对值类型的一个实例的引用。例如,假定要创建一个ArrayList对象来容纳一个Point结构,那么代码如下:

/// <summary>
    /// 声明一个值类型
    /// </summary>
    struct Point
    {
        public int x, y;
    }
    public sealed class Program
    {
        public static void Main()
        {
            ArrayList a = new ArrayList();
            Point p;
            for (var i = 0; i < 10; i++)
            { 
                p.x=p.y=i;
                a.Add(p);//对值类型的P对象进行装箱操作,并将引用添加到ArrayList中
            }
        }
    }

每一次循环迭代都会初始化一个Point,然后被存储到ArrayList当中,那么ArrayList中到底存储的是什么呢,是Point结构,还是它的地址。那么先来看看ArrayList对象的Add方法是如何的实现。Add方法的实现如下:

public virtual Int32 Add(Objcet value);

可以看到Add方法需要一个Object类型的参数,换言之,需要获取托管堆上的一个对象的引用作为参数。但是在此之前Point是一个值类型。所以为了代码正常的工作,需要对Point类型转换为一个在堆中托管的对象,而且需要获取到它的引用。

为了将一个值类型转换为一个引用类型,需要使用一个名为装箱的机制。下面探讨下对值类型的对象是如何进行装箱操作的。

1.在托管堆中分配好内存,分配的内存量是值类型的各个字段需要的内存量加上托管堆上的所有对象都存在的两个额外成员(类型对象指针和同步块索引)的内存量。

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

3.返回对象的地址。现在这个地址是对一个对象的引用,值类型现在是一个引用类型。(这里不是很懂,其实我想明白下此刻线程栈上的变化。)

那么紧接其后,看看拆箱是如何进行的。

Point p=(Point)a[0];

现在要获取ArrayList中的元素0包含的引用,并试图将其放到一个Point值类型的实例P中,为了做到这一点,包含在已装箱Point对象中的所有字段都必须复制到值类型的变量P中,后者在线程栈上,CLR份两步完成这些操作,第一步是获取到已装箱的Point对象中的各个Point字段的地址。这个过程称为拆箱。第二步是将这些字段包含的值从堆中复制到基于栈的值类型的实例中。

拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低得多,拆箱其实就是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型。事实上,指针指向的是已经装箱实例中的未装箱部分。所以和装箱不同,拆箱不要求在内存中复制任何字节。

 三:dynamic动态类型

c#是一种类型安全的编程语言,这意味着所有表达式都解析成某个类型的一个实例,在编译器生成的代码过程中,只会执行这个类型来说有效的操作。和非类型安全语言相比,类型安全的语言的优势在于:程序员会犯很多错误能在编译时检测到,确保代码在你尝试执行它之前是正确的。但是在许多时候,程序仍需要在运行时才知道它的信息。为此,c#编译器允许将一个表达式的类型标记为dynamic。还可以讲一个表达式的结果放到一个变量中,并将变量的类型标记为dynamic,然后使用这个表达式调用一个成员,比如字段、索引器、方法等。下面代码演示dynamic 的操作:

internal static class DynamicDemo
    {
        public static void Main()
        {
            for (var i = 0; i < 2; i++)
            {
                dynamic arg = (i == 0) ? (dynamic)5 : (dynamic)"A";
                dynamic result = Plus(arg);
            }
        }
        private static dynamic Plus(dynamic arg) { return arg + arg;}
        private static void M(int i) { Console.WriteLine("M(Int32):" + i); }
        private static void M(string i) { Console.WriteLine("M(String):" + i); }
    }

那么执行时的结果:

M(Int32):10;

M(String):AA;

在字段、方法参数、返回类型指定为dynamic的前提下,编译器会将类型转换为System.Objcet,并在元数据中向字段、方法参数或返回类型应用System.Runtime.CompilerServices.DynamicAttribute的一个实例。为泛型、结构、接口、委托或方法指定泛型类型实参时,也可以使用dynamic。任何表达式都能隐式的转换为dynamic。因为所有的表达式都是最终都会生成Objcet类型。前面提到过正常情况下,编译不允许将一个表达式从Objcet隐式转换为其他类型;必须使用显示转换,然而,编译器允许使用隐式转换语法将一个表达式从dynamic类型转换为其他类型:

object o1 = 123;//从int抓换为objcet(装箱)
    int i = o1;//Error:无法将类型“objcet”隐式转换为int类型,是否存在一个显式转换?
    int i2 = (int)o1;//从objcet转换为int(拆箱)
    dynamic d1 = 123;//从int类型转换为dynamic(objcet)(装箱)
    int i3 = (int)d1;//拆箱

在大多数情况下, dynamic 类型与 object 类型的行为是一样的。 但是,不会用编译器对包含 dynamic 类型表达式的操作进行解析或类型检查。 编译器将有关该操作信息打包在一起,并且该信息以后用于计算运行时操作。在此过程中,类型 dynamic 的变量会编译到类型 object 的变量中。类型 dynamic 只在编译时存在,在运行时则不存在。

资料《CLR Via c#》

posted @ 2013-01-15 22:31  telang  阅读(2786)  评论(3编辑  收藏  举报