例如分配一个整数:
System.Int32 a = new System.Int32();
优化:
int a = 0;
以上两种分配整数类型,都生成同样的 IL 代码,编译直接支持的数据类型称为基元类型(primitive type)。
基元类型和 .NET 框架类库(FCL)中的类型有直接的映射关系。例如,在 C# 中,int 直接映射为 System.Int32 类型。
以下4行代码,产生同样的 IL 代码:
int a = 0; //最便捷的语法
System.Int32 a = 0; //轻便捷的语法
int a = new int(); //不便捷的语法
System.Int32 a = new System.Int32(); //不便捷的语法
表 5.1 显示了 FCL 中的类型和它们在 C# 中对应的基元类型。
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 字符(不像非托管 C++ 中那样,char 表示的是一个 8 位值) |
float | System.Single | ✔ | IEEE 32位浮点数 |
double | System.Double | ✔ | IEEE 64位浮点数 |
bool | System.Boolean | ✔ | 一个 True 或者 Flase 值 |
decimal | System.Decimal | ✔ | 128 位高精度浮点值,通常用于不容许有舍入误差的金融计算场合。在这 128 位中,1 位表示浮点值的符号,96 位表示浮点值本身。8位表示用96位值除以浮点值所得结果的10的幂次(幂次范围为0~28)。其余的位尚未使用 |
object | System.Object | ✔ | 所有类型的基类型 |
string | System.String | ✔ | 一个字符数组 |
本书作者提倡编译器不要提供基元类型名称,并强制开发人员使用 FCL 类型名,理由如下:
-
比如在 C# 中的 string 实际上映射了 System.String(FCL 类型)。
-
long 映射为 System.Int64。在其他的编程语言中,long 可能映射为一个 Int16 或者 Int32。托管扩展 C++将 long 看作是一个 Int32。
-
FCL 中有很多类型的方法都将一些类型名作为方法名称的一部分。例如,BinaryReader 类型就提供了诸如 ReadBoolean、ReadInt32、ReadSingle 之类的方法;而 System.Convert 类型也提供了诸如 ToBoolean、ToInt32、ToSingle 之类的方法。
比如以下代码:
BinaryReader br =new BinaryReader(...);
float val = br.ReaderSingle();//正确,但是有些不自然
Single val = br.ReaderSingle();//正确,并且感觉也很自然
隐式转型:
Int32 i = 5;
Int64 l = i; //隐式转型为一个64位的值
编译器一般会支持和类型转换、文本常量(literals)、操作符相关的一些模式。
编译器能够在基元类型之间进行隐式或者显示的转型:
Int32 i = 5; //从 Int32 到 Int32 的隐式转型
Int64 l =i; //从 Int32 到 Int64 的隐式转型
Single s = i; //从 Int32 到 Single 的隐式转型
Byte b = (Byte) i; //从 Int32 到 Byte 的显式转型
Int16 v = (Int16) s; //从 Single 到 Int16 的显式转型
显式转换: 如果转型存在潜在的”不安全“,所以需要显式转换。(丢失精度或者数量级)
基元类型以文本常量的形式出现:
Console.WriteLine(123.ToString() + 456.ToString()); //输出:"123456"
5.1.1 Checked 与 Unchecked 基元类型操作
基元类型的算术运算导致结果溢出:
Byte b= 100;
b = (Byte)(b + 200); //b 的值将为44
让C#编译器控制溢出的一种方法:使用 /checked+ 命令行开关。该命令行开关告诉编译器使用带溢出检查的加、减、乘以及转换 IL 指令来产生代码。使用这样的命令行开关将使代码的效率有所降低,这是因为 CLR 需要检查代码中是否会出现溢出。一旦发生溢出,CLR 便会一个 OverflowException 异常。
例如:
Byte b= 100;
b = checked((Byte) (b + 200) ); //抛出异常 OverflowException
b 和 200 转换为两个32位的值,加在一起为300。300又被转换为一个 Byte,则发出 OverflowException 异常。
如果执行 Byte 转型在 checked 操作符外面,将不会抛出异常:
b= (Byte) checked(b + 200); //b为44,不会抛出异常
C# 提供了 checked 和 unchecked 语句,使得语句可接受或者不可接受溢出检查:
//溢出检查
checked {
Byte b = 100;
b = (Byte) (b + 200);
}
//或者
checked{
Byte b = 100;
b += 200;
}
//若在 checked 块调用一个方法,该方法没有受到任何影响
checked{
SomeMethod();//不受任何影响,也对该方法内部进行溢出检查
}
checked 和 unchecked 时的一些推荐原则:
- 如果希望在出现溢出时抛出异常。则显式使用 checked。
- 即使出现了溢出,也不希望有异常抛出。显式使用 unchecked。
使用 /checked+命令行开关: 应用程序运行的速度一般比较慢,因为系统会对任何没有显式表示 checked 或 unchecked 的代码做溢出检查。如果有异常出现,会很容易检测到并及时修复。
使用 /checked-命令行开关: 发布出去的代码运行速度会比较快,而且一般不会产生异常。
5.2 引用类型与值类型
CLR 支持两种类型:引用类型和值类型。
在使用引用类型时:
- 内存必须从托管堆中分配。
- 每个在托管堆中分配的对象都有一些与之关联的额外附加成员必须被初始化。
- 从托管堆中分配对象可能会导致执行垃圾回收集。
值类型:
- 实例通常分配在线程的堆栈上。
- 值类型实例的变量不包含指向实例的指针——变量本身即包含了实例所有的字段。操作实例时无需再解析指针引用。
- 值类型实例不受垃圾收集器的控制,减少了托管堆的压力,以及应用程序在整个生存期中需要垃圾回收的次数。
引用类型: 任何被称为”类“的类型;
值类型: 枚举、结构。所有结构都直接继承自 System.ValueType 类型。由于 System.ValueType 类型直接继承自 System.Object 类型。所以所有值类型都必须继承自 System.ValueType 类型。
考虑将类型声明为值类型:
- 该类型的行为类似于基元类型。
- 该类型不需要继承自任何其他类型。
- 该类型的实例不会频繁地用于方法的参数传递。因为参数以传值的方式传递,会导致值类型实例中的字段被拷贝。
- 该类型的实例不会作为方法的结果频繁地返回。从方法中返回一个值类型也会导致实例中的字段被拷贝到调用者分配的内存中。
- 该类型的实例不会被频繁地用于诸如 ArrayList、Hashtable 之类的集合中。这些管理一组通用对象集合的类会对值类型实例执行装箱操作。导致额外的内存分配,以及额外的内存拷贝操作。
值类型的主要优势: 它们不被分配在托管堆上。(值类型实例在线程的堆上)
引用类型: 引用的对象被分配在托管堆上,而引用类型变量本身在托管栈上。
值类型和引用类型的差别:
-
值类型对象有两种表示:一种是未装箱(unboxed)形式。一种是装箱(boxed)形式。引用类型总是装箱形式。
-
值类型都继承自 System.ValueType。
-
值类型不能作为基类,不引入任何新的虚方法和抽象方法,所有方法都是密封方法。
-
引用类型变量包含着对象在托管堆中的内存地址,默认初始化为 NULL,为NULL会抛出 NullReferenceException 异常。值类型变量,所有字段都被初始化为0 值,不可能产生异常。
-
当将一个值类型变量赋值给另一个值类型变量时,会进行一个”字段对字段“的拷贝的拷贝。但当将一个引用类型变量赋值给另一个引用类型变量时,只会拷贝内存地址。
-
两个或多个引用类型变量可以指向托管堆中的同一个对象,改变这个对象时,会同时影响到引用这个对象的引用类型变量。而每个值类型都有一份自己的”对象“数据拷贝。
-
因为未装箱值类型没有分配在托管堆上,所以一旦定义该类型实例的方法不再处于活动状态,为它们分配的存储空间就会立即释放。(不在托管堆上,而是在线程的堆上,所有不受 CLR 控制)。这意味着值类型实例在内存被回收时不可能收到任何通知(通过 Finalize 方法)。
5.3 值类型的装箱与拆箱
struct Point
{
public Int32 x, y;
}
class Test
{
static void Main()
{
ArrayList a = new ArrayList();
Point p;
for(Int32 i = 0;i < 10; i++)
{
p.x = p.y = i;
//对值类型进行装箱并得到的引用添加到 ArrayList 上
a.Add(p);
}
}
}
Add 接受的参数为一个 Object,表示 Add 需要一个指向托管堆中对象的引用(或指针)。由于 Point 是值类型,必须将 Point 值类型实例转换为一个托管堆上的对象。
因此,以装箱的机制用来将一个值类型转换为一个引用类型。
装箱操作步骤:
- 1、从托管堆中为新生成的引用类型对象分配内存。分配内存的大小为值类型实例本身的大小,加上其他额外的将该值类型实例视为真正的引用对象所需的空间。这些额外的空间包括一个方法表指针和一个SyncBlockIndex。
- 将值类型实例的字段拷贝到托管堆上新分配对象的内存中。
- 返回托管堆中新分配对象的地址。该地址就是一个指向对象的引用。值类型实例也就变成了一个引用类型对象。
注意:由于值类型转换为引用类型后,可以被重用或释放。值类型实例驻留在托管堆中直到最后被垃圾收集器回收。经过装箱的值类型实例的生存期超出了未装箱的值类型实例的生存期。
拆箱: 拆箱只需要进行字段拷贝,并不需要额外的空间拷贝。
一个引用类型的拆箱操作步骤:
- 1、如果该引用为 null,将会抛出一个 NullReferenceException 异常。
- 2、如果该引用指向的对象不是一个期望的值类型的已装箱对象,将会抛出一个 InvalidCastException 异常。
- 3、一个指向包含在已装箱对象中值类型部分的指针被返回。该指针指向的值类型对于引用类型对象通常所具有的附加成员(即一个方法表指针和一个SyncBlockIndex)一无所知。实际上,该指针指向的是已装箱对象中的未装箱部分。
拆箱异常:
static void Main(){
Int2 x = 5;
Object o = x; //装箱
Int16 y = (Int16) o; // 拆箱,抛出一个 InvalidCastException 异常
}
拆箱正常:
static void Main(){
Int32 x = 5;
Object o =x; // 装箱,0指向已装箱对象
Int16 y = (Int16)(Int32) o; //先拆箱为正确的类型,然后再转型
}
拆箱操作不会拷贝任何字段。(个人理解:因为装箱对象的字段在托管堆上,受 CLR 控制。而拆箱后为值类型是不受 CLR 控制的,所以必须拷贝该值类型的字段到线程的堆里)
static void Main(){
Point p;
p.x = p.y = 1;
Object o = p; //对p进行装箱,o指向已装箱对象
p = (Point) o;//对o拆箱,并将字段从托管堆拷贝到堆栈上。
}