CTS (Common Type System)及公共类型系统,与C#的类型结构是极为相似的。.NET的C#就是CTS的一个子集。
这里我会从CLR与CLI的角度来思考类型转换。希望每一个人能更加理解 值类型与引用类型的区别,以及明白类型转换。
首先是从一段代码开始
namespace ClassCon
{
class Foo
{
public override string ToString()
{
return " F ";
}
}
class Program
{
static void Main( string[] args)
{
// 代码段1
Foo f= new Foo();
Object o= (Object) f;
f = (Foo)o;
// 代码段2
Object on= new object();
Foo fn = (Foo)on;
}
}
} //这个程序在代码段2里会出现一个InvalidCastException。
从Foo转到Object是泛化的转换,是类型安全的。 从Object到窄化的转换是可能抛出异常的。但是为什么会有时抛出异常,有时不会呢。从直观角度来看。代码段2的窄化转换肯定会抛出异常,而代码段1不会。这是因为我们有上下文,但是如果脱离上下文怎么判断。而且代码执行的时候它是不会利用这种上下文的。
前一篇文章会讲过值类型与引用类型的区别。值类型的实例就是值,而引用类型的实例是一个对值的引用,它是一个与机器相关的指针。32位机是4字节。其实还有别的重要区别。值类型没有类型信息,想要获得类型信息需要Box(装箱),而引用类型是有类型信息的。(这是ECMA-335 CLI标准提到的)但是这些类型信息是怎么管理的,我想是由CLR负责的,但是我却一直没弄明白到底CLR是怎么管理的。直到看到《.Net本质论 第1卷:公共语言运行库》里的对象头这个概念与一张图。
虽然微软没有公开资料表示有对象头。但是这东西应该是存在的。因为这很好的表示了4字节的引用类型实例为什么会有类型信息以及CLR怎么判断转换是安全的。
可是这图是有缺陷的,因为它没很好的解释类型转换,先在看看代码
Object o= (Object) f;
Console.WriteLine(o.GetType());
Console.WriteLine(f.ToString());
// 输出结果是:
// Class.Foo
// F
这个输出结果很好的直观表示了为什么窄化有时会成功有时会失败。因为泛化后我们依然有子类或实现接口的类的类型信息。
只要窄化的类可以从原来类型的泛化的话,我们的转换就会成功。 输出结果还表明了引用存储了两个类型信息。
一个是转换前的,还有一个是运行时的类型。前面一个是不会在转换中变得,它是创建对象是得到的。后一个就是转换的本质。它是动态的。
而前面的对象头并不能描述转换后的类型,所以我想到一个扩展。将htype 一分为二。一个是Satictype。它与上图的htype是一样的,对象创建之后就不会变了。这很好的说明了转换或类型还是不变的。还有一个RuntimeType(名字当然是随意的)。它最开始和Satictype指向是同一位置。但是转换之后。它就会指向转换目标类型的位置。转换到接口就会指向右图接口的htype。父类就会指向父类的htype。这样就很好的解释了子类型为什么有父类型的操作特点。
获得类型与类型转换的时候,CLR会使用Statictype里面的类型信息。当方法调用的时候它会使用RuntimeType里面的信息,会根据转换后的类型信息调用方法。 有了这个认识与理解,我们可以跳跃式类型转换。
namespace ClassCon
{
interface IFoo
{
void Print( string s);
}
class Foo:IFoo
{
public void Print( string s)
{
Console.WriteLine(s);
}
public override string ToString()
{
return " F ";
}
}
class Program
{
static void Main( string[] args)
{
Foo f= new Foo();
Object o= (Object) f;
Console.WriteLine(o.GetType());
Console.WriteLine(f.ToString());
((IFoo)o).Print( " String "); //在这里你以为o是Object类型,但是它却可以直接转换成IFoo类型。
}
}
}
这种认识感觉是十分清晰的,不错的收获。其实只要理解底层的话,所有的高级的技术就可以变得直观了。也很好的明白我们是怎么一路走过来的!
系列索引: