您的位置:首页 > 编程语言 > C#

c#1所搭建的核心基础之值类型和引用类型

2013-11-02 02:12 281 查看
这个主题很重要,在.NET中做的一切其实都是在和一个值类型或者引用类型打交道。

现实世界中的值和引用

假定你在读一份非常棒的东西,希望一个朋友也去读他。于是你到复印室里复印了一份。这个时候他获得了属于他自己的一份完整副本。在这种情况下,我们处理的是值类型的行为。你和你朋友是各自独立的。你可以在自己的上面加一些注释,他的报纸不会改变。

再假定你在qq空间里发表了一篇日志,你想让朋友看到,这一次,你唯一需要给你朋友的是你qq空间日志所在的url地址。这就是引用类型的行为。当你修改你的日志时,你的朋友就你那个看到改变。

在c#和.net中,值类型和引用类型的差异与现实世界中差别类似。.Net中的大多数类型都是引用类型。除了一下总结的特殊情况,类(使用class来声明)是引用类型,而结构(使用struct来声明)是值类型。特殊情况包括如下方面:

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

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

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

接口类型(使用interface声明)是引用类型,但是可由值类型实现

值类型和引用类型基础知识

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

大多数表达式都有与其相关的静态类型。对于值类型的表达式,它的值就是表达式的值,例如“2+3”的值就是5。 然而,引用类型的表达式,他的值是一个引用,而不是该引用所指代的一个对象。所以string.Empty的值不是一个空字符串-而是对空字符串的一个引用。在平常的讨论中,甚至在一些专业文档中,经常混淆这一区别。例如,你可能这样描述: string.Concat的作用是返回一个字符串,该字符串将所有参数都连接到一起。

变量的值是在它声明时的位置存储的。局部变量的值总是存储在栈中。实例变量的值总是存储到实例本身存储的地方。引用实例(对象)总是存储在堆中,静态变量也是。

两种类型的另一个差异在于,值类型不可以派生出其他类型。这将导致的一个结果就是,值不需要额外的信息来描述值实际是什么类型。把他同引用类型比较,对于引用类型来说,每个对象的开头都包含一个数据块,它标识了对象的实际类型,同时还提供了其他一些信息。永远都不能改变对象的类型—执行简单强制转换时,运行时会获取一个引用,检查它引用的对象是不是目标类型的一个有效的对象,如果有效就返回原始引用,否则抛出异常。

走出误区

误区1:“结构是轻量级的类”

这个误区存在多种形式。

有人认为值类型不能或者不应该有方法或其他有意义的行为。对于这种说法,一个非常典型的反例就是DateTime类型:它作为值类型来提供是很有道理的,因为他非常适合作为数字或者字符形似的一个基本单位来使用。另外它也应该赋予对它的值执行计算的能力。换个角度看这个问题,是数据转移类型一般都是引用类型。总之,具体应该如何决定,应取决于需要的是值类型的语义还是引用类型的语义,而不是取决于这个类型简单与否。

还有一些人认为值类型之所以显得比较轻,是因为性能。事实是在某些情况下值类型很能干,它们不需要垃圾回收,不会因引用类型标识而产生开销,也不需要取值这一步操作。但是在其他方面,引用类型更能干,在传递参数、赋值、将值返回和执行类似的操作时,只需复制4或8字节,而不是复制全部数据。假定ArrayList是一个所谓“纯的”值类型,那么将一个ArrayList传递给一个方法时,就得复制它的所有数据!几乎在所有情况下,性能问题都不是根据这种判断决定的。瓶颈从来不是想当然的,在你根据性能进行设计之前,需要衡量不同的选择。

值得注意的是,将这两者结合也不能解决问题:类型(不管是类还是结构)拥有多少方法并不重要,每个实例所占用的内存不会受到影响(代码本身消耗内存,但这只会发生一次,而不是每个实例都发生)

误区二“引用类型在堆上,值类型在栈上”

这个误区主要归咎于转述这句话的人没有动脑筋。第一部分是正确的—引用类型的实例总是在堆上创建的。但第二部分就有问题了。前面讲过,变量的值是在他生明的位置存储的。

所有假定一个类中有一个int类型的实例变量,那么在这个类的任何对象中,该变量的值总是和对象中的其他数据在一起,也就是在堆上。只有局部变量(方法内部声明的变量)和方法参数在栈上。但是在c#2及更高版本,很多局部变量并不完全放在栈上。

误区三:对象在c#中是通过引用传递的

这或许是传播最广的一个误区了。同样,说这句话的人一般(并不总是)知道c#实际的行为是什么,但不知道“引用传递”的真正意义是什么。重要的一点是:假如以引用传递的方式来传送一个变量,那么调用的方法通过更改其参数值,来改变调用者的变量值。现在请记住:引用类型变量是值的引用,而不是对象本身。不需要引用来传递参数本身,就可以更改改参数引用的那个对象的内容。

例子:

using System;

using System.Collections.Generic;

using System.Linq; using System.Text;

using System.Threading.Tasks;

namespace ConsoleApplication1 {

class Program {

public void AppendHello(StringBuilder builder) {

builder.Append("hello");

builder = new StringBuilder("xyz");

}

static void Main(string[] args) {

StringBuilder mybuilder = new StringBuilder("123");

new Program().AppendHello(mybuilder);

Console.Write(mybuilder);

}

}

}

运行结果是123hello,说明参数对象没有发生改变。说明参数不是引用

调用这个方法时,参数值(对某StringBuilder的一个引用)是以值传递(pass by value)的方式传递的。如果想再方法内部更改builder变量的值,如执行builder=null,语句,调用者看不到这一改变。

有趣的是,在这种错误的说法中,不仅“引用传递”的说法有误,而且“对象传递”的说法也存在问题。无论是引用传递还是值传递,对象本身永远不会被传递。涉及一个引用类型时,要么以“引用传递”方式传递变量,要么以“传值”的方式传递参数值。最起码,这回答了当null做为一个传值参数的值来使用时会发生什么的问题。假如传递的是对象,这个时候就会出问题,因为没有一个对象可供传递!相反,null引用会采用和其他引用一样的“值传递”的方式传递。

装箱和拆箱

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

对于引用类型的变量,他的值永远是一个引用

对于值类型的变量,他的值永远是该值类型的一个值

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

int i=5;

object o=i;

int j=(int) o;

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

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

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

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

整理自:c# in depth
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: