C#基础实践:引用与值类型

来源:互联网 发布:无钢圈内衣品牌 知乎 编辑:程序博客网 时间:2024/04/29 18:22

     引用类型和值类型

      一、引用类型

      表示引用类型的关键字有三大类:class,interface,delegate。 当然,还有一种引用就是基于泛型的定义,不过其基础还是前面三种基本的类型。引用在我的理解中其实就是,基于地址的访问,地址就就需要向系统申请空间,然后指向定所分配空间的开始点,所以就有了class,delagate这两种类型的变量在使用前需要使用关键字new申请空间,然后通过该变量访问对象的数据,方法。而接口呢,需要类实现才能引用,其实,这个过程可以简单的理解成:接口就是方法的格式申明或者定义,而类实现的时候就相当于把接口中的申明的方法进行个性化实现,然后通过类的实例访问,或者转化成接口,通过接口访问,这样看来,接口与类多么的相似:对应的变量都是通过引用的方式在工作。又有点不同:类可以选择性地实现方法,同时实例化后可以通过引用访问方法,存储数据,而接口依赖类实现方法,且不能存储数据。

      接口依赖于类?这个多少让人不爽:如果接口的方法通过类的静态方式实现岂不是不依赖与类的实例(虽然要依赖于类的实现),这样两者都是一种申明而已。在动手实现之前,我突然想到,在pascal里面,是否标记为类方法(类似于C#中的静态方法,当然方法中不能访问类中定义的变量),不创建实例也可以访问的,那C#呢?

1 托管代码的方法地址测试:

1.1 定义


1.2 调用


1.3 结果


测试1 结果很明显:对于托管代码,即使方法也需要有效实例来调用,这个跟非托管的pascal不一样。C#大概有两种存储区域:栈和托管堆。

2 测试尝试静态方法实现接口,绕开类实例

2.1 定义


刚刚编译了一下,编译器已经出现了不爽的波浪线了。

测试1,和测试2说明了C#编译器的规则:1)在托管环境下,类的数据,非静态方法必须实例化对象,然后按“名”索骥  2)接口就是类的附庸,否则谁也别想用,在类实例中找到接口的地址,访问名访问方法,而方法是在类中实现的 3)重要推论:普通类方法(类中定义的非静态,非抽象方法,接口中定义的方法)的地址依赖于调用者的申请,任何类不能以静态方法的方式实现接口方法(一个更简单,明了的说法就是接口方法定义没有static这个关键字,实现类中也不能改变方法的签名),这样的话就违背了前面的结论,这个很重要,用一个简单的图表示如下:


二、值类型

     值类型的关键字,我目前知道的也就只有struct,当重写ToString方法的时候,在Base关键字上F12,弹出的是ValueType.但是值类型,是以关键字struct的形式声明的,所以,值类型和引用在使用上有区别(值类型默认的存储空间是在栈上的,由编译器自动管理,而引用类型的则是要显式的调用new,在托管对上申请空间才能使用),但是两者显然有天然的联系,这个发生联系的地方就是装箱和拆箱机制。valuetype是个抽象类,但是申明了常量接口,值类型的子类重写了这些方法,其变量调用就不会出现装箱,这里面最常见的一个方法就是ToString().所以,值类型变量进行调用ToString就不会出现装箱的问题

    值类型的内容是存储在栈空间上面的,变量直接指向内容,但是值类型在实现的时候可以实现一些接口!从前面的分析可以知道,接口方法的访问是需要明确的引用的,所以,由值变量转化成其实现的接口变量时,肯定会发生装箱的。先看看Int32的实现声明:

当把一个值类型的变量强制转换成其支持的一个接口时,就会发生一个装箱的操作,类似下面

到这里,我突然有个疑问:如果我再进行一次转换赋值会出现怎样的结果呢?已经装箱过一次,是不是编译器会从之前的那个装箱引用会复用吗?写个简单的代码测试了下:

结果会怎样呢?

结果很明显的说明了一个问题,直接的转换是每次都会装箱的,如果直接将一个值变量转换成一个引用类型变量,这个装箱的地址我们是知道的,然后就可以按“名”索骥了,这样做的实质是装箱,那我突然想,值类型不是都有一个默认的无参构造函数吗,那我直接new一个,是不是就到托管堆上了?

针对这个结论,我想大概是这样,值类型有一个无参的构造函数,你调用与不调用,编译器都会执行类似的“优化”操作,将在栈上申请一块类型大小的空间存储其内容。至于有构造函数,我想这个也不难解释,值类型和引用类型的基类都是object.对象天然有构造函数。
但是,我试着改变了代码,将类型转化成 object int32val = New Int32();结果:

输出的结果:

结论就是:不同的值对应不同的引用(赋值未32之后,两个引用已经不一样),但是值未改变引用一直有效(获取接口的时候,由于引用类型的变量,在更新值的时候,完成了一次拆箱赋值,再装箱的过程,所以,看到后面取接口都是一次装箱),另外就是,每次装箱都是独立的,而装箱本身只是针对值类型来说的。

增加一点说明:为什么泛型类或者泛型方法中,值类型的变量无法进行==运算?值类型的成员类型,数量不定,根本没法比较,就算是相同类型,中间存在浮点数类型的变量也是无法进行比较的。而引用类型的可以呢?引用说白了就是就是一个地址的比较,默认空引用为null,任何值比Null大,还有就是地址本身就是一个整形数值,所以可以比较。