第4章 基础类型
4.1 所有类型都从System.Object派生
CLR要求每个类型最终都从System.Object 类型派生,所以可以保证每个类型的每个对象都有一组最基本的方法。
重写(override):继承时发生,在子类中重新定义父类中的方法,子类中的方法和父类的方法是一样的(即方法名,参数,返回值类型都相同)。
重写override一般用于接口实现和继承类的方法改写,要注意:
- 覆盖的方法的标志必须要和被覆盖的方法的名字和参数完全匹配,才能达到覆盖的效果;
- 覆盖的方法的返回值类型必须和被覆盖的方法的返回一致,覆盖的方法要加override关键字;
- 覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
- 被覆盖的方法不能为private,可以为protected,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
- 不能重写非虚方法或静态方法。重写的基方法必须声明为 virtual、abstract 或 override 的。
重载(overload):指的是同一个类中有两个或多个名字相同但是参数不同(参数个数和参数类型)的方法。
虚方法:
1、virtual父类方法表示此方法可以被重写,也就是说这个方法具有多态。表明父类中的方法是通用方法,可以在子类中重写,但不是必须重写的。
2、virtual父类方法可以直接使用,和普通方法一样。
virtual关键字只是明确标识此父类方法可以被重写,其实它和一般的方法没有什么区别。相应的,sealed关键字标识此方法不可以被重写。
简单的说,虚方法就是可以被子类重写(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)//base关键字用于从派生类中访问基类的成员 { 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。
public static bool Equals(object objA, object objB); / public static bool ReferenceEquals(object objA, object objB);
- GetHashCode: 返回对象的值的一个哈希码。哈希代码是一个用于在相等测试过程中标识对象的数值。
public virtual int GetHashCode();
- ToString: 我们经常需要重写这个方法,使它返回一个String对象。
public virtual string ToString();
- GetType: 返回从Type派生的一个对象的实例,指出调用GetType的那个对象是什么类型。返回的Type对象可以和反射类配合使用,从而获取与对象的类型有关的元数据信息。GetType方法是非虚方法,这样可以防止一个类重写该方法,并隐瞒其类型,从而破坏类型安全性。
public Type GetType();
System.Object的受保护的方法:
- MemberwiseClone:这个非虚方法能创建类型的一个新实例,执行实例的浅拷贝。
protected object MemberwiseClone()
- Finalize: 是.net内部的一个释放内存资源的方法。这个方法不对外公开,由垃圾回收器自动调用。
内存:程序和数据平常存储在硬盘(硬盘是一种可记忆盘)等存储器上,不管你开机或关机了,它们都是存在的,不会丢失。硬盘可以存储的东西很多,但其传输数据的速度较慢。所以需要运行程序或打开数据时,这些数据必须从硬盘等存储器上先传到另一种容量小但速度快得多的存储器(无记忆盘),之后才送入CPU进行执行处理。这中间的存储器就是内存。
所有计算机都配有内存,也称RAM(随机存取存储器)。RAM用于存储计算机正在执行的程序以及程序使用的数据。内存可以看作是一个简单的字节数组。在这个数组中,每个内存单元都有自己的内存地址:第一个字节的地址是0,后面依次是1、2、3,等等。内存地址相当于普通数组的下标。计算机可以随时访问内存的任何位置(所以称为“随机存取存储器”)。
内存通常分为四个区:
- 全局数据区:存放全局变量,静态数据,常量
- 代码区:存放所有的程序代码
- 栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等
- 堆区:即自由存储区
线程栈(Thread Stack)
每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多个线程(thread)。
每个线程都拥有一块“自留地”,称为“线程栈”,大小为1M,用于保存自身的一些数据。
比如方法中定义的局部变量、方法调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉,主要由操作系统管理。
所有值类型的变量都是在线程栈中分配的。
托管堆(Managed Heap)
另一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)。
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。详见:第21章 自动内存管理
new操作符
用new关键字创建类的对象时,分配给对象的内存单元就位于托管堆中。在程序中我们可以随意地使用new关键字创建多个对象,因此,托管堆中的内存资源是可以动态申请并使用的,当然用完了必须归还。
Employee emp = new Employee (“ConstructorParam1”);
- 声明一个Employee的引用emp,在线程栈上给这个引用分配存储空间,这仅仅只是一个引用,不是实际的Employee对象。假定emp占4个字节的空间,包含了存储Employee的引用地址。1兆字节(mb)=1024千字节(kb)
- 接着分配托管堆上的内存来存储Employee对象的实例,假定Employee对象的实例是32字节。为了在托管堆上找到一个存储Employee对象的存储位置,CLR在托管堆中搜索第一个从未使用的,32字节的连续块来存储Employee对象的实例。
- 然后把分配给Employee对象实例的地址赋给emp变量,即将新建对象的引用保存到变量emp中。
1 字节(Byte) = 8位(bit)
为什么一个字节是8位?这是因为,2的8次方表示的数是128个,
位: 一个0或1称为一位(bit);
字节:连续八位称为一个字节(Byte);字节是计算机中可单独处理的最小单位。
以下是new操作符所做的事情:
- 它计算类型中定义的所有实例字段需要的字节数。堆上的每个对象都需要一些额外的成员“类型对象指针”和“同步块索引”。
- 它从托管堆中分配指定类型要求的字节数(对象的实例要求的字节数=实例字段需要的字节数+“类型对象指针”+“同步块索引”),从而分配对象的内存。
- 它初始化对象的“类型对象指针”和“同步块索引”。
- 调用类型的实例构造器,向其传入在对new的调用中指定的任何实参,从而将对象的实例字段初始化为良好初始状态。
class SampleClass {
public int id; public string name;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允许将一个对象转换为它的实际类型或者它的任何基类型。
隐式转换:将一个对象 转换为 它的任何基类型,不需要在代码中指定转换类型。
显式转换:将基类型对象 转换为 它的某个派生类型,需要指定转换类型。
- 对于表示数值的基本数据类型来说,数值范围小的数据类型转换成数值范围大的数据类型可以进行隐式转换,而反过来则必须进行显示转换。
例如: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; //父类转换成子类,显式转换
注意:如果两个类之间没有继承关系,则不能进行隐式转换或显式转换,此时必须在被转换的类中定义一个隐式转换方法或显式转换方法。
internal class Employee { } internal class Manager : Employee { } public sealed class Program { public static void Main() { Manager m = new Manager(); PromoteEmployee(m); DateTime newYears = new DateTime(2010, 1, 1); PromoteEmployee(newYears); } public static void PromoteEmployee(Object o) { Employee e = (Employee)o; } }
在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。
使用C#的is和as操作符来转型:
- is检查一个对象是否兼容于指定类型,并返还一个Boolean值:true或false。is操作符永远不会抛出异常。
如果对象引用为null,is操作符总会返还false,因为没有可检查其类型的对象。
- as操作符的工作方式与强制类型转换一样,只是它永远不会抛出一个异常。
检查最终生成的引用是否为null。
4.3 命名空间和程序集
命名空间用于对相关的类型进行逻辑性分组,开发人员使用命名空间来方便地定位一个类型。
创建命名空间的过程非常简单,只需像下面这样在代码中写一个命名空间定义:
namespace CompanyName { public sealed class A { }//TypeDef:CompanyName.A namespace X { public sealed class B { }//TypeDef:CompanyName.X.B } }
//以上注释,编译器在类型定义元数据表中添加的实际类型名称,这是CLR所看到的实际类型名称
在C#中namespace指令的作用:只是告诉编译器为源代码中出现的每个类型名称附加命名空间名称前缀,减少程序员的打字量。
C#的using指令指示编译器尝试为一个类型附加不同的前缀,直到找到一个匹配项。
using指令允许为一个类型或命名空间创建别名,该别名在直接包含此指令的编译单元或命名空间体内有效,看如下代码。
namespace N1.N2 { class A {} } namespace N3 { using A = N1.N2.A; class B: A {} }
命名空间和程序集不一定是相关的:
同一个命名空间的各个类型可能在不同的程序集中实现。例如:System.IO.FileStream类型是在MSCorLib.dll程序集中实现,而System.IO.FileSystemWatcher类型是在System.dll程序集中实现的。