C#基础知识梳理系列二:C#的演绎大师:类型
如果说C#是CLR特邀演员阵容之一,那类型class绝对是C#的演绎/演艺大师、不朽灵魂!它不仅演绎了C#的豪放,也演艺了C#的柔美。时而恢弘、时而细腻。一切类型皆System.Object。这一章将向您解释类型的生成,类型的演绎转换及类型设计的必要元素、类型成员的内存分配,当然还有装箱及拆箱操作。
C#里,所有事物都是被按类划分,一切事物皆对象,任何对象都会对应为某一类。所有被划分出来的类都被归为一个超级“类”,即System.Object。Object被定义在命名空间System 门下,居住于程序集mscorlib.dll小区。类型Object是一个比较开放的类,成员不多,只公开了四个方法:
public virtual bool Equals(object obj);
确定指定的 System.Object 是否等于当前的 System.Object。
public static bool Equals(object objA, object objB);
判断两个给定的对象是否相等,其实就是判断两个对象是否包含相同的值。
public virtual int GetHashCode();
返回当前对象的值的哈希码。在有些集合中判断两个对象是否相等的条件就判断两个对象的哈希码是否相同,所以,如果继承object类的子类中如果重写了Equals方法,则必须重写此方法。
public Type GetType();
返回当前实例的运行时类型
public static bool ReferenceEquals(object objA, object objB);
与Equals方法不同,它是判断两个对象是否指向同一个对象。
public virtual string ToString();
默认获取当前对象的带有命名空间的完全限定名。在子类中调用时,它返回对象的字符串表示。
CLR要求所有的定义类型的祖先(先人板板)必需是System.object。.NET Framework里明确说明所有的类型最终都是从object类型派生。我们所定义的任何类型都是默认从object类派生,而不用明确指定其继承关系,编译器会自动识别并做出相应处理。如下我们定义了一个Person 类,通过IL可以看出类型的实际继承关系,如图:
(1)类型转换
既然有了类型继承,那一定存在多个派生类型同时继承于同一个基类,并且有可能发生将对象从一个类型转换到另一个类型情形。CLR是一个坚持类型安全的平台,由于所有类型都是继承于object 类,也就得到了基类object的GetType()方法。所以调用对象的GetType()方法就可以知道该对象是什么类型。CLR允许将一个对象转换为它的任何基类型,这种转换是自然隐式的;也可以将一个对象转换为它的某一派生类型,但是这种转换是要求显示转换,在转换过程中,CLR要做类型安全检查验证,对于不能向上转换的,将抛出异常。如下代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class Person { public string Name { get; set; } } public class Teacher : Person { public int RootID { get; set; } } class Program { static void Main(string[] args) { Teacher teacher = new Teacher(); Person p = teacher; //转换正常 p.Name = "abc"; teacher = (Teacher)p; //转换正常 //以下转换将会抛出异常 Person person = new Person(); teacher = (Teacher)person; } }
(2)类型转换操作符:as 和 is
C#提供了as和is这两个操作符方便我们进行类型转换。
as是将一个对象从一种类型转换为另一种类型,如果成功转换,则将对象转为目标类型,否则,as将返回null。如下:
Teacher teacher1 = new Teacher(); //转换正常 Person p1 = teacher1 as Person; if (p1 != null) { //其他操作 } Person pTemp = new Person(); //类型检查不通过,t将为null Teacher t = pTemp as Teacher;
is操作符是判断一个对象是否是某种类型,如果是则返回true,否则返回false,在我们使用 as转换前,可以先使用is进行类型确认,如果该对象是目标类型则转,否则不进行转换。如下:
Teacher teacherB; if (pTemp is Teacher) { teacherB = pTemp as Teacher; }
由于is和as都会进行对对象进行类型安全检查,所以上面的代码段将执行两次安全验证。在一个操作过程中执行两次安全验证,似乎降低了性能,所以我们可以采用如下方法,只进行一次安全验证,同样达到了目的。
Teacher teacher1 = new Teacher(); //转换正常 Person p1 = teacher1 as Person; if (p1 != null) { //其他操作 }
这段代码只进行了一次类型安全检查,从而提高了性能。Is操作符只是看上去更语意更一目了然,可以根据自己的喜好选择。
在进行某些类型算术运算过程中,很可能导致溢出!比如精度丢失:将int64位数值转为int32位,我们将在下面会讲到这一点。
C#语言支持基元类型。
基元类型:c#编译器直接支持的数据类型为基元类型。如int、byte、char、string等。这些基元类型与.NET Framework类库都有一一对应。比如:int对应System.Int32、string对应System.String等。更多的对应关系,可查看 MSDN 。
基元类型的算术运算可能导致溢出,所以要注意。如下:
int a = 2147483647;
int b = a + 1; //-2147483648
很显然b的值已经超出我们的正常期望,这种溢出是偷偷地发生,如果我们希望这种情况发下,当发生溢出时,我们想捕获这咱“异常”,可以使用check操作符在运算过程中进行检查,如下:
int c = checked(a + 1);
当发生溢出时,会抛出OverflowException(算术运算导致溢出)异常。当然我们也可以主观地不对这种溢出进行安全验证,可以使用unchecked。如下:
int d = unchecked(a + 1);
同时c#还支持checked和unchecked语句块,如下:
checked
{
int e = a + 1;
}
unchecked
{
int f = a + 1;
}
另外,CLR支持两种数据类型:引用类型和值类型。其实CLR只有一种类型那就是引用类型,因为所有的派生类型都是从System.Object类派生,但是由于CLR本身的运行机制要求引用类型分配在托管堆上,既然内存从托管堆分配,每次分配时可能会导致垃圾回收,而垃圾回收会带来性能损伤。同时由于类型元数据结构的问题,在给对象分配内存时,会有一些额外的数据成员,比如:对象指针、同步块索引等。这些明显占据了一部分内存。所以CLR又提出了值类型这一数据类型,事实上值类型也是从System.Object派生而来,如结构类型。而结构类型派生于System.ValueType,System.ValueType又派生于System.Object。所以,凡是派生于System.ValueType的类型都称为值类型。
值类型
值类型又分为:
简单类型:Int32、Char、Boolean等,这些类型可以声明变量、方法等。
结构类型:struct,void等,这些类型声明常量、字段、方法等。
枚举类型:enum。声明一个由一指定常量集合组成的类型。
值类型的实例一般在线程栈上分配而并非托管堆,声明一个值类型变量后,变量中包含着实例本身的字段值,垃圾回收器不对值类型进行处理,通常在离开它们作用区范围时,这些实例将自动销毁。所有用struct声明的类型都是值类型。当然可以将一个特定的值类型送给引用类型,这就是装箱,下面会讲到。
引用类型
引用类型不存储实际数据值,只是存储它们所代表对象的地址引用。当声明一个引用类型后,它会在线程栈上保存该类型对象在托管堆中的引用地址,同时在托管堆中分配该对象的类型对象指针、同步块索引,实际数据等。所有class声明的类型都是引用类型。
由于引用类型的变量保存着堆上的一个对象的地址,所以在定义一个引用类型变量时它默认为null,当使用一个为null的引用类型变量,会抛出空引用的异常。而值类型的变量保存着实际值,所以它永远不会抛出空引用。
内存分配:
通过上面讲解,我们已经知道,值类型是分配在线程栈中而不是在托管堆中,它是不受垃圾回收器管理。Object是所有类型的最终基类,所以按道理它可以接收任何类型的数据,包括值类型,比如:int a=1; object obj=a; obj要想接收a,则必须对值类型a进行类型转换成object 类型,这个过程就是装箱。装箱会有以下操作:
先在托管中分配内存,然后将值类型的字段复制到新分配的内存堆中,最后返回新对象的地址。比如以下方法:
public void Test() { //装箱 int a = 1; object obj = a; //拆箱 object obj2 = 2; int b = (int)obj2; }
拆箱的过程与装箱相反,但不并完全是将操步骤相反。拆箱是将已装箱obj2中的各个字段的值复制到线程栈上,先获取已装箱变量中的所有字段地址的引用,然后是将每个地址引用的值复制到线程栈上的值类型的实例中。查看上面方法的IL可以看出真实的操作装箱、拆箱步骤:
通过以上的描述可以看出,装箱是要新分配托管堆内存,而这个过程有可能进行垃圾回收,这就带来了性能问题。所以在我们写代码的过程中,尽可能地避免装箱和拆箱的操作。
每个类型可以定义N个预定义的成员,这些成员可能包含如下这些:
常量指出数据值恒定不变的一个符号,使用const定义,例如:
private const decimal PAI = 3.14M;
字段一个只读、可读/写的数据值。如果是静态的,它代表类的一状态数据;如果实例的,它代表对象的状态数据。例如:
private int count = 100;
实例构造器
类型构造器
方法对类型或对象状态数据操作的一个过程实现。如果是静态的,它对类的状态数据进行操作;如果是非静态的,它对类对象的状态数据进行操作。
操作符重载
转换操作符
属性:它可以像方法一样操作类或对象的状态数据,但看上去却像字段一样的书写方式。如果是静态的,它对类的状态数据进行操作;如果是非静态的,它对对象的状态数据进行操作。通过IL,我们可以看到它是有方法实现的。如下,我们声明了一个int型的Age和一个string型的Name:
事件可向N个方法发送通知。如果是静态的,它可向N个静态方法发送通知;如果是非静态的,它可向N个实例方法发送通知。事件常常用一个委托链来维护所有已登记的方法。
类型可以在一个类的内部嵌套定义一个类型。
无论是类型还是类型成员,都可以指定其可访问性。例如我们可以给一个类型的可访问性指定为public或是internal。如果被指定为internal,则此类只可被在它所在的程序集的其他成员访问。如果想让该类型被其他程序集访问,可使用一个名为System.Runtime.ComiplerServices.InternalsVisibleTo的属性来达到目的。详细信息可查询MSDN。
在C#中,可以使用如下可访问性定义:public、 private、 protected 和internal。