《CLR via C#:框架设计》读书笔记 - 类型基础

4.2 类型转换
返回

CLR最重要的特性之一就是类型安全性。在运行时,CLR总能知道一个对象时什么类型。由于GetType是非虚方法,所以一个类型不可能伪装成另一个类型,通过这个方法,可以知道对象的确切类型。

CLR允许将一个对象转换为它的(实际)类型或它的任何基类型。

使用C#is和as操作符

is 检查一个对象是否兼容于(实际类型或基类型)指定类型,并返回一个布尔值。

as 检查一个对象是否能强制转换成指定类型,若不可以,返回null

4.4 运行时相互关系
返回

本节将揭示类型、对象、线程栈和托管堆在运行时的相互关系。此外,还将解释调用静态方法、实例方法和虚方法的区别。

例1 展示了在调用方法时,线程栈是如何处理局部变量和参数的
返回

图1展示了已加载了CLR的一个进程。在这个进程中,可能有多个线程。一个线程创建时,会分配到1MB大小的栈。这个栈空间用于向方法传递实参,也用于方法内部的局部变量。在图中,线程已执行了一些代码,准备调用M1方法。

图1一个线程的栈,准备调用M1方法

图2 在线程栈上分配M1方法中的局部变量

图3 M1调用M2时,将实参和返回地址压入栈中

图4 在线程栈中分配M2的局部变量

在线程执行M2内部代码,最终抵达return语句,造成CPU指针被设置成栈中的返回地址,而且M2的栈帧(代表当前线程的调用栈的一个方法调用)会展开(unwind),使线程栈返回到M1调用M2之前的状态,如图2。同理M1调用完后,线程栈会返回到如图1的状态。

托管堆的内存分配机制【1】
引用类型的实例分配于托管堆上,而线程栈却是对象生命周期开始的地方。对32位处理器来说,应用程序完成进程初始化后,CLR将在进程的可用地址空间上分配一块保留的地址空间,它是进程(每个进程可使用4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内存,这块地址空间即是托管堆。

托管堆又根据存储信息的不同划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap),GC Heap用于存储对象实例,受GC管理;Loader Heap又分为High-Frequency Heap、Low-Frequency Heap和Stub Heap,不同的堆上又存储不同的信息。Loader Heap最重要的信息就是元数据相关的信息,也就是Type对象,每个Type在Loader Heap上体现为一个Method Table(方法表),而Method Table中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、所有的方法等等(此句与本书下面例2中的类型对象有矛盾,不过本书中的类型对象更易理解)。Loader Heap不受GC控制,其生命周期为从创建到AppDomain卸载。

例2 展示了在调用方法时,托管堆是如何工作的
返回

源代码:

复制代码
1 internal class Employee
2 {
3 public Int32 GetYearsEmploye() { return -1; }
4 public virtual String GenProcessReport() { return string.Empty; }
5 public static Employee LockUp(String name) { return null; }
6 }
7 internal class Manager : Employee
8 {
9 public override string GenProcessReport() { return base.GenProcessReport(); }
10 }
复制代码
注意区分堆中的类型对象和对象。

图5 CLR已加载到进程中,它的堆已初始化,一个线程的栈已创建,现在马山要调用M3

图6 Employee和Manager类型对象会在M3被调用创建

JIT何时创建类型对象?

当JIT编译器将M3的IL代码转换成本地CPU指令时,会注意到M3内部引用的所有类型:Employee,Int32,Manager以及String(”Joe”)。这个时候,CLR要确保定义了这些类型的所有程序集都已加载。然后,利用程序集的元数据,CLR提取与这些类型有关的信息,并创建一些数据结构来表示类型本身。在图6中,为了一目了然,我们在堆中只显示Emplyee和Manager,Int32和String类型对象很可能在调用M3之前就被创建了。

类型对象包括:

(1)类型对象指针(type object pointer)

(2)同步索引块(sync block index)

(3)静态数据字段

(4)方法表

图7 在线程栈上分配M3的局部变量

图8 分配并初始化一个Manager对象

CLR在堆中创建对象的步骤:

(1) 初始化类型对象指针,使之指向对应类型对象

(2) 初始化同步索引块

(3) 初始化实例字段(实例字段包括本身及其基类的实例字段)

(4) 调用类型的构造器(它本质上是可能修改某些实例数据字段的一个方法)。New操作法会返回对象内存地址,该地址保存在栈中的变量e中

图9 Employee的静态方法Lookup为Joe分配并初始化一个Manager对象

调用静态方法的步骤(为何如此复杂,参考8.6扩展方法):

(1) CLR会定位于定义静态方法的对应的类型对象。

(2) JIT编译器在类型对象的方法表中查找与被调用的方法对应的记录项,对方法进行JIT 编译(如有还没编译的话),在调用之。

