C#1所搭建的核心基础(二)-值类型和引用类型

  在.NET中做得一切其实都是和一个值类型或者引用类型打交道,但极有可能一些人使用C#开发了很长时间,对这些差异也只是有一个模糊的概念。更糟糕的是,可能还存在着一些误解。稍不留神,就很容易作出一个简短但不正确的陈述。这里我将做简要讨论只是为了深入更高版本C#的世界,C#1的哪些主题的基本元素是必须理解的。

  先来看看值类型和引用类型的基本差异在现实世界和在.NET中是如何自然体现的。

  现实世界中的值和引用

  假定你正在读一份报纸,为了给朋友一个副本,需要复印报纸的全部内容并交给他。这样,他将获得属于他自己的一份完整的副本。在这种情况下,我们处理的是值类型的行为。你的报纸和你朋友的副本都是各自独立的。可以在你的报纸上添加一些注释或画一些图画,你朋友的报纸根本不会改变。

  再来假定你正在浏览一个网页。与前一次相比,这一次,唯一需要给朋友的就是网页的URL。这是引用类型的行为,URL代替引用。为了读到文档,必须在浏览器中输入URL,并要求它加载网页来导航引用。另一方面,假如网页由于某种原因发生了改变,你和你的朋友下次载入页面时,都会看到那个改变。

  在C#和.NET中,值类型和引用类型的差异和现实世界中的差别类似。.NET中的大多数类型都是引用类型,你以后创建的引用类型极有可能比值类型多的多。除了以下总结的特殊情况,类(使用class来声明)是引用类型,而结构(使用struct来声明)是值类型。

  特殊情况包括如下方面:

  1)数组类型是引用类型,即使元素类型是值类型(所以int[]仍是引用类型,即使int是值类型);

  2)枚举(使用enum来声明)是值类型;

  3)委托类型(使用delegate来声明)是引用类型;

  4)接口类型(使用interface来声明)是引用类型,但可有值类型实现。

  学习值类型和引用类型时,要掌握的重要概念是一个特殊的表达式的值是什么。为了使问题具体化,我使用了表达式最常见的例子-变量。但是,同样的道理也适用于属性、方法调用、索引器和其他表达式。

  对于值类型的表达式,它的值就是表达式的值,这很容易理解。例如,表达式“3+2”的值就是5。然而对于引用类型的表达式,它的值是一个引用,而不是该引用所指代的对象。所以表达式String.Empty的值不是一个空字符串,而是对空字符串的一个引用。

  为了进一步演示这个问题,来看看存储了两个整数x和y的一个Point类型,它的一个构造函数能获得两个值。现在,这个类型可以实现为结构或类。下图展示了执行下面两行代码的结果。

  在两种情况下,p1和p2在赋值后都具有相同的“值”。然而,在Point是引用类型的情况下,那个“值”是引用:p1和p2都引用同一个对象。在Point是值类型的情况下,p1的值是一个“点”的完整的数据,也就是这个“点”的x和y值。将p1的值赋给p2,会复制p1的所有数据。

  有一点需要指出,变量的值是在它声明的位置存储的。局部变量的值总是存储在栈(stack)中(这个论断只有在C#1中才是完全成立的,在更高版本的C#中,在特定情况下,局部变量最终可能存储在堆上)。实例变量的值总是存储在实例本身存储的地方。引用类型实例(对象)总是存储在堆(heap)中,静态变量也是。

  两种类型的另一个差异在于,值类型不可以派生出其他类型。这将导致的一个结果就是,值不需要额外的信息来描述实际是什么类型。把它同引用类型比较,对于引用类型来说,每个对象的开头都包含一个数据块,它标识了对象的实际类型,同时还提供了其他一些信息。


  以上对值类型和引用类型做了说明,相信认真读完的人已经对值类型和引用类型有了一定了解。许多人对于装箱和拆箱的理解也存在一定的误区,下面将作一个简单说明。

  有的时候,我们就是不想用值类型的值,就是想用一个引用。之所以会发生这种情况,有多种原因。幸好,C#和.NET提供了一个名为装箱(boxing)的机制,它允许根据值类型来创建一个对象,然后使用对这个新对象的一个引用。在接触实际例子前,先来回顾两个重要的事实:

  1)对于引用类型的变量,它的值永远是一个引用;

  2)对于值类型的变量,它的值永远是该值类型的一个值。

  基于这两个事实,下面3行代码第一眼看上去似乎没有太多的道理:

  int i=5;

  object o=i;

  int j=(int) o;

  这里i是值类型变量,o是引用类型变量。将i的值赋给o有道理吗?o的值必须是一个引用,而数字5不是引用,它是一个整数值。实际发生的事情就是装箱:运行时将在堆上创建一个包含值(5)的对象(它是一个普通对象)。o的值是对该新对象的一个引用。该对象的值是原始值得一个副本,改变i的值不会改变箱内的值。

  第3行执行相反的操作—拆箱。必须告诉编译器将object拆成什么类型。如果使用了错误的类型,就会抛出一个InvalidCastException异常。同样,拆箱也会复制箱内的值,在赋值之后,j和该对象之间不再有任何关系。

  以上一段话其实已经简单明了的解释了装箱和拆箱。剩下的唯一的问题就是要知道装箱和拆箱在什么时候发生。拆箱一般是很明显,因为要在代码中明确地显示一个强制类型转换。装箱则可能在没有意识到的时候发生。上面展示的是一个简单的版本。但是为一个类型的值调用ToString、Equals或GetHashCode方法时,如果该类型没有覆盖这些方法,也会发生装箱。另外,将值作为接口表达式使用时---把它赋给一个接口类型的变量,或者把它作为接口类型的参数来传递---也会发生装箱。例如:IComparable x=5;语句会对数字5进行装箱。

  之所以要留意装箱和拆箱,是由于他们可能会降低性能。一次装箱和拆箱操作的开销是微不足道的,但假如执行千百次这样的操作,那么不仅会增大程序本身的操作开销,还会创建数量众多的对象,而这些对象会加重垃圾回收器的负担。同样,这种性能损失通常也不是大问题,但还是应该引起注意。

  这里对值类型和引用类型、装箱和拆箱操作做了简单介绍,上一节对委托做了简单介绍。希望对各位有所帮助。
 

posted @ 2012-09-03 08:48  爱智旮旯  阅读(813)  评论(7编辑  收藏  举报