基元类型、引用类型和值类型
基元类型
编译器直接支持的数据类型称为基元类型(primitive type)。基元类型直接映射到Framework类库(FCL)中存在的类型。
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字符 |
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,dynamic和object完全一致。但C#编译器允许使用简单的语法让dynamic变量参与动态调度 |
从某个角度来说,可以认为C#编译器自动为所有源代码文件添加using指令为类型创建别名。
using int = System.Int32;
说到这里应该就可以理解String和string的关系了,C#的string(关键字)直接映射到System.String(类型),所以两者实际上没有区别。
溢出检查
对基元类型执行算术运算时可能造成溢出。不同语言处理溢出的方式不同,C/C++不将溢出视为错误,允许值回滚;Microsoft Visual Basic则将溢出视为错误,并在检测到溢出时抛出异常。而C#允许程序员自己决定如何处理溢出,溢出检查默认关闭。下面介绍两种方式打开或关闭溢出检查:
-
特定区域控制溢出检查
C#通过checked和unchecked操作符在代码的特定区域进行溢出检查。
byte b = 100; b = checked((byte)(b + 200)); //抛出OverflowException异常
C#也支持checked和unchecked语句块,对语句块中的所有语句都进行或不进行溢出检查
checked { byte b = 100; b = (byte)(b + 200); }
-
全局性控制溢出检查
只需打开编译器的/checked+开关,系统就会对没有显示标记checked或unchecked的代码进行溢出检查(当然这种情况下应用程序运行起来会慢一些)。要在Visual Studio中更改Checked设置,打开项目属性,按照以下步骤即可找到开关:Properties|Build|Advanced|Check for arithmetic overflow/underflow
##引用类型和值类型
CLR支持两种类型:值类型和引用类型。引用类型总是从托管堆分配,C#的new操作符返回指向对象的内存地址。使用引用类型存在一些性能问题,例如:
- 内存必须从托管堆分配。
- 堆上分配的每个对象都有一些额外成员(类型对象及同步快索引),这些成员必须初始化。
- 从托管堆分配对象时,可能强制执行一次垃圾回收。
为了提供简单和常用的类型的性能,CLR提供了值类型,值类型的实例一般在线程栈上分配。值类型实例的变量包含实例本身的字段,所以不包含实例的指针。值类型不受垃圾回收器的控制。因此,值类型的使用缓解了托管堆的压力,并减少了应用程序生存期内的垃圾回收次数。
当然,也不是所有的情况下值类型都可以提高程序性能,甚至在某些特定的情况下会对性能造成损害。例如当实参以值类型方式传递或方法返回一个值类型时,实例中的字段会复制到调用者分配的内存中,若实例较大,便会对应用程序的性能造成影响。
值类型的装箱和拆箱
许多时候,都需要获取对值类型实例的引用。首先,我们来看一段代码:
ArrayList arr = new ArrayList();
for (int i = 0; i < 10; i++)
{
arr.Add(i);
}
这段代码很简单,循环10次向ArrayList中添加Int类型变量i。现在我们考虑一下,ArrayList中储存的究竟是什么?要搞清楚这个问题,先看一下Add方法的源代码:
public virtual int Add(Object value) {
Contract.Ensures(Contract.Result<int>() >= 0);
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size] = value;
_version++;
return _size++;
}
注意这里的参数类型是Object类型,也就是说Add方法获取对托管堆上的一个对象的引用来作为参数。
所以,为了使代码正常工作,Int值类型必须转换成托管堆中的对象,而且要获得该对象的引用。将值类型转换为引用类型要使用装箱操作。以下,是对值类型的实例进行装箱操作的过程:
- 在托管堆上分配内存。包括值类型各字段所需的内存量,还要加上类型对象指针和同步块索引的内存量。
- 将值类型的字段复制到新分配的堆内存。
- 返回对象地址。
这时我们再回到开头的代码,C#编译器检测到向要求引用类型的方法传递值类型,所以自动生成代码对对象进行装箱操作。Int值类型实例i的字段复制到新分配的Int对象中,并将地址返回给Add方法。
Int.Tostring()方法不进行装箱操作
以下是官方文档中对装箱的描述,Int.ToString()方法显然不满足装箱的条件。
装箱用于在垃圾回收堆中存储值类型。 装箱是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。 对值类型装箱会在堆中分配一个对象实例,并将该值复制到新的对象中。 ——装箱和取消装箱
了解了装箱操作后,接着谈谈拆箱,假设要获取ArrayList中的第一个元素。
int x = (int)arr[0];
将Int对象中的字段复制到值类型变量x中,后者在线程栈上。CLR分两步完成:
- 获取已装箱的Int对象的地址。这个过程称为拆箱。
- 将字段包含的值复制到线程栈的值类型实例中。
注意:对对象进行拆箱时,只能转型为最初未装箱的值类型。
Int32 x = 5;
object o = x;
//Int16 y = (Int16)o; //System.InvalidCastException
Int16 y = (Int16)(Int32)o; //OK
##Dynamic基元类型
在大多数情况下,dynamic类型与object类型的行为类似。如果操作包括dynamic类型的表达式,那么不会通过编译器对该操作进行解析或类型检查。编译器将有关操作信息打包在一起,之后这些信息会用于在运行时评估操作。在此过程中,dynamic类型的变量会编译为object类型的变量。因此,dynamic类型只在编译时存在,在运行时则不存在。
请看以下示例代码:
dynamic d;
int x = 5;
d = x + x;
Console.WriteLine(d); //10
string s = "5";
d = s + s;
Console.WriteLine(d); //"55"
注意,所有的表达式都能隐式转型为dynamic,因为所有表达式最终都生成从Object派生的类型。正常情况下,编译器不允许将表达式从Object类型隐式转换为其他类型,但编译器允许使用隐式转型将表达式从dynamic转型为其他类型。
object o = 10;
//int i = o; //Error:Cannot implicitly convert type 'object' to 'int'.
int i = (int)o; //ok
dynamic d = 10;
int j = d; //ok
虽然使用动态功能可以简化语法,但也要看是否值得。因为使用dynamic关键字生成的payload代码使用到Microsoft.CSharp.dll程序集,在运行时,此程序集必须加载到AppDomain中,这会损害应用程序的性能,增大内存消耗。Microsoft.CSharp.dll还会加载System.dll和System.Core.dll。若使用dynamic与COM组件交互,还会加载System.Dynamic.dll。加载这些程序集以及额外的内存消耗,会对性能造成影响。