假定Employee通过查询数据库查找名家“Joe”的employee,发现Joe是一个经理,所以在内部,Lookup方法在堆上构造了一个新的Manger对象。

注意,e不再引用第一个Manager对象。而且这个对象因没有变量引用,将会变成垃圾回收的主要目标。

图10 Employee的非虚实例方法GetYearsEmployed在调用后返回5

调用非虚实例方法的步骤:

(1) JIT编译器会找到与“发出调用的那个变量e的类型(Employee)”对应的类型对象(Employee类型对象)。

(2) 如果Emplyee类中没有定义正在调用的方法,JIT编译器会回溯类层次结构图(一直回溯到Object),查找调用的方法。之所以可以回溯,是因为每个类型对象都有一个字段引用了它的基本类型,这个信息图中没有显示。

图11 调用Employee的虚实例方法GenProgressReport,最终执行的是Manager重写的这个方法

调用的是虚实例方法的步骤:

(1) JIT要在方法中生成额外的代码,方法每次调用时,都会执行这些代码。

(2) 这些代码首先检查发出调用的变量,然后找到它所指的对象。在本例中e指向的是Manager对象。

(3) 然后找到对象指向的类型对象。在本例中Manager对象指向的是Manager类型对象。

(4) 若该类型对象方法表内的对应方法是非虚实例方法,调用之;若没找到对应的非虚实例方法,JIT编译器会回像调用非虚实例方法第二步一样回溯层次结构,直到找到匹配的方法。

注意:若Employee发现的Joe是个Employee,而不是Manager,Lookup会在堆内创建一个Employee对象,他的类型对象指针指向Employee类型对象。这样一来,最终执行的就是Eomployee的GenProgressReport实现,而不是Manager的GenProgressReport实现。

Emplyee和Manager类型对象都包含“类型对象指针“成员。这是由于类型对象本质上也是对象。CLR创建类型对象,必须初始化这些成员。那么如何初始化这些成员?

CLR开始在一个进程中运行时,会立即为MSCorLib.dll中定义的System.Type类型定义一个特殊的类型对象。Employee和Manager类型对象是该类型的“实例”。

当然System.Type类型对象本身也是一个对象,它比较特殊,她的“类型对象指针”指向它自己。

另外,System.Object的GetType方法返回的是对象所指向的类型对象。

复制代码
class Base
{
public string name = “Base”;
public static void StaticMethod()
{
Console.WriteLine(“Base static method.”);
}

    public  void NonStaticMethod()
    {
        Console.WriteLine("Base non static method.");
    }

    public virtual void VirtualMethod()
    {
        Console.WriteLine("Base virtual method.");
    }
}

class Derived : Base
{
    public new string name = "Derived";

    public new void NonStaticMethod()
    {
        Console.WriteLine("Derived non static method.");
    }

    public override void VirtualMethod()
    {
        Console.WriteLine("Derived virtual method.");
    }
}

复制代码

复制代码
static void Main(string[] args)
{
Base.StaticMethod(); //Base static method.
Derived.StaticMethod(); //Base static method.

        Base b=new Base();
        Derived d=new Derived();
        Base c = new Derived();

        b.NonStaticMethod();      //Base non static method.
        d.NonStaticMethod();      //Derived non static method.
        c.NonStaticMethod();      //Base non static method.

        b.VirtualMethod();        //Base virtual method.
        d.VirtualMethod();        //Derived virtual method.
        c.VirtualMethod();        //Derived virtual method.

        Console.WriteLine(b.name);//Base
        Console.WriteLine(d.name);//Derived
        Console.WriteLine(c.name);//Base

        Console.Read();
    }

复制代码

只有多态方法,没有多态实例字段【2】
从上述调用虚方法的得知,多态是如何实现的,即使变量e的类型是基类Employee,但只要它指向的对象是派生类Manager,且有覆盖虚方法的实例方法实现,就会调用Manager中的方法。

但实例字段并不会覆盖,见如下代码:

复制代码
1 class Employee
2 {
3 public string name = “Employee”;
4 }
5
6 class Manager : Employee
7 {
8 public string name = “Manger”;
9 }
10
11 class Program
12 {
13 static void Main(string[] args)
14 {
15 Employee e = new Manager();
16 Console.WriteLine(e.name); //显示:Employee
17 }
18 }
复制代码
上述代码,Employee e = new Manager(); 具体过程参见创建引用类型的实例的过程

小结:

在使用变量调用方法或字段时,只有调用的是虚实例方法,是根据变量指向的对象,再找指向对象指向的类型对象中找相应的方法(多态方法是这样实现的);
调用非虚实例方法或字段时,是直接根据变量的对象类型,找相应的方法或字段。

5.3 值类型的装箱和拆箱
返回

