基元类型、引用类型和值类型

基元类型


  编译器直接支持的数据类型称为基元类型(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#允许程序员自己决定如何处理溢出,溢出检查默认关闭。下面介绍两种方式打开或关闭溢出检查:

  1. 特定区域控制溢出检查

    C#通过checked和unchecked操作符在代码的特定区域进行溢出检查。

     byte b = 100;
     b = checked((byte)(b + 200)); //抛出OverflowException异常
    

    C#也支持checked和unchecked语句块,对语句块中的所有语句都进行或不进行溢出检查

     checked
     {
         byte b = 100;
         b = (byte)(b + 200);
     }
    
  2. 全局性控制溢出检查

    只需打开编译器的/checked+开关,系统就会对没有显示标记checked或unchecked的代码进行溢出检查(当然这种情况下应用程序运行起来会慢一些)。要在Visual Studio中更改Checked设置,打开项目属性,按照以下步骤即可找到开关:Properties|Build|Advanced|Check for arithmetic overflow/underflow


##引用类型和值类型

  CLR支持两种类型:值类型和引用类型。引用类型总是从托管堆分配,C#的new操作符返回指向对象的内存地址。使用引用类型存在一些性能问题,例如:

  1. 内存必须从托管堆分配。
  2. 堆上分配的每个对象都有一些额外成员(类型对象及同步快索引),这些成员必须初始化。
  3. 从托管堆分配对象时,可能强制执行一次垃圾回收。

  为了提供简单和常用的类型的性能,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值类型必须转换成托管堆中的对象,而且要获得该对象的引用。将值类型转换为引用类型要使用装箱操作。以下,是对值类型的实例进行装箱操作的过程:

  1. 在托管堆上分配内存。包括值类型各字段所需的内存量,还要加上类型对象指针和同步块索引的内存量。
  2. 将值类型的字段复制到新分配的堆内存。
  3. 返回对象地址。

这时我们再回到开头的代码,C#编译器检测到向要求引用类型的方法传递值类型,所以自动生成代码对对象进行装箱操作。Int值类型实例i的字段复制到新分配的Int对象中,并将地址返回给Add方法。

Int.Tostring()方法不进行装箱操作

以下是官方文档中对装箱的描述,Int.ToString()方法显然不满足装箱的条件。

装箱用于在垃圾回收堆中存储值类型。 装箱是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。 对值类型装箱会在堆中分配一个对象实例,并将该值复制到新的对象中。 ——装箱和取消装箱

了解了装箱操作后,接着谈谈拆箱,假设要获取ArrayList中的第一个元素。

int x = (int)arr[0];

将Int对象中的字段复制到值类型变量x中,后者在线程栈上。CLR分两步完成:

  1. 获取已装箱的Int对象的地址。这个过程称为拆箱
  2. 将字段包含的值复制到线程栈的值类型实例中。

注意:对对象进行拆箱时,只能转型为最初未装箱的值类型

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。加载这些程序集以及额外的内存消耗,会对性能造成影响。

posted @ 2017-08-23 19:54  Answer.Geng  阅读(771)  评论(0编辑  收藏  举报