类型转换
装箱和拆箱
MSDN中的定义:
装箱是将值类型转换为 object 类型或由此值类型实现的任一接口类型的过程。 当 CLR 对值类型进行装箱时,会将该值包装到 System.Object 内部,再将后者存储在托管堆上。 取消装箱将从对象中提取值类型。 装箱是隐式的;取消装箱是显式的。 装箱和取消装箱的概念是类型系统 C# 统一视图的基础,其中任一类型的值都被视为一个对象
装箱
1. 托管堆分配内存并创建一个新对象
2. 把值类型的值复制到对象中
3. 返回指向对象的引用(指针),引用存储在线程栈中
装箱需要分配内存并新建对象占性能
拆箱
1. 将托管堆的对象的值复制到线程栈中
拆箱需要显示转换
ToString()函数不涉及装箱,通过查看IL代码可以证明,因为ToString被重写,不符合装箱的定义。
类型转换的方法
类型转换的方法很多:
1. 隐式转换 适用于安全的转换(不会造成信息丢失),例如从派生类转换为基类,较小整数类型转换为较大整数类型,低精度类型转换为高精度类型
2. 显示转换 一般适用于不安全的转换
3. 类的函数Convert.ToInt32,Int32.Parse以及Int32.TryParse(string, out int)函数
4. 用户自定义函数,用户可以重载隐式转换和显示转换操作符或者自定义函数来进行类型的转换
public static explicit operator int(float f); float显示转换(强制转换)为int类型
public static implicit operator float(int i); int隐式转换为float类型
public void Test(object o)
{
Manager m = (Manager)o;
}
如果传入一个不相干的对象,在编译时期不能检测出来,只有在运行时才抛出FormatException。因此,在写函数参数时最好要定义为object类型。
下面介绍3个我们常用的三个函数的区别,由3点体现出来
Int32.Parse(string): 1. 当参数为null,抛出ArgumentNullException 2. 当参数为 ”非整数的字符串“(比如"2.2”, “abcZ”)时,抛出FormatException 3. 当参数的整数值上下溢出,抛出OverflowException
Convert.ToInt32(string): 1. 返回0 2.抛出FormatException 3. 抛出OverflowException
bool Int32.TryParse(string, out int): 在1,2,3情况下. out 值为0,返回的bool值为false,不抛出异常
运行时的关系
上图是CLR VIA C# 中的一幅图,描述调用函数M3托管堆和线程堆的状态
调用M3的一些托管堆的变化:
1. 调用M3,JIT编译器将代码编译IL(如果需要的话),并且添加所需的类型对象(Type Object),M3中有Employee、Manager、String类型对象,String类型应该早就添加了,这儿只画了Manager和Employee类型对象。其中派生类的有指针指向基类。如果是虚函数,那么会额外添加一些信息描述。
2. Employee e; 初始化为null
3. e = new Manager(); 需要创建一个Manager对象(object),所有的对象都有类型对象指针和同步块索引。初始化类型对象指针,指向类型对象。现在有一个问题,类型对象的类型对象指针只想哪儿呢?指向Type类的类型对象,而Type类型对象的类型对象指针指向自己,因此,当调用GetType()函数的时候可以获知对象的真类型。
线程栈
当一个函数M1调用函数M2时
-1. 将函数M2的函数参数入栈,因为需要使用M1的局部变量或者其他
0. call
1. push ebp; 保存M1的基址
2. mov ebp esp; 修改ebp保存的是M2的基址
3. M2的局部变量入栈
4. mov esp ebp; 修改esp指向的地址
5. pop ebp; 恢复M1的基址
6. ret
编译器可能会进行优化,将局部变量或者函数参数存放到寄存器中,但是执行思想是不变的。
线程栈是从高地址像低地址发展,而堆栈是从低地址像高地址发展。