类型从System.Object派生
C#中所有类型都是从System.Object派生的,可以显式派生,也可以隐式派生。System.Object的公共方法如下:
1 #region 程序集 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 2 // C:Program Files (x86)Reference AssembliesMicrosoftFramework.NETFrameworkv4.6.1mscorlib.dll 3 #endregion 4 5 using System.Runtime.ConstrainedExecution; 6 using System.Runtime.InteropServices; 7 using System.Runtime.Versioning; 8 using System.Security; 9 10 namespace System 11 { 12 // 13 // 摘要: 14 // 支持 .NET Framework 类层次结构中的所有类,并为派生类提供低级别服务。这是 .NET Framework 中所有类的最终基类;它是类型层次结构的根。若要浏览此类型的.NET 15 // Framework 源代码,请参阅 Reference Source。 16 [ClassInterface(ClassInterfaceType.AutoDual)] 17 [ComVisible(true)] 18 public class Object 19 { 20 // 21 // 摘要: 22 // 初始化 System.Object 类的新实例。 23 [NonVersionableAttribute] 24 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 25 public Object(); 26 27 // 28 // 摘要: 29 // 在垃圾回收将某一对象回收前允许该对象尝试释放资源并执行其他清理操作。 30 [NonVersionableAttribute] 31 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] 32 ~Object(); 33 34 // 35 // 摘要: 36 // 确定指定的对象实例是否被视为相等。 37 // 38 // 参数: 39 // objA: 40 // 要比较的第一个对象。 41 // 42 // objB: 43 // 要比较的第二个对象。 44 // 45 // 返回结果: 46 // 如果对象被视为相等,则为 true,否则为 false。如果 objA 和 objB 均为 null,此方法将返回 true。 47 public static bool Equals(Object objA, Object objB); 48 // 49 // 摘要: 50 // 确定指定的 System.Object 实例是否是相同的实例。 51 // 52 // 参数: 53 // objA: 54 // 要比较的第一个对象。 55 // 56 // objB: 57 // 要比较的第二个对象。 58 // 59 // 返回结果: 60 // 如果 objA 是与 objB 相同的实例,或如果两者均为 null,则为 true,否则为 false。 61 [NonVersionableAttribute] 62 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] 63 public static bool ReferenceEquals(Object objA, Object objB); 64 // 65 // 摘要: 66 // 确定指定的对象是否等于当前对象。 67 // 68 // 参数: 69 // obj: 70 // 要与当前对象进行比较的对象。 71 // 72 // 返回结果: 73 // 如果指定的对象等于当前对象,则为 true,否则为 false。 74 public virtual bool Equals(Object obj); 75 // 76 // 摘要: 77 // 作为默认哈希函数。 78 // 79 // 返回结果: 80 // 当前对象的哈希代码。 81 public virtual int GetHashCode(); 82 // 83 // 摘要: 84 // 获取当前实例的 System.Type。 85 // 86 // 返回结果: 87 // 当前实例的准确运行时类型。 88 [SecuritySafeCritical] 89 public Type GetType(); 90 // 91 // 摘要: 92 // 返回表示当前对象的字符串。 93 // 94 // 返回结果: 95 // 表示当前对象的字符串。 96 public virtual string ToString(); 97 // 98 // 摘要: 99 // 创建当前 System.Object 的浅表副本。 100 // 101 // 返回结果: 102 // 当前 System.Object 的浅表副本。 103 [SecuritySafeCritical] 104 protected Object MemberwiseClone(); 105 } 106 }
CLR要求所有对象都用new操作符创建。new操作符所做的事情有:
●计算类型和所有基类型(一直到System.Object)中定义的所有实例字段所需要的字节数。堆上的每一个对象都需要一些额外的开销成员,包括“类型对象指针”、“同步块索引”。CLR利用这些成员管理对象。这些额外开销成员的字节数要记入对象大小。
●从托管堆中分配类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0。
●初始化对象的“类型对象指针”和“同步块索引”成员。
●调用类型的实例构造器,传递在new中指定的实参。
new在执行了这些操作后,返回一个指向新建对象的一个引用或指针。在C#中,没有与new相对应的delete,而是依靠CLR的GC机制来进行垃圾回收。
类型转换
CLR的最重要特性之一就是类型安全。在运行的时候,CLR总是知道对象的类型是什么。调用GetType可以知道对象的确切类型。
CLR允许将对象转化为它的(实际)类型或者基类型。C#不要求任何特殊语法即可将对象转化为任何基类型,因为向基类型的转换被认为是安全的隐式转换。然而,将对象转化成某个派生类型的时候,C#要求开发人员只能进行显示转换,因为这种转换有可能在运行时失败。下面展示一下向基类型和派生类型的转换:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace Project_1 9 { 10 internal class Employee : System.Object { 11 } 12 public sealed class Program:System.Object 13 { 14 public static void Main(string[] args) 15 { 16 Object obj = new Employee(); 17 Employee e = (Employee)obj; 18 } 19 } 20 }
下面看看在运行时发生的事情:在运行时,CLR检查转型操作,确定总是转换为对象的实际类型或者它的任何基类型。比如下面代码虽然可以编译通过,但是会有异常:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace Project_1 9 { 10 internal class Employee : System.Object { 11 } 12 internal class Manager : Employee { 13 } 14 public sealed class Program:System.Object 15 { 16 public static void PromoteEmployee(Object o) { 17 Employee e = (Employee)o; 18 } 19 20 public static void Main(string[] args) 21 { 22 Manager m = new Manager(); 23 PromoteEmployee(m); 24 DateTime newYears = new DateTime(2019, 2, 20); 25 PromoteEmployee(newYears); 26 27 } 28 } 29 }
Main构造一个Manager对象并将其传给PromoteEmployee。之所以可以编译成功并运行,因为Manager最终从Object派生,而PromoteEmployee传入的正是一个Object。进入到PromoteEmployee内部之后,PromoteEmployee内部之后,CLR核实对象o引用的就是一个Employee对象,或者是从Employee派生的一个类型的对象。由于Manager从Employee派生,所以CLR执行类型转换,允许PromoteEmployee继续执行。
PromoteEmployee返回后,Main构造一个DateTime对象并传给PromoteEmployee。同样的,Datetime从Object派生,所以编译器会调用PromoteEmployee的代码。但是进入PromoteEmployee内部之后,CLR会检查类型转换,发现o引用的既不是Employee,也不是Employee的派生类型。因此CLR会禁止转型,抛出异常System.InvalidCastException。
因此,PromoteEmployee应该将参数类型指定为Employee,这样就可以在编译的时候报错了。
C#中进行类型转换的另一个方法是is操作符。is检查对象是否兼容于指定类型,返回Boolean。is操作符永远不会抛出异常。上面那个程序使用is操作符检查一下就可以避免抛出异常了:
public static void PromoteEmployee(Object o) { if (o is Employee) { Employee e = (Employee)o; } }
但是如果这种做,CLR实际上会检查两次对象类型。虽然增强了安全性,但是对性能产生了影响。因此,C#提供了as操作符来优化这种情况:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace Project_1 9 { 10 internal class Employee : System.Object { 11 } 12 internal class Manager : Employee { 13 } 14 public sealed class Program:System.Object 15 { 16 public static void PromoteEmployee(Object o) { 17 Employee e = o as Employee; 18 if (e != null) 19 { 20 System.Console.WriteLine(e.GetType()); 21 } 22 } 23 24 public static void Main(string[] args) 25 { 26 Manager m = new Manager(); 27 PromoteEmployee(m); 28 DateTime newYears = new DateTime(2019, 2, 20); 29 PromoteEmployee(newYears); 30 } 31 } 32 }
在这里,CLR检查o是否兼容于Employee类型。如果是,as返回对同一个对象的非null引用;如果不是,返回null。as操作符只检查一次对象类型,而if只检查e是否为null。
as的工作方式与强制类型转换一样,只是永远不抛出异常。如果对象不能转型,结果就是null。如果需要,请手动抛出异常。
命名空间和程序集
命名空间对相关的类型进行逻辑分组,开发人员可以通过命名空间定位类型:
public sealed class Program { static void Main(string[] args) { System.IO.FileStream fs = new System.IO.FileStream("text.txt", System.IO.FileMode.Open); System.Text.StringBuilder sb = new System.Text.StringBuilder(); }
这样子做太繁琐了,可以通过using指令简化代码:
using System.IO; using System.Text; namespace Program2 { public sealed class Program { static void Main(string[] args) { System.IO.FileStream fs = new FileStream("text.txt", System.IO.FileMode.Open); System.Text.StringBuilder sb = new StringBuilder(); } } }
对于编译器,命名空间的作用就是为类型名称附加以句点分隔的符号,使名称变得更长,更可能具备唯一性。C#的using指令指示编译器尝试为类型名添加不同前缀,直到找到匹配项。但是,CLR并不知道“命名空间”,在访问类型的时候,CLR需要知道完整的类型名称。
检查类型定义的时候,编译器必须知道要在什么程序集中检查。编译器扫描引用的所有程序集,在其中查找类型定义。一旦找到了正确的程序集,程序集信息和类型信息就嵌入生成的托管模块的元数据中。为了获取程序集信息,必须将定义了被引用类型的程序集传给编译器。C#编译器自动在MSCorLib.dll程序集中查找被引用的类型。MSCorLib.dll程序集包含所有核心Framework类库类型的定义。
因此有一个潜在问题:有可能有多个类型在不同的命名空间同名。这时,只能在开发中为类型定义提供唯一性的名称,或者使用完整的引用。
命名空间和程序集不一定相关:同一个命名空间的类型可能在不同程序集出现,同一个程序集也可能出现在不同的命名空间。在文档中查找类型的时候,会明确指出命名空间,以及所在程序集。
运行时的相互关系
这部分内容将解释类型、对象、线程栈和托管堆在运行时的相互关系。此外,还将解释调用静态方法、实例方法和虚方法的区别。
上图展示了已加载CLR的一个Windows进程。该进程可能有多个线程,线程创建时会分配到1MB的栈。栈空间用于向方法传递实参,方法内部定义的局部变量也在栈上。栈从高位内存地址向低位内存地址构建。现在线程已经执行了一些代码,栈顶已经有一些数据了。假定线程执行的代码要调用M1方法。
方法开始执行的时候,在线程栈上分配局部变量name的内存。
M1调用M2方法,将局部变量name作为实参传递。这就造成name局部变量中的地址被压入栈。M2方法内部使用参数变量s标识栈位置。此外,调用方法时还会将“返回地址”压入栈。被调用的方法在结束之后应返回值该位置。
M2方法开始执行的时候,在县城中为length和tally分配内存。然后,M2方法内部的代码开始执行。最终抵达return语句,造成CPU指令指针被设置成栈中的返回地址,M2的栈帧(栈帧,StackFrame,代表当前线程调用栈中的一个方法调用。执行线程的过程中,进行的每个方法调用都会在调用栈中创建并压入一个StackFrame)展开,恢复成下图的样子。之后,M1继续执行M2调用之后的代码,M1的栈帧将准确反映M1需要的状态。
最终,M1会返回到他的使用者。这同样通过将CPU的指令指针设置成返回地址来实现,M1的栈帧展开。之后,调用M1的方法继续执行M1调用之后的代码,那个方法的栈帧将准确反映它需要的状态。
现在,来看这组代码:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Program3 8 { 9 internal class Employee { 10 public Int32 GetYearsEmployed() { 11 return 10; 12 } 13 public virtual String GetProgressReport() { 14 return "Employee Haha"; 15 } 16 public static Employee Lookup(String name) { 17 Employee Joe = new Employee(); 18 return Joe; 19 } 20 } 21 internal sealed class Manager : Employee { 22 public override string GetProgressReport() 23 { 24 return "Manager Haha"; 25 } 26 } 27 28 class Program 29 { 30 31 static void Main(string[] args) 32 { 33 M3(); 34 } 35 36 private static void M3() 37 { 38 Employee e; 39 Int32 year; 40 e = new Manager(); 41 e = Employee.Lookup("Joe"); 42 year = e.GetYearsEmployed(); 43 System.Console.WriteLine(e.GetProgressReport()); 44 } 45 } 46 }
JIT编译器将M3的IL代码换成本机的CPU指令时,会注意到M3内部引用的所有类型。这时CLR要确认定义了这些类型的所有程序集已经加载。然后,利用程序集的元数据,CLR提取与这些类型有关的信息,创建一些数据结构来表示类型本身。下图展示了为Employee和Manager类型对象使用的数据结构。
堆上的所有对象都包含两个额外成员:类型对象指针和同步块索引。如图所示,Employee和Manager都有这两个成员。定义类型时,可在类型内部定义静态数据字段。为这些静态数据字段提供支援的字节在类型对象自身中分配。每个类型对象最后都包含一个方法表。在方法表中,类型定义的每个方法都有对应的记录项。
当CLR确认方法需要的所有类型对象都已创建,M3的代码已经编译之后,就允许线程执行M3的本机代码。M3的序幕代码执行时必须在线程栈中为局部变量分配内存,如下图所示:
CLR自动将所有的局部变量初始化为null或者0.然而,如果代码试图访问尚未显示初始化的局部变量,C#会报告错误消息。
然后,M3执行代码构造了一个Manager对象。这使得在托管堆创建Manager的一个实例