值类型和引用类型(Value Type & Reference Type)html
.NET使用两种不一样的物理内存来存储数据,数据类型可简单分为值类型和引用类型。当声明一个值类型的变量后,系统会在栈(stack)中分配适当的内存来存储值类型的数据。而引用类型的变量虽然也利用栈,但栈上的地址是对堆(heap)地址的引用,引用类型的数据都存储在托管堆上,栈存储的是引用类型在托管堆上的地址的一个引用。数据库
值类型之间的相互赋值会产生副本,若是将100赋值给x,那么当你把x赋值给y时就会发生一次值拷贝。这样,x和y都有一个相同的值,但两个变量指向栈上的地址是不同的。也即y是拷贝了x的副本,它们各自有本身的版本。即便你把x传递给一个方法,在方法体内部改变这个x,原来的x也并未改变,方法接收的这个x将是原来那个x的一份拷贝。数组
string比较特殊,自己string一旦建立就不能被改变,当为它赋予一个新字符时,原来那个字符还驻留在内存中,只不过再也不有变量指向原来那个字符在堆上的地址而已。即便更改一个string的大小写,也不会真的改变这个字符,这只会在堆上建立一个新字符。为了下降内存占用,C#的字符串还有留用机制,假设某个字符串在内存中已经有了地址,那么假如两个变量的值都是该字符串,则两个字符串的引用地址是相等的。但字符串留用机制只针对常量,因此下面第二个测试引用相等性的输出是false。安全
将值类型变量传递给方法,方法会创建值类型变量的拷贝,将引用类型变量传递给方法,方法会创建引用类型变量的地址的拷贝。也即值类型参数在方法中是独立的,与外部的那个变量没有关系,在方法内部改变这个变量不会影响外部那个变量,而引用类型由于传递的是引用地址,因此在方法中改变该变量会同时改变外部的那个变量。ide
将一个数组赋值给另外一个数组时,是将前一个数组的引用传递给另外一个数组,但传递一个数组给某个方法时,发生的是值拷贝。函数
从占用内存空间上考虑,值类型的释放明显快于引用类型,由于存储在栈中的数据一旦离开做用域(块)就会被马上销毁而不用等待垃圾收集器来完成销毁的工做,试考虑在一个方法中定义了一个值类型的数据,一旦方法执行结束,该值会被马上清除,又假设在方法中定义一个引用类型x,方法中调用了另外一个方法,假设另外一个方法也调用了其它方法而且每个方法都引用了x,那么x是不可能立刻被销毁的,由于引用类型不创建拷贝,堆上的数据被改变,那么引用这个地址的变量都会被改变。从执行效率上考虑,当拷贝发生时,引用类型比值类型更高效,执行效率更快,由于它不须要副本,只须要拷贝一个堆地址的引用。值类型却须要在栈上分配内存空间存储副本数据。针对不一样的状况应采起不一样的方式处理这个问题。性能
null能够赋值给引用类型的变量,它表明的含义是未指向堆上的任何地址。若是x="",则x指向了""在堆上存储的地址,因此null!=""。学习
值类型不能够被赋值为null,但数据库表的值类型却能够为null,从表里查询的数据若是是null则没办法赋值给C#的值类型,为了解决这个问题,从2.0开始可使用Nullable<T>来表示一个能够为空的值类型。可使用可空修饰符?来代表任意类型是能够被赋值为null的。测试
void是在声明方法时使用,代表该方法没有任何返回类型。大数据
直接写出的一个值就是字面量(值)。如12345六、"aa"。但string x="aa"则不是。
值类型分为枚举(Enum)和结构(Struct)。
内置的值类型就是struct类型,从小到大依次为:sbyte<short<int<long<float<double<decimal
字符将被自动转换为其对应的UTF-16编码,一个英文字符对应的UTF-16编码占一个字节,一个中文字符对应的UTF-16编码占两个字节。char类型的字符不能使用双引号,只能使用单引号括起来。
全部值类型或自定义的结构类型都派生自System.ValueType类。
注意
C#编译器默认整数类型的字面量是int类型,若是int存储不了该值则默认是long类型,浮点数则被默认是double类型。即便你把1赋值给一个byte类型的变量,该值也是Int32类型。
当你用一个byte类型的变量存储整数值时,该值被默认为是int,但实际存储的只有8个位。若是该变量参与数学运算,则它的值又会被当作int,在C#中整数类型都是以int或long进行计算,C#并无为byte等类型重写任何数学运算符。另外,整数类型相除若是预期结果有小数,小数不会保留,计算这样的结果应使用浮点数类型。
能够为值类型的字面量指定后缀以转换该值被C#默认为的类型,可用的后缀(不区分大小写)有:m、d、f、u、l、ul,分别表示:decimal、double、float、uint、long、ulong。
小转大就是隐式转换。也即隐式转换老是发生于位数小的类型转位数大的类型。
大转小就是强制转换,也即位数大的类型转位数小的类型可能会丢失精度(sbyte转int没问题,int转flota没问题,倒过来则是错误的),编译器会及时提示错误。需考虑强转。
计算机以数字的二进制形式进行存储。当声明了一个值类型的变量时,系统会根据它可存储的bit位数来进行内存分配(划分)。下图是cpu的栈位,0-7是8个位,8个位=1个字节。内存编号是存储数据的地址,变量标识符(变量名)指向了数据存储的物理地址(内存编号)。
数值的存储规则
1.该数的二进制数的位占不满内存划分的位时,系统会把0放置在该二进制数以前进行填充直到占满为止。
根据你声明的值类型的可存储最大位数,cpu自动为其划分对应位数的栈,下图涂色区域是可存储8位的栈,其它以此类推。
如今假设咱们要声明一个int类型的变量来存储3。
数字3的二进制数是11,该数只占2个bit位,2个位占不满int所声明的32个位,因此11前面会被填充30个0:
0000 0000 0000 0000 0000 0000 0000 0011(看下图)
补0时从内存编号的高位开始补起,下图中能够看到是从10000003的区域开始补0:
999999999的二进制数是1110 1110 0110 1011 0010 0111 1111 11,该数只占30个位,30个位占不满int所声明的32个位,因此该数前面会被填充2个0:
0011 1011 1001 1010 1100 1001 1111 1111(看下图,3的二进制数已占满从内存编号10000000开始到10000003的区域,因此999999999的二进制数将划分在后面,灰色部分)
补0(补码)时从内存编号的高位开始补起,下图中能够看到是从10000007的区域开始补0:
2.负数的存储是把当前数字的绝对值的满位后的二进制数按位取反再+1的形式来表示,流程是:1.取绝对值。2.转化为二进制数。3.不满位数则以0补位。4.按位取反:1变0,0变1。5.用结果数+1。6.用结果数逢二进一。按位取反称为反码,+1称为补码。
假设如今要用short存储数字-1000,绝对值1000的二进制数是1111 1010 00,该数只有10位,前面要补6个0获得0000 0011 1110 1000,每一个位取相反数(1的相反数是0)获得:1111 1100 0001 0111,1111 1100 0001 0111+1=1111 1100 0001 0112,逢二进一获得:1111 1100 0001 1000,首位的1会被计算机识别为负号,负号占了1个位。以下图:
写个程序检测一下:
引用类型分为三种
1.类(Class)
2.接口(Interface)
3.委托(Delegate)
计算机以数字的二进制形式进行存储。当声明了一个引用类型的变量时,系统会在栈上默认为其划分32个位用于存储该标识符对该对象地址的引用。这个分配内存的流程以下:
在Main中声明了一个Animal类型的变量时,系统在栈上份内存并把每一个位所有都刷成0,如图:
接着你new一个对象
此时系统会扫描该对象的成员,上面咱们在该对象的类里定义了一个32位的ID和一个16位的NameCode ,计算后获得48个位,系统就在堆上面为其划分48个位用来存储该对象。
从30000001开始先分配32个位,接着分配16个位。完成后,须要把对象在堆上的起始地址(内存编号)30000001转换为二进制数,这个二进制数会被填充到栈上,栈就完成了对堆的引用。30000001转换为二进制数获得:
1110 0100 1110 0001 1100 0000 1 (4*6=24位,不够32位,因此在其高位补足7个0)获得:
0000 0001 1100 1001 1100 0011 1000 0001 (恰好32位)
这个数字会被填充到刚才被刷成0的栈上,这样,栈就完成了对实例对象的引用,30000001的二进制数就成为了指向Animal对象的真正地址。30000001的二进制数填充到栈后如图:
这样animal这个变量在栈上的数据就被刷成了一个内存上的物理地址,这个地址指向了该变量所对应的对象的真正数据。
如今假设你要把animal赋值给另外一个变量,如图:
此时,系统会把animal在栈上的内存编号所引用的地址(30000001的二进制表示)copy、填充到animal2在栈上的内存编号所占用的位,假设此时10000004到10000007已经被其它数据占满,那么这个copy会在10000008处开始填充,如图:
若是类型里有引用类型的成员,好比一个string类型的成员,那么系统一样会在堆上(而非栈上)为该成员变量分配32个位用来存储能指向它数据的地址,而后在堆上另辟一块区域去存储它的值。
方法在执行时,系统会在栈的高(内存编号从高到低,从下往上为函数划份内存)位上为该方法分配一个stack frame的空间。分配完成后stack frame以下图,栈帧用于存储函数做用域并执行。做用域中为方法的变量分配栈空间,一条规则是这样的:在哪一个方法中声明哪些变量,那么那些变量就由那个方法负责为它们划份内存。因此在如下在A方法的做用域中声明了两个byte类型的变量x和y,因此在栈帧包含的栈上为x和y划分了两个字节,A方法还接收了两个参数,由于参数是byte类型,因此还会发生值拷贝,这样,A方法还须要为i和z划份内存,Main方法中声明了i和z,因此在Main方法的栈帧上会为i和z划份内存,Main方法还接收一个string类型的args参数,因此还须要为args在堆上划份内存,再把堆地址填充到Main的栈帧所包含的栈上。
以上的Main方法是caller,因此调用的A方法的两个参数i和z的内存分配就划归给Main管理。看图:
栈溢出(stack overflow)就是由于运行时,方法的stack frame空间是由高位向低位划份内存,若是方法有返回值,return后函数终止,它区块内的全部变量就会当即被销毁,但若是一直没有rentun,好比无限递归,这会形成一直向上划份内存,直到低位的区块被完全占满,最终就会致使栈溢出。
最后咱们须要知道,程序运行时,不管是值类型抑或引用类型,当程序执行到声明这些变量的代码的时候,就会为其划分对应的内存,而后将每个位都刷成0,直到赋值后才会有数据。这就是为何当声明一个变量却不使用它时,编译器会提示你还未使用过该变量,由于当程序运行起来后未使用的变量会浪费内存资源。
咱们能够把栈当作小盒子,把堆当作大箱子。装箱拆箱是指不一样数据类型之间的转换。
由于值类型较小而引用类型较大,把小数据装进大箱子是装得下的,因此装箱是属于隐式进行。把大数据装进小盒子不必定装得下,对象装进小盒子就有可能拆掉大箱子后都装不下,因此拆箱是属于显示或强制进行,系统不会自动为你转换,这须要你本身手动显示或强制转换,装箱拆箱须要一个装或拆的过程,因此大量装和拆就会形成性能损耗。
假设有一个Animal类型的变量,若是直接把这个变量赋值给另外一个Animal变量,这个行为不叫拷贝,应叫作赋值,这会使两个Animal变量指向同一个堆上的地址。如今假设Animal有一个int类型的ID字段和一个Person类型的person字段,当拷贝Animal对象时,你有两个选择:
1.只拷贝Animal对象的ID,只拷贝Animal对象的person指向的堆地址,此为浅拷贝。
2.拷贝Animal对象的ID,拷贝Animal对象的person的数据,此为深拷贝。
你可使用Object的MemberwiseClone方法建立对某对象的浅拷贝。MemberwiseClone是一个受保护的方法,只能在Object的派生类的类代码块中使用,该方法返回一个浅拷贝的对象,该对象只拷贝了源对象的值类型的成员,而引用类型的成员则只有一个堆引用地址。
从上面代码的结果可知,改变p2的成员department,则p的department也会跟着被改变。由于p2的department修改了堆上的值,而深度拷贝能够解决这个问题。
转换
对于值类型来讲,小转大,大能存储小,因此隐式转换就能够完成。 大转小,小不能存储大,不被容许,因此必须显示甚至强制转换。对于引用类型来讲,子类转父类/基类,子派生自父类/基类,因此隐式转换就能够完成。父类/基类转小,父类/基类并不从子类派生,因此不存在转换问题。
隐式转换:直接赋值
相同类型之间,小转大,可隐式转换。隐式转换就是编译器自动进行转换,不须要你亲自动手。
显示转换:(类型)变量
相同类型之间,大转小,精度丢失,编译器会提示错误,此时须要你亲自动手显示转换。
强制转换:Convert.Toxxx( )方法 | Parse()方法
不一样类型之间进行转换时才须要强转,编译器没法推测转换结果,这种转换若是出错只能在运行时抛出异常。也即强制转换就是告诉编译器不要插手个人逻辑,我对个人行为负责。一般状况都是须要将一个object类型转换为其它类型时使用强转。
安全强制转换:TryParse()方法 | as操做符
这是最保险的方法,TryPrase方法是值类型的方法,它接受两个参数,一个是被转换的操做数,另外一个是out类型的操做数。该方法测试操做数是否可被转换,并返回一个bool值,若是结果为真,就把转换结果给out变量,为假则不。as操做符是引用类型的操做符,它测试当前操做数的类型是否能够转换为目标类型,若是不能则返回null,该操做符不会由于转换失败抛出异常。
非转换:toString()
任何类型都继承了Object类,它提供了toString()方法,既然是任何类型,则结构类型一样可使用toString(),但null由于没有指向堆上的地址,因此为null的变量使用该方法会抛错。
自定义显示转换:关键字explicit
自定义隐式转换:关键implicit