Net的类型分为两种,一种是值类型,另一种是引用类型。这两个类型的本质区别,值类型数据是分配在栈中,而引用类型数据分配在堆上。那么如果要把一个值类型数据放到堆上,就需要装箱操作;反之,把一个放在堆上的值类型数据取出来,则需要进行拆箱操作。
对于如下简单的装箱和拆箱操作语句。
int i = 123;
object obj = i;//Boxing
if( obj is int )
int j = (int) obj;//Unboxing
效率更高的做法如下:
SomeType st = obj as SomeType;
if (st != null )
{
st.SomeTypeMethod();
}
明白了这两名词的意思,现在说说为什么要减少装箱和拆箱操作
原因有两个,主要是关于效率:一个就是对于堆的操作效率比较低;另一个就是对于堆上分配的 内存 资源,需要GC来回收,从而降低程序效率。
如何减少呢,涉及到这两个操作比较多的是,格式化输出操作,例如:String.Format,Console.WriteLine之类的语句。
Console.WriteLine( "Number list:{0}, {1}, {2}",1,2,3 );
对于“1,2,3”来说,相当于前面的“123”一样,需要经过装箱和拆箱两个操作。那么如何避免呢,其实只要向WriteLine传递引用类型数据即可,也就是按照如下的方式。
Console.WriteLine( "Number list:{0}, {1}, {2}", 1.ToString(),2.ToString(),3.ToString() );
由于“1.ToString()”的结果是String类型,属于引用类型,因此不牵扯装箱和拆箱操作。
牵扯到装箱和拆箱操作比较多的就是在集合中,例如:ArrayList或者HashTable之类。
把值类型数据放到集合中,可能会出现潜在错误
什么是值类型和引用类型
- 什么是值类型:
- 进一步研究文档,你会发现所有的结构都是抽象类型System.ValueType的直接派生类,而System.ValueType本身又是直接从System.Object派生的。根据定义所知,所有的值类型都必须从System.ValueType派生,所有的枚举都从System.Enum抽象类派生,而后者又从System.ValueType派生。
- 所有的值类型都是隐式密封的(sealed),目的是防止其他任何类型从值类型进行派生。
- 什么是引用类型:
- 在c#中所有的类都是引用类型,包括接口。
区别和性能
- 区别:
- 值类型通常被人们称为轻量级的类型,因为在大多数情况下,值类型的的实例都分配在线程栈中,因此它不受垃圾回收的控制,缓解了托管堆中的压力,减少了应用程序的垃圾回收的次数,提高性能。
- 所有的引用类型的实例都分配在托管堆上,c#中new操作符会返回一个内存地址指向当前的对象。所以当你在创建个一个引用类型实例的时候,你必须要考虑以下问题:
- 内存是在托管堆上分配的
- 在分配每一个对象时都会包含一些额外的成员(类型对象指针,同步块索引),这些成员必须初始化
- 对象中的其他字节总是设为零
- 在分配对象时,可能会进行一次垃圾回收操作(如果托管堆上的内存不够分配一次对象时)
- 性能:
- 在设计一个应用程序时,如果都是应用类型,那么应用程序的性能将显著下降,因为这会加大托管堆的压力,增加垃圾回收的次数。
- 虽然值类型是一个轻量级的类型,但是如果大量的使用值类型的话,也会有损应用程序的性能(例如下面要讲的装箱和拆箱操作,传递实例较大的值类型,或者返回较大的值类型实例)。
- 由于值类型实例的值是自己本身,而引用类型的实例的值是一个引用,所以如果将一个值类型的变量赋值给另一个值类型的变量,会执行一次逐字段的复制,将引用类型的变量赋值给另一个引用类型的变量时,只需要复制内存地址,所以在对大对象进行赋值时要避免使用值类型。
class SomRef
{
public int x;
}
struct SomeVal {
public int x;
}
class Program {
static void ValueTypeDemo() {
SomRef r1 = new SomRef();//在堆上分配
SomeVal v1 = new SomeVal();//在栈上分配
r1.x = 5;//提领指针
v1.x = 5;//在栈上修改
SomRef r2 = r1;//只复制引用(指针)
SomeVal v2 = v1;//在栈上分配并复制成员
}
}
引用类型分配在托管堆上,值类型分配在线程栈上:其实这种说法的前半部分是对的,后半部分是错的。因为变量的值在它声明的位置存储的,所以假如某一个引用类型中有一个值类型的变量, 那么该变量的值总是和该引用类型的对象的其它数据在一起,也就是分配在堆上。(只有局部变量(方法内部声明的变量)和方法的参数在栈上)
对象在c#中默认的是用引用传递的:其实在调用方法的时候,参数值(对象的一个引用)是以传值得方式传递的,如果你想以引用方式传递的话,可以使用ref或者out关键字。
- 使用非泛型集合时:比如ArrayList,因为这些集合需要的对象都是object,如果你将一个值类型的对象添加到集合中时会执行一次装箱操作,当你取值时会执行一次拆箱操作,所以在应用程序中应避免使用这种非泛型的集合。
- 大家都知道System.Object是所有类型的基类,当你调用object类型的非虚方法时会进行装箱操作(例如GetType方法)。在调用object的虚方法时,如果你的值类型没有重写虚方法也要进行装箱操作,所以在定义自己的值类型时,应重写object内部的虚方法(例如ToString方式)
使用ArrayList时,每添加一个时间都会进行一次装箱操作,而使用List<DateTime>时就不会进行装箱操作,从而提高应用程序的性能。
C#中常见的泛型集合:
Queue<T>;
Stack<T>;
List<T>;
Dictionary<Tkey,Tvalue>;
HashSet<T>;