.NET学习笔记(二) ——系统类型和通用操作 (上)

这段时间在学习的过程中,也看了其他不少人写的读书笔记,感觉每个地方都有好多东西可以写很多内容。但鉴于目前自己是在第一次学习阶段,很多地方无法弄的太深。此笔记也主要是对每一块内容学习的一个总结,每次在写笔记时,我觉得自己都会有新的收获,了解的更清楚。希望随着学习的深入也能写一些有深度的东西。最近这几天晚上上不了网,而且太累,导致耳鸣了一两天,所以今天才写笔记。以后每天还是早点睡觉,身体是革命的本钱啊!

 

一 类型基础

 

在.net里,FCL中定义了很多的类型,CLR的要求是每个类型都要继承自System.Object这个类型。在我们定义一个类行的时候,往往是隐式继承于Object的。Object这个类型定义了四个公有的实例方法和两个受保护的方法,而系统中所有类型都能使用这些通用的方法。

  • Equals: 此方法是判断两个对象的值是否相同的。在Object中的实现是判断两个对象是否指向同一个对象。而在派生类中,主要用于判断值是否相等。其中引用类型和值类型是不同的。自己定义的类型要判断时需要重写此函数。
  • GetHashCode:这个方法是返回对象的散列码。如果一个对象被用作散列表的一个键值,那么该对象的类型应该重写此方法。
  • Tostring:此方法默认情况下是返回一个类型的全名。另一种常见的用法就是重写该方法让它返回一个表示对象状态的字符串。还可以通过重写他来得到一个表示对象字段值的字符串。
  • GetType:方法返回一个类型为继承自Type的对象实例,标识了该方法所属对象的类型。此方法是一个非虚方法,可以防止派生类重写此方法而隐瞒实际的类型,破坏类型安全。
  • MemberwiseClone:这也是个非虚方法,他是创建一个新的类型实例,并将去字段设置为和this对象的字段相同。最后返回创建实例引用。后面的深拷贝时回用到此方法。
  • Finalize:这是一个虚方法,当垃圾回收齐判定某个对象为可回收的垃圾时,垃圾回收器回在对象被回收前调用此方法。此方法很重要。后面学习中还会具体涉及。

CLR要求每个对象都需要用new来创建,new的话系统会执行一系列的内存等分配工作。但要注意的是CLR中没有提供delete这样一个关键字来手动的释放内存,因为这些都是由垃圾回收器来完成的。也许有人会奇怪,我们平时定义一些简单的数值变量的时候并没有用new,只是在定义类的时候才用。这就引出了后面的话题。.net中的数据类型

 

二 数据类型

 

.net框架中几种数据类型:基元类型,值类型和引用类型。不同的类型有不同的作用,不同的创建方法,销毁方法和不同的内存分配。通过这一部分的学习,使我对这些都有了一定的了解。我自己也觉得这块比较有用。在后面还会继续深入的学习。

 

1:基元类型

编译器直接支持的数据类型称为基元类型。这就是基元类型的定义。比如C#支持的int,char,long,string等等,这些都称为基元类型。对于这种编译器直接支持的类型,都允许我们不使用new,而使用更直接,更方便的方式来创建他们的对象。也就是我们平时定义这些对象用的方法:int x = 0; 而不是用System.Int32 a = new System.Int32()

实际上这2种方法是一样的。为什么会这样呢?我们知道.NET的结构,他是一个可以兼容不同语言的平台,不同的编译器的基元类型可能不同,但他们都和FCL中的类型有直接映射的关系。如C#中int直接映射为System.Int32类型,后者称为.net的内置类型。这样在不同编译器开发的程序,最后还是使用FCL中的类型来表示的。而也适用与其他编译器的基元类型就称为与CLS兼容。比如C#的int型,在VB中也可以使用。而uint就不行。

