类型基础

万物归宗:System.Object


  正如标题所述,所有的类型最终都派生自System.Object类,因此每个类型都存在一组最基本的方法。

System.Object的公共方法:

公共方法 说明
Equals 如果两个对象具有相同的值,就返回true
GetHashCode 返回对象的值的哈希码
ToString 默认返回类型的完整名称(但经常重写该方法来返回包含对象状态表示的String对象)
GetType 返回从Type派生的一个类型的实例,指出调用GetType的对象是什么类型

System.Object的受保护方法:

受保护方法 说明
MemberwiseClone 创建类型的新实例,并将新对象的实例字段设与this对象的实例字段完全一致
Finalize 在垃圾回收器判断对象应该作为垃圾被回收之后,在对象的内存被实际回收之前,会调用这个虚方法

CLR要求所有对象都由new操作符创建,以下是new操作符的作用:

  1. 计算类型及其所有基类型(直至System.Object类型)中定义的所有实例字段需要的字节数。
  2. 从托管堆中分配类型要求的字节数,从而分配对象的内存,分配的所有字节都设为零。
  3. 初始化对象的“类型对象指针”和“同步块索引”成员。
  4. 调用类型的实例构造器,传递在new调用中指定的实参。
  5. 返回指向新建对象的一个引用或指针。

## 扭转乾坤:类型转换

  开发人员经常需要将对象从一种类型转换到另一种类型。在C#中,可以执行以下几种类型转换:

  1. 隐式转换:一种类型安全的转换,不会导致数据丢失,因此不需要任何特殊的语法。例如从较小整数类型转换到较大整数类型;从派生类到基类的转换。
  2. 显示转换:显示转换需要强制转换运算符。在转换中可能丢失数据或转换失败。例如从数值精度较大的类型向精度较小的类型转换;从基类到派生类的转换。
  3. 用户自定义转换:可以进行一些自定义的方法使不具有基类和派生类关系的自定义类型进行转换。
  4. 使用帮助程序类转换:可以使用System.BitConverter类、System.Convert类和Parse方法对不兼容的类型进行转换。

下面的示例演示了隐式类型转换与显示类型转换:

class Program
{
    static void Main(string[] args)
    {
        double a = 3.14;
        Console.WriteLine(a);//output  3.14

        //Object是double类型的基类,隐式类型转换
        Object b = a;
        Console.WriteLine(b);//output  3.14

        //转换中会造成精度丢失,显示类型转换
        int c = (int)a;
        Console.WriteLine(c);//output  3

        Console.ReadLine();
    }
}

(int)、Convert.ToInt32()、int.Parse()以及int.TryParse()的区别

  1. (int)适合简单数据类型之间的转换,它只能从其他数字类型转换为int类型,如果用它来转换字符串,无法通过编译。

  2. 当参数是null时,Conver.ToInt32()返回0,int.Parse()会抛出异常,而int.TryParse()本身则返回false,通过out int result参数返回0。如下是Convert.ToInt32方法的源码:

     public static int ToInt32(String value) {
         if (value == null)
             return 0;
         return Int32.Parse(value, CultureInfo.CurrentCulture);
     }
    
  3. Convert.ToInt32()可以转换多种类型,而int.Parse()/TryParse()只能转换数字类型的字符串。

  4. Convert.ToInt32()在转换小数时,如果小数位是两个整数之间的数字,则返回二者之间的偶数。例如:Convert.ToInt32(4.5)是4,Convert.ToInt32(5.5)是6。而(int)方法直接返回整数位。

is和as

通常情况下,可以使用is操作符检查对象与指定类型是否兼容。

class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        bool isDog = animal is Dog;
        Console.WriteLine(isDog);	//False
        animal = new Dog();
        isDog = animal is Dog;
        Console.WriteLine(isDog);	//True

        Console.ReadLine();
    }

}

class Animal
{
  
}

class Dog : Animal
{
  
}

使用is操作符来避免转型时发生异常:

if (animal is Dog)
{
     Dog dog = (Dog)animal;
}

上述代码中,CLR实际检查了两次对象类型,is操作符首先核实animal对象是否兼容Dog类型。如果结果位True,在if语句内部转型时,CLR再次核实animal是否引用一个Dog,两次检查对性能造成一定的影响(CLR首先判断对象的实际类型,然后遍历继承层次结构,对每个基类型去核对指定的类型Dog)。为此,C#提供了as操作符,以简化代码的写法,同时提升程序的性能。

Dog dog = animal as Dog;
if (dog != null)
{
	
}

上述代码中,CLR核实animal是否兼容于Dog类型。如果是,as返回同一个对象的非null引用;如果不兼容,as返回null。if语句只需要判断dog对象是否为null,这个检查性能要比校验对象的类型快很多。


## 以简御繁:命名空间与程序集

  命名空间对相关的类型进行逻辑分组,开发人员可通过命名空间方便地定位类型。对于编译器而言,命名空间的作用就是为类型名称加以句点分隔的符号,使名称更长,更具唯一性。C#的using指令指示编译器尝试为类型名称附加不同的前缀,直至找到匹配项。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

  以上是我们在创建控制台应用程序时,编译器自动引用的命名空间。如果编译器在源代码文件或引用的任何程序集中找不到具有指定的名称类型,就会在类型名称前附加System前缀,检查这样生成的名称是否与现有类型匹配。如果任然找不到匹配项,就继续为类型名称附加System.Collections.Generic前缀。这样在using指令的帮助下,不仅能极大地减少打字,还增强了代码的可读性。

