Web开发之C#:(3)C#数据类型基础

作者:陆金龙    发表时间:2016-07-21 00:02   


1.C#的数据类型

Object

C#所有类型隐式继承自System.Object。C#支持两种类型:值类型和引用类型。

结构和枚举是值类型。

类、数组、委托、接口是引用类型。

基元类型

C#中还存在一种编译器直接支持的数据类型。基元类型直接映射到FCL中存在的类型,相对应的二者在生成IL代码时没有区别。

引用类型

继承关系:引用类型如果没有从其他类型派生,就默认的直接继承自Object。

内存分配:引用类型的实例总是从托管堆上分配,C# new操作符返回对象的内存地址。

对象:即类型的一个实例,是类型实例的另一种称谓。

值类型

文档将所有值类型称为结构或枚举,值类型用struct或enum来声明。

继承关系:结构体直接继承自System.ValueType;而枚举直接继承自System.Enum, Enum类又直接继承自System.ValueType。System.ValueType又派生自System.Object。

内存分配:做为局部变量和方法参数的值类型的实例在线程栈上分配,即使使用new,也是在栈上分配空间。在引用类型实例中的值类型字段,与对象中其他数据一起都在堆上。

实例:代表值类型实例的一个变量中,并不包含一个指向实例的指针,变量中包含了实例本身的字段。

System.ValueType是以class声明的,是引用类型,但是从它派生的类型却是值类型。因为Clr会对于从System.ValueType派生的类型进行特殊处理,在内存分配时直接分配到栈上,使其具有值类型的特征。

引用类型与值类型比较

声明:值类型用struct、enum进行声明,引用类型用class、delegate、interface等声明。

继承关系:值类性隐式继承自System.ValueType,值类型不可从其他类型派生也不可做为其他值类型或引用类型的基类型。值类型和引用类型都可实现接口。

实例特点:引用类型在创建实例相比比值类型消耗更多的内存和性能;值类型的使用能缓解托管堆中的压力。

传值方式:值类型是复制传值,比起引用类型的传值要更消耗能存和性能。要慎用大尺寸、频繁传值的值类型实例。

内存释放:值类型不受垃圾回收器控制,在离开作用域后自动释放所占内存。引用类型的对象在没有被引用时,其占用的空间由垃圾回收器统一进行回收管理。

对象

类或结构定义的作用类似于蓝图。从本质上说,对象是按照此蓝图分配和配置的内存块。

程序可以创建同一个类的多个对象。对象也称为实例,可以存储在命名变量中,也可以存储在数组或集合中。

由于类是引用类型,因此类对象的变量引用该对象在托管堆上的地址。如果将同一类型的第二个对象分配给第一个对象,则两个变量都引用该地址的对象。

由于结构是值类型,因此结构对象的变量具有整个对象的副本。结构的实例也可以使用 new 运算符来创建(有参构造方法),但这不是必需的。

2.Object的公开方法

1)Equals

定义:

public class Object {

public virtual Boolean Equals(Object obj){

if(this==obj) return true;

return false;

}

}

有以述源码可见,Object中Equals方法默认只实现了同一性,而非相等性。但这是一个虚方法,用户可以在需要的情况下为自己的类型重写该方法来更完美是实现相等性。

2)ReferenceEquals

public class Object {

public static Boolean ReferenceEquals(Object objA,Object objB){

return objA==objB;

}

}

由于类型能重写Equals方法,重写的方法应调用其基类的Equals,因此Object虽然为Equals实现了对象的同一性比较,我们却不能用类型那个的Equals方法来测同一性。Object另外提供了一个静态方法ReferenceEquals来进行同一性比较。

3)GetHashCode

定义的一个类型重写了Equals方法,那么应该重写GetHashCode方法,因为Hashtable、Dictionary及其他一些集合实现中,要求两个对象相等,必须具有相同的哈希码,所以要确保相等性算法与哈希算法是一致的。

4)ToString

默认返回类型的完整名称,即this.GetType().FullName,可根据需要重写。

 

3.类型转换

类型与基类之间有时需要进行转换,存在隐式转换和显式转换两种方式。

1)隐式转换

根据里氏替换原则,子类应当可以替换父类并出现在父类能够出现的任何地方。因此子类对象类型可以直接隐式的转换为父类(基类)类型。如:

Object o = new ObjA();

2)显式转换

父(基)类对象转为子类对象有可能不成功,因为可以从父类派生多个子类,当前父类对象可能不兼容于某些子类类型。此时需要用到显式转换,设计到两个关键字:is 和as。如is和强制转换配合使用:

if(o is ObjA){

ObjA a= (ObjA)o;

}

 

或者使用as转换,当遇到不兼容情况时返回null:

ObjA a=o as ObjA;

if(a!=null){

//业务代码

}

3)装箱和拆箱

System.ValueType是引用类型,值类型隐式继承自System.ValueType意思是值类型可转换为System.ValueType类型,转换过程有装箱操作。

所有类型隐式继承自System.Object类是指他们可以转换为System.Object类,值类型转换为Object类型有装箱现象。

什么是装箱和拆箱?先来回顾一下栈和堆的概念:

栈(stack) 
位于常规内存区(general random-access memory),处理器可以通过栈指针(stack pointer)对它进行直接访问。指针向下移动创建新的存储空间,向上释放存储空间。是仅次于寄存器(registers)的最快、最有效率的分配内存方法。一般用来存储那些已知大小的值类型和引用,栈中分配的数据在超出作用域后自动被释放。


堆(heap) 
这是一段多用途内存池(general-purpose pool of memory)。堆的优点是分配内存时编译器无需知道该分配多少空间,以及数据的生存期。在堆中分配的对象必须通过垃圾回收期回收后才能释放,因此效率要比栈低一些。

 

装箱:将值类型转换为引用类型。当我们把值类型参数传递给需要引用类型参数的方法时,会自动进行装箱操作。

步骤:
1. 从托管堆分配内存,包括添加方法表指针和SyncBlockIndex。 
2. 将值类型字段拷贝到该内存。 
3. 返回该地址的引用。


拆箱:获取指向对象中包含的值类型部分的指针。一般拆箱之后会进行字段拷贝操作,两个操作加起来才是真正与装箱互反的操作。

步骤:
1. 如果引用为null,抛出NullReferenceException。 
2. 如果目标不是已装箱的值类型,抛出InvalidCastException。 
3. 返回指向包含在已装箱对象中值类型部分的引用,而不是重新在堆栈建立值类型实例,因此需要GC回收才能释放。

 

4.命名空间

命名空间用于对相关的类型进行逻辑分组。

using指令可一次引入命名空间,在使用时可省去代码中类的命名空间部分,只写最后一部分类型名。也可在using指令中使用别名替代指定的命名空间,代码中使用别名做为前缀和类型名组合表示类型。

命名空间是由用户根据需要定义的,与程序集没有直接的对应关系,只是VS在创建类型时会默认地使用程序集和文件夹组合做为命名空间部分。