有时候在写程序时不知道是用int还是Int32,是用string还是String,实际上他们是一样的,不同的是前面的是c#中的类型,而后者是FCL中的类型。他们之间有一个映射的关系。但建议使用FCL中的类型。他可以避免不同语言中的不一致性,也体现了语言无关性,当然还是根基个人习惯决定。下面4种方法都是对的。具体语言的的基元类型参见MSDN

int a  = 0;
System32.Int32 a = 0;
int a = new int();
System.Int32 a = new System.Int32()

 

2:值类型和引用类型

上面所讲的基元类型,并不是独立与这两种类型存在的第三种类型。基元类型可以是值类型,也可以是引用类型。比如int就是值类型,而string就是引用类型。由上面我们可以知道基元类型定义对象的时候可以不使用new,所以不能以是否用new创建对象来判断此对象是否为引用类型(或许很多人以前会认为用new创建的就是引用类型,现在应该明白了把。何况CLR是要求所有类型都用new创建的)

那么值类型和引用类型主要有那些区别呢?

对于引用类型来说,再创建对象的时候,new操作符号会成托管堆中分配一个空间用来存储对象;然后初始化对象的附加成员,第一个成员是指向类型方法的方法表。第二个成员是SyncBlockIndexs。CLR用这2个成员管理对象实例。接着会传入new操作符指定的参数,调用类型的构造函数。最后会返回一个指向新创建的对象的引用。这个引用存在线程的堆栈上。这个对象最后会由垃圾回收器进行回收。

由此可见,引用类型的性能不高。因为每使用一个对象,都要进行内存分配这些操作,初始化额外的2个成员,使用垃圾回收。这些都会使性能大打折扣。为了提供使用常用类型的性能,就引入了值类型。值类型是一种轻量级的类型。它通常被分配到线程的堆栈上。它本身包含的不是对象的引用,而是对象实际所包含的所有字段的值(这里可能会不理解字段的意思。确实我们常用的int,char等值类型,没有字段,但struct类型也是值类型,他就可以包含多个字段)。所以在使用此对象时也不用解析指针引用。而且值类型不收垃圾回收器的管理,减少了托管堆的压力。

在.NET框架中明确指出了那些是引用类型,那些是值类型。见下图,具体参见MSDN

 

其中值类型全部继承于System.ValueType类型,而他又继承与System.Object.所以所有的值类型必须继承与System.ValueType类型。所以我们在定义自己的值类型时不能为其选择任何基类型。但我们可以为它实现哟个或多个接口。CLR还规定,值类型不能做为任何其他类型的基类,因为值类型都是密封的,不能被继承。目前C#中的所有值类型都是基元类型。

引用类型和值类型内存布局图如下:

值类型的优点是代价小,性能高。但相比引用类型也有些限制。值类型对象有2种表示:装箱和未装箱。而引用类型总是装箱的。对于值类型,我们不能为它添加虚方法,它不能被继承。

关于值类型和引用类型具体的内存分配可以参见:http://www.cnblogs.com/anytao/archive/2007/05/23/must_net_08.html 这里分析的比较详细。

 

 

三 类型转换

 

类型转换是我们总回碰到的一个问题,他会出现在很多地方。CLR一个重要的特性就是类型安全。CLR在运行时总能知道一个对象的类型,我们可以利用GetTyoe方法来得到对象的准确类型。前面说了他是一个非虚方法,所以我们无法重写它来隐藏实际的类型。对于类型转换,CLR允许我们把对象转换为其原来的类型或它的任何一个基类型。各种编程语言自己决定如何提供这些转类型的操作,C#中是用隐式转换的。但把某个类型转换为他的派生类是就需要显式转换。而在运行时CLR会检查转类型操作,已确保总是将对象转为它的实际类型、或者它的任何基类型。

看下面的列子:

Employee a = new Employee();   //隐式的转换为本身
Object o = a;       //隐式的把Employee转为Object,转到基类
Employee b = (Int32)o;     //显式把Object转为Employee,转到派生类
DtaeTime yy = (DataTime)o;     //编译时正确,运行时系统抛出异常(InvalidCastExpection)

