基元类型:编译器直接支持的数据类型称为基元类型。
FCL类型:Framework类库中存在的类型。
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字符(char不像在非托管c++中那样代表一个8位值) |
float | System.Single | 是 | IEEE32位浮点值 |
double | System.Double | 是 | IEEE64位浮点值 |
bool | System.Boolean | 是 | true/false值 |
decimal | System.Decimal | 是 | 128位高精度浮点值,常用于不容许舍入误差的金融计算。128位中,1位是符号,96位是值本身(N),8位是比例因子(k).decimal实际值是±N*10^k,其中-28<=k<=0。其余位没有使用。 |
string | System.String | 是 | 字符数组 |
object | System.Object | 是 | 所有类型的基类型 |
dynamic | System.Object | 是 | 对于CLR,dynamic和object完全一致。但C#编译器允许使用简单的语法让dynamic变量参与动态调度。 |
CLR支持两种类型:引用类型和值类型。引用类型总是从托管堆分配,C#的new操作符返回对象内存地址——即指向对象数据的内存地址。
使用引用类型必须注意以下四点:
值类型一般在线程栈上分配(虽然也可作为字段嵌入引用类型的对象中)。 在代表值类型实例的变量中不包含指向实例的指针,相反,变量中包含了实例本身的字段。值类型实例不受垃圾回收器的控制。
所有值类型从System.ValueType中派生,ValueType本身又从System.Object中派生。所有值类型都隐式密封,防止将值类型用作其他引用类型或值类型的基类型。
根据前一章的叙述可理解引用类型对象在堆上开辟空间,其包含类型对象指针和同步索引块以及一些自身的实例字段,其引用的地址会存储在栈上,值类型对象会在栈上开辟空间,那么当我们对一个引用类型做了复制操作(即类似于Object o1;Object o2=o1,则此时o2是o1的复制)会在栈中复制出一个新的对象对其引用类型在堆上的内存引用,此时我们对o1的更改的同时o2也会改动,因为二者此时引用的是同一个东西。
相反,值类型对象复制后是两个不同的对象,二者之前不会有关联。
除非满足以下全部条件,否则不应将类型声明为值类型
因为实参默认以传值方式传递,返回也相同,都会对值类型实例中的字段进行复制,对性能造成损害,所以还必须满足以下条件
值类型变量赋值给另一个值类型变量,会执行逐字段的复制。引用类型变量赋值给另一个一引用类型的变量只复制内存地址。
简单的说值类型到引用类型就是装箱,反之为拆箱。
那么我们来深入了解一下
struct Point{
public Int32 x,y;
}
public sealed class Program{
public static void Main(){
ArrayList a = new ArrayList();
Point p;
for(Int32 i = 0; i<10;i++){
p.x=p.y=i;
a.Add(p);
}
...
}
}
在循环中可看到,add获取的是一个Object参数(获取托管堆上的一个对象引用或指针)来作为参数,但p是值类型,所以Point值类型在此必须转换成真正的,在堆中托管的对象,且必须获得该对象的引用。
装箱步骤:
拆箱并不是直接倒过来,代价会低很多。
eg: Point p =(Point) a[0];
我们通过此方式获取第一个元素的引用
public static void Main(){
Point p;
p.x=p.y=1;
Object o =p;
p=(Point)o;
p.x=2;
o=p;
}
由上述代码可直观的看出,后三行代码仅为更改p的x的值进行了拆箱再封箱的操作,其复制对应用程序性能的影响非常大。
public static Void Main(){
Int32 v = 5;
Object o = v;
v = 123;
Console.WriteLine(v+", "+(Int32)o);
}
可以看出上述代码进行过3次装箱么?第一次装箱在Object o =v;此时对v进行装箱将引用放到o中,第二和第三次在Console.WriteLine()中,v 此时进行装箱,将指针留在栈上以进行Concat(连接)操作。第三次在o进行拆箱后再次对Int32进行装箱,将指针保留在栈上进行Concat操作。
上述代码可简化为:
public static Void Main(){
Int32 v = 5;
Object o = v;
v = 123;
Console.WriteLine(v.ToString()+", "+o);
}
由于v.ToString方法会返回一个String类型对象,以及o本身就是Object类型对象,可避免后两次装箱及o对Int32的拆箱操作。
如果知道自己代码将会造成编译器反复对一个值类型进行装箱,可以改成手动方式进行装箱
public static void Main(){
Int32 v = 5;
Console.WriteLine("{0},{1},{2}",v,v,v);//这里对v进行了3次装箱
//上述代码完全可以更改为:
Int32 v = 5;
Object o = v;
Console.WriteLine("{0},{1},{2}",o,o,o);
}