1.1 编程语言的基元类型
下面给个C#基元类型与对应的FCL类型
表格:
C#基元类型
|
FCL类型
|
CLS相容
|
说明
|
sbyte | System.SByte | 否 | 有符号8位值 |
byte | System.Byte | 是 | 无符号8位值 |
short | System.Int16 | 是 | 有符号16位值 |
ushort | System.UInt16 | 否 | 无符号16位值 |
int | System.Int32 | 是 | 有符号32位值 |
uint | System.UInt32 | 否 | 无符号32位值 |
long | System.Int64 | 是 | 有符号64位值 |
ulong | System.UInt64 | 否 | 无符号64位值 |
char | System.Char | 是 | 16位Unicode字符 |
float | System.Single | 是 | IEEE32位浮点值 |
double | System.Double | 是 | IEEE64位浮点值 |
bool | System.Boolean | 是 | 一个true/false值 |
decimal | System.Decimal | 是 | 一个128位高精度浮点值(常用于金融计算) |
string | System.String | 是 | 一个字符数组 |
object | System.Object | 是 | 所有类型的基类型 |
dynamic | System.Object | 是 | 对于CLR,和object完全一致 |
讨论点:使用FCL类型还是用基元类型
C#语言规范称:从风格上说,最好使用关键字(基元类型)。
作者赞同:我情愿使用FCL类型名称,完全避免使用基元类型名称。
作者的理由:
1.例如:有些程序员认为,int在32位的机器上代表32位整数,在64位的机器上代表64位整数。其实呢这是跟操作系统无关的,int始终映射到System.Int32,如果使用Int32一目了然,就不会产生那样的误解了。
2.例如:在C#中,long映射到System.Int64,而在C++中long视为一个Int32,甚至大多数语言都不将long看作一个关键字。
--其实我也是赞同作者的观点。
checked和unchecked基元类型操作
都是溢出给逼得,控制溢出的一个办法使用/checked+编译器开关,如果发生溢出,抛出OverflowException异常
除了全局性的打开和关闭溢出检查,C#提供了checked和unchecked来对特定代码溢出检查,类似于try catch块。
作者建议:
1.尽量使用有符号数值类型代替无符号数值类型(无符号数值类型是CLS不相容的)
2.如果可能会发生不希望的溢出,把这些可能发生的代码放在checked块中
1.2 引用类型和值类型
CLR支持的两种类型:引用类型和值类型
在使用引用类型时必须注意到一下问题:
1.内存必须从托管堆中分配
2.在堆上分配的每个对象都有些额外的成员,这些成员必须初始化
3.对象的其他字节(为字段而设)总是设为零
4.从托管堆上分配一个对象时,可能强制执行一次垃圾回收操作
一下演示值类型与引用类型的区别:
//引用类型 class SomeRef { public Int32 x; } //值类型 struct SomeVal { public Int32 x;} static void ValueTypeDemo() { SomeRef r1 = new SomeRef();//在堆上分配 SomeVal v1 = new SomeVal();//在栈上分配 r1.x = 5; //提领指针 v1.x = 5; //在栈上修改 Console.WriteLine(r1.x); //显示5 Console.WriteLine(v1.x); //同样显示5 SomeRef r2 = r1; //只复制引用指针 SomeVal v2 = v1; //在栈上分配并复制成员 r1.x = 8; //r1.x和r2.x都会更变 v1.x = 9; //v1.x会更改,v2.x不变 }
1.3 值类型的装箱与拆箱
装箱与拆箱的概念太一般了,总之值类型与引用类型之间互相转换就会引发装箱与拆箱行为,而这个过程对性能有负面影响
例如:
void Method() { Int32 i=0; Object o=i; Int32 j=(Int32)o; }
理解一下上述代码在内存中是如何变化的
如图:
解释装箱操作时内部发生的事情:
1.在托管对中分配好内存,分配的内存量=值类型各个字段需要的内存量+托管堆中所有对象都有的两个额外成员(类型对象指针和同步索引块)需要的内存量
2.值类型的字段复制到新分配的堆内存。
3.返回对象的地址,现在这个地址是对一个对象的引用,值类型现在是个引用类型。
解释拆箱操作时内部发生的事情:
1.获取以装箱对象的各个字段的地址(如果“对已装箱值类型实例引用”的变量为null,就会抛出NullRefrenceException异常)
2.将这些字段包含的值从堆中复制到基于栈的值类型实例中
未装箱的值类型是比引用类型更“轻”的类型,归于以下两个原因:
1.它们不再托管堆上分配。
2.它们没有堆上的每个对象都有的额外成员,也就是类型对象指针与同步索引块。
通过以下例子来验证对值类型、装箱和拆箱的理解程度
//Point是个值类型 internal struct Point { private Int32 m_x,m_y; public Point(Int32 x,Int32 y) { m_x=x; m_y=y; } public void Change(Int32 x,Int32 y) { m_x=x; m_y=y; } public override string ToString() { return String.Format("({0},{1})", m_x, m_y); } } static void Main(string[] args) { Point p = new Point(1, 1); //调用WriteLine之前会对p进行装箱,WriteLine会在已装箱的Point上调用ToString Console.WriteLine(p);//显示(1,1) //该方法将P在栈上的m_x和m_y字段的值都该为2 p.Change(2, 2); //再次对p进行装箱,WriteLine会在已装箱的Point上调用ToString Console.WriteLine(p);//显示(2,2) //第三次装箱,将已装箱的p对象的引用赋予o Object o = p; Console.WriteLine(o);//显示(2,2) /*我们希望通过Change方法来更新已装箱的Point对象中的字段 * 首先转型为一个Point,就是对o拆箱, * 并将已装箱的Point中的字段复制到线程栈上的一个临时Point中 * 这个临时的Point的m_x和m_y字段会编程3和3 * 但已装箱的Point不受这个影响,所以会再次显示(2,2) */ ((Point)o).Change(3, 3); Console.WriteLine(o);//显示(2,2) } 现在的问题所在是,我们不能更改已装箱值类型中的字段,不过我们可以使用接口来欺骗C# //接口定义了一个Change方法 internal interface IChangeBoxedPoint { void Change(Int32 x, Int32 y); } //Point是个值类型 internal struct Point:IChangeBoxedPoint { private Int32 m_x,m_y; public Point(Int32 x,Int32 y) { m_x=x; m_y=y; } public void Change(Int32 x,Int32 y) { m_x=x; m_y=y; } public override string ToString() { return String.Format("({0},{1})", m_x, m_y); } } //接口方法如何修改一个已装箱的值类型中的字段 static void Main(string[] args) { //对p进行装箱,更改它已装箱的对象,然后丢弃它 ((IChangeBoxedPoint)p).Change(4, 4); Console.WriteLine(p); //显示(2,2) //更改已装箱的对象,并显示 ((IChangeBoxedPoint)o).Change(5, 5); Console.WriteLine(o);//显示(5,5) }
1.4对象哈希码
能将任何实例放到一个哈希表集合中
System.Object提供了虚方法GetHashCode,它能获取任意对象的Int32哈希码
如果一个类型重写了Equals方法,那么还应重写GetHashCode方法