C#基础之基本类型
本丝花了近半年,终于将《CLR Via C#》这本书看完了(请不要BS本⼈的看书速度T_T),这确实是⼀本好书,⼤⼤们推荐的果然值得⼀读。
虽然很多东西还没有尽得其要,我常想在⾃⼰深刻掌握了某个知识点后再总结分享出来(不知道⼤家是不是这个⼼理),但现在我觉得应该在⼀个⼈成长的过程中就去做这件事情,所以有了本篇不成⽂的总结,⽂中知识点⼤量来⾃《CLR Via C#》这本书,在此对作者及翻译者表⽰感谢!另⽂中如有错误的地⽅,欢迎⼤家指出!
术语解释:
CLR: 公共语⾔运⾏时(Common Language Runtime)
FCL:Framework类库(Framework Class Library)
IL: 中间语⾔(Intermediate Language)
类型基础
CLR要求所有的类型最终都是从System.Object派⽣的。这符合在⾯向对象的语⾔和设计中,⼀切皆为对象!
从图中,我们可以看到,显式从System.Object派⽣的类ExplicitlyDerivedFromObject和没有显式指定其⽗类的类ImplicitlyDerivedFromObject,最终⽣成的IL中间都是⼀致的。如果我们没有指定⼀个类的⽗类,编译器会⾃动为我们的类加上System.Object⽗类。
基元类型
编译器直接⽀持的数据类型称为基元类型(Primitive type),每种基元类型都对应FCL中的某⼀类型。如下表格所⽰每种基元类型与对应的FCL类型:
我们也可以简单理解成,编译器会⾃动在我们的每个源码⽂件中,加上以下using指令(using在此的作⽤是给类型起⼀个别名):
using int = System.Int32;
using string = System.String;
……
理解了这⼀点,我们应该能知道在我们的代码中,写int还是Int32,⽤string还是⽤String本质上是⼀样的!从下⾯两种不同的写法⽣成的IL代码来看,结果也是符合预计的。
我们还应该知道,C#中的int永远是代表32位整型,long永远是64位整型等。这⼀点和其他编程语⾔可
能是不⼀致的(⽐如C/C++可能会根据机器平台决定,⽐如int在16位机器上可能是16位,⽽在32位机器上可能是32位。对于C/C++本⼈早已记忆模糊,这⾥如果有错误,请指出!)。
引⽤类型、值类型
CLR⽀持两种类型:引⽤类型和值类型,它们的区别是在内存分配⽅式上的差异:引⽤类型是从托管堆上分配的;值类型是在线程栈上分配的。⽽CLR的垃圾回收是针对托管堆的,因此值类型不受垃圾回收器的控制。
在FCL中,所有称为“结构”(struct)的类型都是值类型,所有称为“类”(class)的类型都是引⽤类型。所有的Struct都直接派⽣⾃抽象类System.ValueType,⽽System.ValueType直接从System.Object派⽣。所有的枚举都直接从System.Enum派⽣,⽽后者⼜派⽣⾃System.ValueType,所以枚举也是值类型。由于CLR的单继承规则,所以我们在定义值类型时,不能指定基类型,但可以实现接⼝。同时从下图⽣成的IL也可以看出,值类型是隐式密封的(sealed),也就是说也不能从值类型派⽣。
虽然引⽤类型与值类型实质只是内存分配上的差异,但这种差异会导致两种类型在⾏为表现上有着明显不同,⽐如下⾯的例⼦:
struct ValType { public int x;}
class RefType { public int x;}
class Program
{
static void Main(string[] args)
{
ValType v1 = new ValType(); //在栈上分配内存
RefType r1 = new RefType(); //在堆上分配内存
v1.x = 2;
r1.x = 2;
//执⾏到这⾥,内存结构请见图1
Console.WriteLine(v1.x);  //2
Console.WriteLine(r1.x);  //2
ValType v2 = v1;    //在栈上分配内存(v2),并把v1栈的内容复制到v2
RefType r2 = r1;    //把r1的堆地址复制给r2
v2.x = 5;  //只改变v2栈的内容
r2.x = 5;  //由于r2和r1都引⽤同⼀个堆上的对象,改变r2也会改变r1
//执⾏到这⾥,内存结构请见图2
Console.WriteLine(v1.x);    //2
Console.WriteLine(r1.x);    //5 注意这⾥变成了r2修改后的值
Console.WriteLine(v2.x);    //5
Console.WriteLine(r2.x);    //5
Console.ReadKey();
}
}
⾸先我们定义⼀个⼀值类型与⼀个引⽤类型,内部都只有⼀个字段。⽤new操作符分配内存时,值类型v1的内存分配在了线程栈上,引⽤类型r1的内存分配在了托管堆上,在程序运⾏到第⼀次WriteLine输出时,看到的结果是⼀致的。但接下来声明两个新的对象并执⾏赋值时,这⾥的发⽣的事明显不同:虽然赋值操作都是拷贝线程栈上变量的内容,但由于值类型变量v1的栈内容就是ValType类型实例本⾝,⽽引⽤类型r1的栈内容是RefType对象实例在堆上的地址。所以赋值后的结果就是,v1和v2各保存了⼀份ValType类型实例,⽽r1和r2保存了同⼀块堆内存的地址。所以改变r2对象导致了r1对象的随同改变。下⾯是内存⽰意图:
图1
图2
虽然值类型实例不需要垃圾回收,但由于值类型在传递时,传递的是内容本⾝,所以并不适合将所⼀些实例较⼤的类型定义为值类型。实现上除⾮满⾜以下所有条件,否则不应该将⼀个类型声明为值类型。
没有更改其字段的成员,即该类型是不可变的。(建议所有字段为readonly)
类型不需要从其他任何类型继承。(值类型不能选择基类)
类型也不会派⽣出其他任何类型。(所有的值类型都是隐式密封sealed的)
实例较⼩(约<=16Byte)或较⼤但不作为⽅法实参传递,也不从⽅法返回。
值类型的装箱与拆箱
将值类型转换成⼀个引⽤类型的过程叫装箱,整个过程看起来是这样的:
1. 在托管堆中分配好内存,分配的内存量=值类型的各个字段所需的内存量+所有堆上对象都有的两个额外成员(类型对象指针和同步块
索引)所需的内存量。
2. 值类型的字段复制到新分配的内存。
3. 返回对象的地址。writeline输出数值变量
拆箱仅是获取⼀个指针的过程,该指针指向包含在⼀个对象中的原始值类型(数据字段)。虽然拆箱⽐装箱代价低,但实际在拆箱之后往往紧接着就是赋值操作(内存复制)。显然装箱和拆箱/复制会对
应⽤程序的速度与内存消耗上产⽣不利影响,所以应该了解到这⼀点,并尽量避免装箱和拆箱操作。那么什么时候会发⽣装箱和拆箱,最直观的⽅法就是看⽣成的IL代码(IL对应指令是分别是box与unbox),⽐如下⾯的例⼦:
⽰例中ArrayList的Add⽅法参数是Object类型,也就是说⼀个引⽤类型(在堆上分配的内存),当我们
传递int类型时,这⾥便会将int实例装箱,以返回⼀个堆上的地址。在将array[0]强制转型为int时,由于值类型int的对象是在线程栈上分配的,所以这⾥拆箱并紧接着发⽣赋值(内存复制)操作。同时为了对⽐,我加了引⽤类型的reference,可以看出引⽤类型是不会发⽣装箱与拆箱的。
那么如何避免(或减少)装箱与拆箱:
尽量使⽤泛型集合。
尽量将装箱与拆箱操作移到循环体之外。
定义⼀个⽅法如果可接收引⽤类型或值类型时,尽量不要将参数定义为object,可以考虑通过重载定义多个版本或定义泛型⽅法。
总结
本⽂只能算是对⾃⼰看书的⼀点⼩结,分享出来的⽬的⼀是希望如果对某些知识理解有误,能及时得到⼤家指正;同时如果您感觉这篇博⽂有⼀点⼩价值,那我的第⼆个⽬的也就达到了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。