复制代码
1 struct Point
2 {
3 public Int32 x, y;
4 }
5 public static class Program
6 {
7 public static void Main()
8 {
9 System.Collections.ArrayList a = new System.Collections.ArrayList();
10 Point p;
11 p.x = p.y = 1;
12 a.Add(p); //对值类型进行装箱,因为Add方法原型:public virtual int Add(object value)
13 p = (Point)a[0]; //对已装箱的Point对象进行拆箱
14 }
15 }
复制代码
装箱过程:

(1) 在托管堆分配好内存。

(2) 把值类型的字段复制到新分配的堆内存

(3) 返回对象的地址。

拆箱过程:

(1) 获取已装箱的值对象的各个字段地址

(2) 把字段的值复制到基于栈的值类型字段中

值类型调用方法注意:

(1) 若值类型重写基类的虚方法,可以非虚的调用,因为值类型是隐式密封的,没有类从它们派生。

(2) 若要调用非虚的、继承的方法,那么在调用时,值类型实例会装箱,以便通过this【1】指针将对一个对象的引用传给基方法。

(3) 若将值类型的一个未装箱实例转型为类型的某个接口时,要求对实例进行装箱,因为接口变量必须包含对堆上对象的引用

5.3.2 对象的相等性和同一性
返回

System.Object类型提供了一个名为Equals的虚方法,基本实现如下

复制代码
1 public class Object
2 {
3 public virtual Boolean Equals(Object obj)
4 {
5 //若指向同一对象,返回true
6 if (this == obj) return true;
7 return false;
8 }
9 }
复制代码
以上代码实现的是同一性,而非相等性。而且以上实现也不合理,下面展示如何在内部正确实现一个Equals方法:

(1) 若Obj实参为null,就返回false,因为在调用非静态的Equals方法时,this所标识对象不可能为null。

(2) 若this和obj实参引用的是同一对象,返回true

(3) 若this和obj实参引用类型不同,返回false

(4) 若类型定义的每个实例字段,任何一个不相等,返回false

(5) 调用基类型的Equals方法,一般比较它定义的任何字段。

由于System.Object的虚方法Equals可以重写,所以不能调用它来测试同一性,因此,Object提供了一个静态方法ReferenceEquals,原型如下:

复制代码
1 public class Object
2 {
3 public static bool ReferenceEquals(object objA, object objB)
4 {
5 return (objA == objB);
6 }
7 }
复制代码
若想检查同一性,务必使用ReferenceEquals,而不是==操作符(除非先把两个操作数都转型为Object),因为==也可以被重载。

在System.ValueType(所有子类型基类)重写了Object.Equals方法,并进行了正确的实现来执行相等性测试,实现如下:

(1) 若obj实参为null,返回false

(2) 若this和obj实参引用不同类型对象,返回false

(3) 对每个实例字段进行比较,任何一个字段不等,返回false

(4) 返回true,ValueType的Equals方法不调用Object的Equals方法。

6.2 类型的可见性
返回

类有两种可见性:

(1) public,对所有程序集中的代码可见。

(2) internal,仅对定义它的程序集中的代码可见,对其它程序集中的代码不可见。

注:如果没有显式声明类的可见性,C#编译器默认设置为internal。

6.3 成员的可访问性
返回

表1 成员的可访问性

CLR术语

C#术语

描 述

Private

private

成员只能由定义该成员的类型中的方法或者该类型的所有嵌套类型中的方法访问

Family

protected

成员只能由定义该成员的类型中的方法、该类型的所有嵌套类型中的方法或者该类型的一个派生类型(与程序集无关)的方法访问

Family和

(不支持)

成员只能由定义该成员的类型中的方法、该类型的所有嵌套类型中的方法或者同一程序集中定义的该类型的所有派生类型中的方法访问

Assembly

Assembly

internal

成员只能由定义该成员的程序集中的方法访问

Family或

protected internal

成员可以由定义该成员的类型的所有嵌套类型、所有派生类型(与程序集无关)的方法或者定义该成员的程序集中的所有方法访问

Assembly

Public

public

成员可以由所有程序集的所有方法访问

6.6 组件、多态和版本控制
返回

表2 C#关键字及其对组件版本控制的影响

C#关键字

类 型

方法/属性/事件

常量/字段

abstract

表示该类型不能构建实例

表示在构建派生类型的实例之前派生类型必须重写并实现这个成员

(不允许)

virtual

(不允许)

表示这个成员可以由派生类型重写

(不允许)

override

(不允许)

表示派生类型重写了基础类型的成员

(不允许)

sealed

表示该类型不能用作基础类型

表示这个成员不能被派生类型重写,该关键字仅用于重写了虚方法的方法

(不允许)

new

应用于嵌套类型、方法、属性、事件、常量或者字段时,表示该成员与基类中类似的成员没有关系

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值