3和4好象没什么区别,都是显式的转换到派生类。为什么第4个就错了呢,而且是运行时报错。因为在编译时,系统检查o为Object型,而第四个也是显式的转换到派生类。所以不会报错。但在运行时,CLR会检查o所指向的引用,发现引用类型是一个Employee型的,而不是DtaeTime本身或任何DtaeTime的派生类型。所以在运行时回抛出异常。如果CLR可以进行这种转换,就失去了安全性,想想一个DtaeTime类型值为其他类型,在程序中会怎么样呢?而类型安全正是.NET框架中非常重要的一部分。

在我们进行类型转换时,有时因为复杂的继承关系,可能不太清楚某个类型和某个类型是否兼容(能否进行转换)。这个时候我们可以使用系统提供的2个操作符号:is 和 as.关于这两个操作符,网上讨论非常的多,我这里就简单介绍下,要注意的是2个操作符号只能用于引用转换、装箱转换和取消装箱转换。expression is/as type,其中: expression 引用类型的表达式。 type为类型。

is主要是判断某个对象或引用是否属于某个类型,如果是就返回true.不是就返回false.如果对象或引用为NULL,也返回false

Employee a = new Employee();   
Object o = a;  
//返回true,执行转换
if (o is Employee)
{
  Employee b = (Employee)o;
}
//返回false,不执行转换
if (o is DtaeTime) 
{
  DtaeTime yy = (DataTime)o;
}  

在上面的例子中看到,进行转换的话进行了2次兼容性检查(if中用is来检查。转换时CLR进行的检查)。下面是用as进行的检查,相比较as更简洁,效率更高。但要注意的是用as进行检查后,需要加上一个if判断,如果没有判断直接使用了指向空的引用,会引发一个NullReferenceException的异常。使用as时,只有CLR对类型进行了一次检查。后面只需要检查是否为空就行了。

Employee a = new Employee();   
Object o = a;  

Employee b = o as Employee;   //这里CLR检查o所引用的对象是不是Employee,是则返回所指的对象的非空指针。
if (b==BULL)                  //检查是否为空
{...}
else
{...}

DtaeTime yy = o as DataTime;  //这里CLR检查o所引用的对象是不是DataTime,不是则返回NULL,那么yy的引用为空。
if (yy==BULL)                //检查是否为空
{...}
else
{...}

如果按前面讲的,这个地方可能无法转化成功,因为a,b,c的类型都不同,而且没有继承关系(值类型不能被继承)。但实际上他是可以执行的。因为对于这些C#编译器熟悉这些基元类型,在编译时回安装自己的规则。也就是说编译器能事变一些通用的编程模式,产生必要的IL代码来使之按期望运行。

如果两个类型间是安全的,那么是可以进行隐式的转换,如果是不安全的,也就是可能导致丢失精度或者数量级的丢失,就需要显式的来转换了。对于小数转正数,C#是直接丢弃小数部分的。关于基元类型的转换规则可以参考C#语法。

对于编译器没有把某些类型当作基元来支持,我们就不能直接进行如上转型的操作,我们可以使用System.Convert类型的静态方法,在不同的对象类型之间进行转换。Convert知道如何在FCL提供的核心类型中间进行转换如(DtaeTime,String,Int32,Int64,Single,Double,decimal等等)。

顺便提下,对于许多基元类型运算操作可能会导致溢出,而CLLR只在32位或64位的数值上进行运算。不同的编译器对溢出处理是不同的。

CLR提供了检查和不检查溢出的运算操作码(如add,add.ovf)。在C#中可以通过check和uncheck来控制溢出时是否抛出异常。

类型转换是个非常复杂也很重要的的东西,我觉得要理解了不同的数据类型之间的关系,才能比较好的理解理解转型这个地方。


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注