《CLR via C#》Chapter4 类型基础
第四章 类型基础
4.1 所有类型都从System.Object派生
GetHashCode() 与 Equals()
如果对象要在哈希表集合中用作键使用,那么应该重写GetHashCode方法;如果两个对象具有相同的值,那么Equals应该返回true。
Object类的Equals和GetHashCode具体实现都是internalcall,由CLR实现,Equals源码实现可参考以下:
// https://stackoverflow.com/questions/384294/where-is-the-implementation-of-internalequalsobject-obja-object-objb
// sscli20/clr/src/vm/comobject.cpp
// 不过这份实现很老了,简单参考即可
FCIMPL2(FC_BOOL_RET, ObjectNative::Equals, Object *pThisRef, Object *pCompareRef)
{
CONTRACTL
{
THROWS;
DISABLED(GC_NOTRIGGER);
INJECT_FAULT(FCThrow(kOutOfMemoryException););
MODE_COOPERATIVE;
SO_TOLERANT;
}
CONTRACTL_END;
if (pThisRef == pCompareRef) // 同一个引用类型对象实例
FC_RETURN_BOOL(TRUE);
// Since we are in FCALL, we must handle NULL specially.
if (pThisRef == NULL || pCompareRef == NULL)
FC_RETURN_BOOL(FALSE);
MethodTable *pThisMT = pThisRef->GetMethodTable();
// If it's not a value class, don't compare by value, <- 只有值类型的对象才进行值比较
if (!pThisMT->IsValueClass()) // 不同的引用类型对象,在这里结束,返回False
FC_RETURN_BOOL(FALSE);
// Make sure they are the same type.
if (pThisMT != pCompareRef->GetMethodTable())
FC_RETURN_BOOL(FALSE);
// Compare the contents (size - vtable - sink block index).
BOOL ret = memcmp(
(void *) (pThisRef+1),
(void *) (pCompareRef+1),
pThisRef->GetMethodTable()->GetBaseSize() - sizeof(Object) - sizeof(int)) == 0;
FC_GC_POLL_RET();
FC_RETURN_BOOL(ret);
}
FCIMPLEND
new操作符会做什么
所有对象都用new操作符创建,new操作会做的事情有:
- 计算类型及其所有基类型定义的所有实例字段(即非静态字段)需要的字节数。堆上的每个对象需要一些额外的成员,包括类型对象指针(type object pointer)和同步块索引(sync block index),CLR利用这些成员管理对象,额外成员的字节数也计入兑现大小
- 从托管堆中分配需求的字节数,分配的所有字节设为0
- 初始化对象的类型对象指针和同步块索引成员
- 调用类型的实例构造器,调用System.Object的构造器(此构造器什么都不做,简单返回)
4.2 类型转换
类型安全是CLR最重要的特性之一,运行时CLR总是知道对象的类型是什么
类型伪装是许多安全漏洞的根源,它还会破坏应用程序的稳定性和健壮性。因此,类型安全是CLR极其重要的一个特点。
C#不要求特殊语法 即可将对象转换为它的基类型,因为向基类型的转换被认为是安全隐式转换。然而转换为它的派生类型时,C#要求开发人员只能进行显示转换,因为转换可能在运行时失败。
注意,命名空间和程序集不一定相关。同一个命名空间中的类型可能在不同程序集中实现,同一个程序集也可能包含不同命名空间中的类型。
4.4 运行时的相互关系
[复习][计算机组成原理] 当每个线程创建完成后都被分配一定大小的栈空间,用于存放局部变量,方法间传递参数,栈空间从高地址向低地址增长。方法调用时,参数,以及被调用方法结束时的返回地址会被压入栈中
类型对象
前面提到每个堆上的对象都包括了类型对象指针(Type object ptr)和同步块索引(sync block index),类型对象(type object,即每个类型唯一的对象/实例)也是一样的
定义类型的静态成员时,用于存放这些静态内容的所有字节就存放在类型对象中
每个类型对象中还存放了方法表,方法表里记录了类型每一个方法的入口,需要补充的是这里并不区分静态方法和非静态方法。可以看到对于子类,方法表中仅有重写(override)了的方法。
CLR会在方法执行前保证该方法依赖的所有类对象均已创建完毕(但是对于非JIT是否也是方法执行前创建,这个需要确认下)
类型实例的创建与方法调用
[解惑] CLR会自动将局部变量赋值为null或0,但编译器会将“对未赋值局部变量的使用”进行报错处理
举例示例中的Manager对象,分配在堆上,同样拥有类型对象指针(Type object ptr)和同步块索引(sync block index),除此之外还有用于存放所有非静态字段/实例字段(instance data fields)的许多字节,这些字段当然包含了基类定义的字段
最后new操作符会返回Manager对象在堆上的内存地址,存放在局部变量e中(e分配在栈上)
静态方法的调用
当调用一个类型的静态方法时,CLR先根据类型找到类型对象,再从类型对象的方法表中找到对应的方法进行调用。
非静态方法的调用
当调用一个非虚方法(nonvirtual)时,CLR(原文为JIT Compiler)将会直接根据变量的类型定位到对应类型的类型对象;如果在方法表中没有找到这个方法,CLR将会查看类型继承结构直到Object来寻找这个方法。每个类型对象都有一个字段指向它的基类,此内容并没有绘制到图中。
当调用一个虚方法时(virtual),会多执行一些代码:根据变量的类型对象指针找到对象的准确类型,并在此类型对象的方法表中查找方法入口。
值得一提的是,对于上述Manager
和Employee
两个类型对象,它们的类型对象指针也是在初始化时赋值的,它们的类型是System.Type
,所以它们的类型对象指针指向的就是System.Type
这个类型的类型对象。
而System.Type
类型对象的类型对象指针指向它自己,毕竟这个对象也是一个System.Type
类型的实例,结构如下图。
最后回顾并复习一下,使用System.Object
的GetType()
方法可以返回存储在类型对象指针成员中的内容,从而确定任意对象的确切类型。