4.1 所有类型都从System.Object派生
“运行时”要求每个类型最终都从System.Object 类型派生,所以可以保证每个类型的每个对象都有一组最基本的方法。
重写(override):继承时发生,在子类中重新定义父类中的方法,子类中的方法和父类的方法是一样的(即方法名,参数,返回值都相同),由 override 声明重写的方法称为重写基方法。
例如:基类方法中声明为virtual,派生类中使用override申明此方法的重写。
重写override一般用于接口实现和继承类的方法改写,要注意:
v 覆盖的方法的标志必须要和被覆盖的方法的名字和参数完全匹配,才能达到覆盖的效果;
v 覆盖的方法的返回值必须和被覆盖的方法的返回一致;
v 覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
v 被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
v 不能重写非虚方法或静态方法。重写的基方法必须是 virtual、abstract 或 override 的。
namespace 方法重写 { class TestOverride { public class Employee { public string name; // Basepay is defined as protected, so that it may be accessed only by this class and derived classes. protected decimal basepay; // Constructor to set the name and basepay values. public Employee(string name, decimal basepay) { this.name = name; this.basepay = basepay; } // Declared virtual so it can be overridden. public virtual decimal CalculatePay() { return basepay; } } // Derive a new class from Employee. public class SalesEmployee : Employee { // New field that will affect the base pay. private decimal salesbonus; // The constructor calls the base-class version, and initializes the salesbonus field. public SalesEmployee(string name, decimal basepay, decimal salesbonus) : base(name, basepay) { this.salesbonus = salesbonus; } // Override the CalculatePay method to take bonus into account. public override decimal CalculatePay() { return basepay + salesbonus; } } static void Main() { // Create some new employees. SalesEmployee employee1 = new SalesEmployee("Alice", 1000, 500); Employee employee2 = new Employee("Bob", 1200); Console.WriteLine("Employee4 " + employee1.name + " earned: " + employee1.CalculatePay()); Console.WriteLine("Employee4 " + employee2.name + " earned: " + employee2.CalculatePay()); } } /* Output: Employee4 Alice earned: 1500 Employee4 Bob earned: 1200 */ }
System.Object的公共方法:
- Equals: 如果两个对象具有相同的值,就返回true。
- GetHashCode: 返回对象的值的一个哈希码。如果某个类型的对象要在一个哈希表集合中作为Key使用,该类型应该重写这个方法。
- ToString: 该方法默认返回类型的完整名称(this.GetType ().FullName)。然而,我们经常需要重写这个方法,使它返回一个String对象。
- GetType: 返回从Type派生的一个对象的实例,指出调用GetType的那个对象是什么类型。返回的Type对象可以和反射类配合使用,从而获取与对象的类型有关的元数据信息。GetType方法是非虚方法,这样可以防止一个类重写该方法,并隐瞒其类型,从而破坏类型安全性。
System.Object的受保护的方法:
- MemberwiseClone: 这个非虚方法能创建类型的一个新实例。
- Finalize: 在垃圾回收器判断对象应该被作为垃圾回收之后,在对象的内存被实际回收之前,会调用这个虚方法。简单的说,虚方法就是可以被子类重写(override)的方法,如果子类重写了虚方法,那么运行时将使用重写后的逻辑,如果没有重写,则使用父类中虚方法的逻辑。
内存格局通常分为四个区:
- 全局数据区:存放全局变量,静态数据,常量
- 代码区:存放所有的程序代码
- 栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等
- 堆区:即自由存储区
线程堆栈(Thread Stack)和托管堆(Managed Heap)
每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多个线程(thread),每个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,比如函数中定义的局部变量、函数调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉,主要由操作系统管理。所有值类型的变量都是在线程堆栈中分配的。
另一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)。
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
CLR要求所有类型对象都用new操作符来创建:Employee emp = new Employee (“ConstructorParam1”);
用new 关键字创建类的对象时,分配给对象的内存单元就位于托管堆中。在程序中我们可以随意地使用new 关键字创建多个对象,因此,托管堆中的内存资源是可以动态申请并使用的,当然用完了必须归还。
声明一个Employee的引用emp,在线程堆栈上给这个引用分配存储空间,这仅仅只是一个引用,不是实际的Employee对象。假定emp占4个字节的空间,包含了存储Employee的引用地址。接着分配托管堆上的内存来存储Employee对象的实例,假定Employee对象的实例是32字节,为了在托管堆上找到一个存储Employee对象的存储位置,.Net运行库在托管堆中搜索第一个从未使用的,32字节的连续块来存储Employee对象的实例,然后把分配给Employee对象实例的地址赋给emp变量。new执行了以上所有这些操作之后,会返回指向新建对象的一个引用。在前面的示例代码中,这个引用会保存到变量emp中。
以下是new操作符所做的事情:
- 它计算类型中定义的所有实例字段需要的字节数。堆上的每个对象都需要一些额外的成员“类型对象指针”和“同步块索引”。
- 它从托管堆中分配指定类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0。
- 它初始化对象的“类型对象指针”和“同步块索引”。
- 调用类型的实例构造器,向其传入在对new的调用中指定的任何实参。
class SampleClass { public string name; public int id; public SampleClass() { } public SampleClass(int id, string name) { this.id = id; this.name = name; } } class ProgramClass { static void Main() { SampleClass Employee2 = new SampleClass(1234, "Cristina Potra"); } }
没有和new操作符对应的一个delete操作符。换言之,没有办法显示释放 为一个对象分配的内存。CLR采用了垃圾回收机制,能自动检测到一个对象不再被使用或访问,并自动释放对象的内存。
4.2类型转换
CLR最重要的特性之一就是类型安全性。在运行时,CLR总是知道一个对象是什么类型。调用GetType方法,总是知道一个对象确切的类型是什么。
CLR允许将一个对象转换为它的实际类型或者它的任何基类型。
C#不要求特殊语法即可将一个对象转换为它的任何基类型,因为向基类型转换被认为是安全的隐式转换。然而,将对象转换为它的某个派生类型时,C#要求开发人员只能进行显式转换。
隐式转换不需要在代码中指定转换类型,例如:int intNumber = 10; double doubleNumber = intNumber; intNumber会被隐式转换成double类型。
显式转换则相反,需要指定转换类型,例如:double doubleNumber = 10.1; int intNumber = (int)doubleNumber;
对于表示数值的基本数据类型来说,数值范围小的数据类型转换成数值范围大的数据类型可以进行隐式转换,而反过来则必须进行显示转换。
就像上面的两个例子一样。 对于类类型来说,子类转换成父类可以进行隐式转换,而反过来则必须进行显式转换,例如:string str1 = "abc";object obj = str1; //子类转换成父类,隐式转。 string str2 = (string)obj; //父类转换成子类,显式转换 如果两个类之间没有继承关系,则不能进行隐式转换或显式转换,此时必须在被转换的类中定义一个隐式转换方法或显式转换方法。
在Main方法中,会构造一个Manager对象,并将其传给PromoteEmployee。这些代码能成功编译并运行,因为Manager最终从Object派生的,而PromoteEmployee期待的正是Object。在PromoteEmployee内部,CLR核实o引用的是一个Employee对象,或者是从Employee派生的一个类型的对象。由于Manager是从Employee派生的,所以CLR执行类型转换,运行PromoteEmployee继续执行。
PromoteEmployee返回之后,Main继续构造一个DateTime对象,并将其传给PromoteEmployee。同样的,DateTime是从Object派生的,所以编译器会顺利编译调用
PromoteEmployee的代码。但在PromoteEmployee内部,CLR会检查类型转换,发现o引用的是一个DateTime对象,它既不是一个Employee,也不是从Employee派生的任何类型。所以CLR会禁止转型,并抛出一个System.InvalidCastException异常。
声明PromoteEmployee方法的正确方式是将参数类型指定Employee,而不是Object。
v 使用C#的is和as操作符来转型
is检查一个对象是否兼容于指定类型,并返还一个Boolean值:true或false。is操作符永远不会抛出异常。
如果对象引用为null,is操作符总会返还false,因为没有可检查其类型的对象。
as操作符的工作方式与强制类型转换一样,只是它永远不会抛出一个异常。
检查最终生成的引用是否为null。
4.3命名空间和程序集
命名空间用于对相关的类型进行逻辑性分组,开发人员使用命名空间来方便地定位一个类型。
密封类的修饰符,用了这个修饰符的类就不能被其他类继承了。
应该有一种简单的方式来直接引用FileStream和StringBuilder类型。C#编译器通过using指令来提供这种机制。
C#的using指令指示编译器尝试为一个类型附加不同的前缀,直到找到一个匹配项。
using指令允许为一个类型或命名空间创建别名。
在C#中namespace指令的作用:只是告诉编译器为源代码中出现的每个类型名称附加命名空间名称前缀,减少程序员的打字量。
命名空间和程序集不一定是相关的。
同一个命名空间的各个类型可能在不同的程序集中实现。例如:System.IO.FileStream类型是在MSCorLib.dll程序集中实现,而System.IO.FileSystemWatcher类型是在System.dll程序集中实现的。
4.4运行时的相互关系
参考http://www.cnblogs.com/MeteorSeed/archive/2012/01/24/2325575.html
本节将解释类型、对象、线程堆栈和托管堆在运行时的相互关系,以及调用静态方法、实例方法和虚方法的区别。
进程是指在系统中正在运行的一个应用程序;线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。
当系统加载一个CLR的进程,进程里面可能有多个线程,这时候系统会给这个进程创建一个大小为1M的线程堆栈。这个线程堆栈用来存放方法调用的实参,和方法内部定义的局部变量。