命名空间和程序集的关系

  命名空间和程序集不一定相关。同一个命名空间中的类型可能在不同程序集中实现,同一个程序集也可能包含不同命名空间中的类型。

襟江带湖:运行时的相互关系


  本节将讲述类型、对象、线程栈和托管堆在运行时的相互关系。首先介绍两个简单的概念:

  • 序幕(prologue)代码:在方法开始工作前对其进行初始化(序幕代码在线程栈中为变量分配内存)。
  • 尾声(epilogue)代码:在方法做完工作后对其进行清理,以便返回至调用者。

下图展示了线程执行的代码调用方法的过程。

执行步骤说明:

  1. MethodA的序幕代码在线程栈上分配局部变量x(I)。
  2. 调用MethodB方法,将局部变量x作为参数,MethodB方法内部使用参数变量i标识栈位置(II)。
  3. 调用MehthodB时会将返回地址压入栈(III),待MethodB方法执行结束后返回至该位置。
  4. MethodB的序幕代码在线程栈中为局部变量y分配内存(IV)。
  5. MehtodB方法执行完毕后,CPU指令指针被设置成栈中的返回地址,MehtodB的栈帧展开,恢复到只有I的状态。
  6. MehtodA执行完毕返回给调用者。

以上示例的方法中仅包含变量的声明,下面,通过一个比较复杂的示例深入讨论CLR的工作方式。创建Animal类及派生类Dog(这里请忽略方法实现,仅给出返回结果用以说明)。

class Animal
{
    public int GetAge()
    {
        return 5;
    }

    public virtual string Cry()
    {
        return "aaaa";
    }

    public static string Eat ()
    {
        return "Delicious!";
    }
}

class Dog : Animal
{
    public override string Cry()
    {
        return "wang!";
    }
}

Windows进程启动,CLR加载到其中,托管堆初始化,创建一个线程(包含它的1MB栈空间),执行MethodC方法。

执行步骤说明:

  1. JIT编译器将MethodC的IL代码转换成本机CPU指令时,CLR要确认方法内部引用的类的所有程序集都已加载,利用程序集的元数据,CLR提取信息并创建数据结构来表示类型本身。如图中Animal类型对象(III)和Dog类型对象(IV),事实上这里还应包括String和Int的类型对象,这里就不做描述了。

    • 堆上所有对象都包括两个成员:类型对象指针(type object pointer)和同步块索引(sync block index)。

    • 为静态数据字段提供支援的字节在类型对象自身中分配。

    • 每个类型对象都包含一个方法表,记录类型定义的每个方法。

  2. 当CLR确认方法需要的类型对象都已创建,MethodC的代码编译之后,就允许线程执行MehtodC的本机代码了。MethodC的序幕代码在线程栈中为局部变量分配内存并将所有的局部变量初始化为null或0。

  3. MethodC执行代码构造一个Dog对象,使得在托管堆创建Dog类型的一个实例(VI)。new操作符返回Dog对象的内存地址,保存到变量animal中(标注为3的箭头)。

    • 任何时候在堆上新建对象,CLR都自动初始化内部的类型对象指针成员来引用和对象对应的类型对象。。

    • 在调用类型的构造器之前,CLR会先初始化同步块索引,并将对象的所有实例字段都设为0或null。

  4. MethodC的下一行代码调用Animal类型的实例方法GetAge,调用实例方法时,JIT编译器会找到变量animal的类型Animal对应的类型对象,如果类型对象上没有找到该方法,就追溯类的层次结构,并在沿途的每个类中查找该方法。当JIT编译器在类型对象的方法表中找到了被调用方法的记录项,就对方法进行JIT编译,再调用JIT编译好的代码。最后,将GetAge方法返回的整数5保存到age变量当中。

  5. 下一行代码调用Animal的静态方法Eat。调用静态方法时,CLR会定位与定义静态方法的类型对应的类型对象,JIT编译器在类型对象的方法表中查找与被调用方法对应的记录项。对方法进行JIT编译,执行编译好的代码,并将结果保存在变量eat中。

  6. 下一行代码构造了一个Animal对象,返回对象的地址并保存到局部变量animal当中(标注为6的箭头),animal不再引用dog对象。由于没有变量引用该对象,垃圾回收机制会自动回收该对象占用的内存。

  7. 最后,调用animal对象的实例方法Cry,并将结果保存到cry变量中。

补充:Animal和Dog类型对象都包含类型对象指针是因为类型对象本身也是对象。CLR开始在一个进程中运行时,会为MSCorLib.dll中定义的System.Type类型创建一个特殊的类型对象,Animal和Dog类型对象都是这个类型的实例,它们的类型对象指针会初始化为System.Type类型对象的引用。而System.Type类型对象的类型对象指针指向自己。

posted @ 2017-08-19 21:17  Answer.Geng  阅读(490)  评论(1编辑  收藏  举报