C#中的类型(一)——类型基础
在学习C#的过程中,第一个遇到的就是类型,大多数同学都觉得很简单,然后一带而过。但是回过头来看看,类型中还是有很多需要我们注意的问题。本系列文章是以《CLR via C#》为基础,再加上一些其他人写的博客以及我自己的经验和感悟写的,希望可以由浅入深,慢慢的把类型需要注意的问题讲清讲透,使我也在写博客的过程中学习到更多。
好了,废话不多说,我们来开始第一节。这一节是类型的最基础问题,可能里面的点大家都很熟,但是这些又是类型的基础,需要弄清楚。
一、System.Object
C#中所有的类型都是继承于System.Object类型的,所以在讲其他的内容之前我们先看看System.Object类型的代码:
// 摘要: // 支持 .NET Framework 类层次结构中的所有类,并为派生类提供低级别服务。这是 .NET Framework 中所有类的最终基类;它是类型层次结构的根。 [Serializable] [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] public class Object { // 摘要: // 初始化 System.Object 类的新实例。 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] public Object(); // 摘要: // 确定指定的 System.Object 是否等于当前的 System.Object。 // // 参数: // obj: // 与当前的 System.Object 进行比较的 System.Object。 // // 返回结果: // 如果指定的 System.Object 等于当前的 System.Object,则为 true;否则为 false。 [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public virtual bool Equals(object obj); // // 摘要: // 确定是否将指定的 System.Object 实例视为相等。 // // 参数: // objA: // 要比较的第一个 System.Object。 // // objB: // 要比较的第二个 System.Object。 // // 返回结果: // 如果认为对象相等,则为 true;否则为 false。 [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public static bool Equals(object objA, object objB); // // 摘要: // 用作特定类型的哈希函数。 // // 返回结果: // 当前 System.Object 的哈希代码。 [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public virtual int GetHashCode(); // // 摘要: // 获取当前实例的 System.Type。 // // 返回结果: // System.Type 实例,表示当前实例的确切运行时类型。 [SecuritySafeCritical] public Type GetType(); // // 摘要: // 创建当前 System.Object 的浅表副本。 // // 返回结果: // 当前 System.Object 的浅表副本。 [SecuritySafeCritical] protected object MemberwiseClone(); // // 摘要: // 确定指定的 System.Object 实例是否是相同的实例。 // // 参数: // objA: // 要比较的第一个 System.Object。 // // objB: // 要比较的第二个 System.Object。 // // 返回结果: // 如果 objA 是与 objB 相同的实例,或者如果二者都为空引用,则为 true;否则为 false。 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public static bool ReferenceEquals(object objA, object objB); // // 摘要: // 返回表示当前 System.Object 的 System.String。 // // 返回结果: // System.String,表示当前的 System.Object。 public virtual string ToString(); }
C#中使用new操作符来创建一个对象,如类Animal定义为:
public class Animal { private string _name; public string Name { get; set; } public Animal() { //Do some thing } public Animal(string name) { _name = name; } }
(1)、首先CLR需要知道这个实例化的对象需要多大的内存存储,所以会计算该类型及其所有基类(一直到System.Object)中定义的所有实例字段需要的字节数。另外因为在堆上存储的所有对象都需要额外的两个成员——类型对象指针(type object pointer)以及同步块索引(sync block index),所以也要把他们的字节数包括在内。另外顺便说一句,这额外的两个成员是由CLR来管理对象使用的。
总之一句话,第一步是计算要分配多大内存给这个对象的。
(2)、从托管堆中分配指定大小的内存。
(3)、初始化对象额外的两个成员——类型对象指针和同步块索引
(4)、调用类型的构造函数(也有的地方翻译为“构造器”,我习惯叫做构造函数,下同),传入在new的调用中指定的参数。构造函数的调用时一级一级往下的,也就是说要从Object类的构造函数调用开始,一级一级往下调用,每个构造函数都会初始化本类型中定义的实例字段。在本例中,他会先调用System.Object.Object(),然后调用 Animal.Animal(string name)。
tips:我们深入的了解下构造函数的执行顺序。所以我们继续构造一个Dog类,该类继承与Animal:
public class Dog : Animal
如果执行Dog dogModel1 = new Dog();,则构造函数的执行顺序为:{ public Dog() { //Do some thing } public Dog(string name) { //_name = name; } public Dog(string name, int age) { //do some thing } }
System.Object.Object(); Animal.Animal(); Dog.Dog(); 如果执行Dog dogModel1 = new Dog("Keli");,则构造函数的执行顺序为:
System.Object.Object(); Animal.Animal(); Dog.Dog(string name);
如果执行Dog dogModel1 = new Dog("Keli", 15);,则构造函数的执行顺序为:
System.Object.Object(); Animal.Animal(); Dog.Dog(string name, int age);
如果Animal只有Animal.Animal(string name)构造函数的话,那么在Dog中的构造函数就会报错,而且这是在编译阶段就会被发现的错误,如下图:
如果你想在调用Dog的构造函数Dog(string name)时调用父类Animal的Animal(String name),则需要在构造函数中做如下修改:
public Dog(string name) : base(name) { //_name = name; }
System.Object.Object(); Animal.Animal(string name); Dog.Dog(string name);
同样,也可以在调用构造函数时先调用该类的另一个构造函数,使用的是this关键字:
public Dog(string name, int age) : this(name) { //do some thing }
这种情况下,执行Dog dogModel1 = new Dog("Keli");时,调用的顺序是这样的:
System.Object.Object(); Animal.Animal(string name); Dog.Dog(string name); Dog.Dog(string name, int age);
请不要将构造函数的执行顺序刻意的写成死循环,如:
public Dog(string name) : this(name, 14) { //_name = name; } public Dog(string name, int age) : this(name) { //do some thing }
编译时不会报错,而在运行时会报SystemOverFlowException错误,报错信息如下:
|
二、类型转换
这里的类型转换并非基元类型的转换,而是基础的那种类型转换。
类型安全性是CLR最重要的特性之一,所以在类型转换中CLR也需要确认转换安全才能隐式转换,而在不确认是否安全的情况下,需要程序员强制转换。这种情况下一切后果由程序员负责。
其实总结起来很简单,就是所有对象向自己的基类型转换是安全的,所以可以直接隐式转换;而向自己的某个派生类型转换时,转换可能失败,所以需要开发人员显示转换。
例如:
Object o = new Animal(); Animal a = (Animal)o; Dog d = (Dog)o;
第二种情况是Object类型转换为自己的派生类Animal,所以需要强制转换,但是由于o实际指向的是一个Animal对象,所以这样的转换也是安全的,所以这一步不会报错;
第三中情况是将Object类型转换为派生类的派生类,也需要强制转换,但是由于o指向的是一个Animal对象,而Animal类型是Dog类型的父类,不能转换,所以这一步在运行时会报错。
C#中的 as 和 is操作符
is操作符是检查一个对象是否兼容于指定的类型,如果是则返回true,否则返回false。实际上也就是能不能成功转换与指定的类型。
例如:
bool b1 = o is Animal;
bool b2 = o is Dog;
上面的例子的b1 = true; b2 =
false;bool b2 = o is Dog;
所以is操作符一般用在判定一个类型是否能安全的转换成另一个类型的场合,例如:
if (o is
Animal)
{
Animal a2 = (Animal)o;
}
但是这样写有一个弊端,那就是在if中会验证一次o是否可以转换为Animal类型,在实际转换中又会转换一次,而类型转换时对性能有一定影响的,所以我们可以用as操作符来简化这段代码,既安全的转换了类型,又节省性能。{
Animal a2 = (Animal)o;
}
as是将一个对象从一种类型转换为另一种类型,如果成功转换,则将对象转为目标类型,否则,as将返回null。as的工作方式和强制类型转换时一样的,但是永远不会抛出异常,因为出现异常会被抑制,结果为null。
这样,上面的例子就可以转换为:
上面的代码仅仅会类型转换一次,即as,而在if语句中,仅仅是验证实例 a 是否为null,这个检查消耗的性能比类型转换要小得多。
a = o as Animal; if (a != null) { //Do something }
三、命名空间和程序集
1、命名空间(namespace):
命名空间是用于对相关的类型进行逻辑性分组,开发人员使用命名空间来方便的定位一个类型。
比如你电脑上有很多文档,你要建立一个目录来逻辑性的吧这些文档分成很多个类,比如“学习资料”、“电影”等,这种分成的这些类就是相当于我们这里说的命名空间,以后找某个电影文件就可以去“电影”类去找。
当我们在使用某个类型的时候是需要这个类型的全称的,也就是需要将命名空间带上,但是C#编译器为了方便开发人员,应用了using指令来简化我们的代码。相信这个大家都懂的,我就不多说了。C#编译器在编译时遇到一个类型,会尝试着在前面加上using中的命名空间来依次匹配。
如果两个命名空间中有相同名称的命名类型,此时在使用该类型时就需要将其命名空间带上来区分。
也可以使用命名空间别名来解决这个问题,例如NameSpace1 和NameSpace2 都有Class1这个类型,当需要使用时可以用如下方法:
using np1 = NameSpace1; using np2 = NameSpace2; np1.Class1 = null; np2.Class1 = null;
还有一种极端情况,就是命名空间名称和引用的类型名称完全相同,此时就需要使用外部别名功能,具体的使用可参见msdn:http://msdn.microsoft.com/zh-cn/library/ms173212.aspx
2、程序集(assembly):
程序集是一个或多个模块/资源文件的逻辑性分组,他是重用,安全性以及版本控制的最小单元。由于程序集不是本系列讨论的重点,暂时仅仅提这么多,可以参考《CLR via C#》中文第三版的P6-P10。
3、命名空间和程序集的关系:
很多童鞋在纠结这个问题,我想说这个真的没有必要比较,他们几乎没有任何关系。一个程序集可以包含几个命名空间,同时一个命名空间也可以分布在几个程序集中。
暂且可以这样理解:刚才讲命名空间的时候举了一个文档的例子,如果说命名空间是指这些文档按照逻辑性分出来的类的话,那么程序集就是这些文档的物理分类,比如C盘、D盘等。
好了,类型基础就总结这么多,回头看看大部分都是《CLR via C#》中的内容,权当是这本书的复习吧。最后再次推荐下这本书,写的很好。