.NET 框架程序设计 第Ⅱ部分 类型与通用语言运行时
类型基础
Object:所有类型的基类型
new操作符执行的工作:
1、从托管堆(managed heap)中分配指定类型所需数量的字节来作为存储其对象的内存空间
2、初始化对象的附加成员(overhead members)(方法表指针,SyncBlockIndex)
3、传入new操作符中指定的参数,调用类型的实例构造器
将对象转换为基类型被认为是安全的,C#允许隐式转型。而将对象转换成派生类型可能导致失败,C#要求显式转型
is,as 操作符(都不会抛出异常)
using 指示符(using Wintellect;),using 别名(using WintellectWidget=Wintellect.Widget;)
基元类型、引用类型与值类型
基元类型(Primitive Type)是编译器直接支持的数据类型,基元类型能够以文本常量的形式出现,如123.ToString()
基元类型之间的转换如果是安全的,C#允许进行隐式转型,这里“安全”的意思是指转换过程中不会造成数据丢失,如果转型存在潜在的“不安全”,C#要求显式转型。对于数值类型,“不安全”意味着可能会丢失精度或者数量级
checked,unchecked 操作符,checked,unchecked 语句(默认情况下溢出检查是关闭的)
checked,unchecked 作用相关IL指令:加法(add/add.ovf),减法(sub/sub.ovf),乘法(mul/mul.ovf),数据转换(conv/conv.ovf)
decimal是一个特殊的基元类型,因为CLR没有直接操作decimal值的IL指令(如上面的加、减、乘、数据转换),这意味着操作decimal值的代码效率会比操作其它的CLR基元类型的代码效率要低。另外,因为没有上述操作decimal的IL指令,所以checked和unchecked对它没有任何影响。如果对decimal值的操作没有安全的执行系统总会抛出System.OverflowException异常
引用类型(class),值类型:struct(派生自System.ValueType),enum(派生自System.Enum)
引用类型的一些性能事实:
1、内存必须从托管堆中分配
2、每个在托管堆中分配的对象都有一些与之相关联的额外附加成员必须被初始化
3、从托管堆中分配对象可能会导致执行垃圾收集
值类型实例通常分配在线程的堆栈上
在某些情况下值类型能够获得更好的性能。尤其是如果以下表述都正确的时候,就应该考虑用值类型:
1、该类型的行为类似于基元类型(不可变类型:没有提供可更改其字段的成员的类型)
2、该类型不需要继承自任何其它类型
3、该类型不会被任何其它类型继承
4、该类型的实例不会频繁的用于方法的参数传递。默认情况下,参数以传值的方式传递,这会导致值类型实例中的字段被拷贝,从而损伤应用程序的性能
5、该类型的实例不会作为方法的结果频繁的返回。从方法返回一个值类型也会导致实例中的字段被拷贝到调用者分配的内存中,因此也会损伤应用程序的性能
6、该类型的实例不会被频繁的用于诸如ArrayList、Hashtable之类的集合中。这些管理一组通用对象集合的类会对值类型实例执行装箱操作,这将导致额外的内存分配,以及额外的内存拷贝操作,从而也会损伤应用程序的性能(泛型集合没有这个问题)
值类型的主要优势在于它们不被分配在托管堆上。当然,相对于引用类型,值类型也有自己的一些限制。下面是值类型和引用类型之间的一些差别:
1、值类型对象有两种表示:一种是未装箱(unboxed)形式,一种是装箱(boxed)形式。引用类型总是装箱形式
2、值类型都继承自System.ValueType。System.ValueType没有在System.Object之外定义任何新的方法。但是,System.ValueType重写了其中的Equals方法。当两个ValueType对象的字段相互匹配时,Equals方法会返回true。另外,System.ValueType还重写了GetHashCode方法,它会使用对象的实例字段并根据一定的算法来产生一个散列码值。由于这个默认实现存在性能问题,当定义自己的值类型时,我们应该重写Equals方法和GetHashCode方法,为它们提供一个显式的实现
3、因为我们声明的值类型或引用类型不能以一个值类型作为基类,所以我们不能向值类型中引入任何新的虚方法。更不可能有任何抽象方法,所有的方法都隐含为密封(sealed)方法
4、引用类型变量包含着对象在托管堆中的内存地址。默认情况下,当一个引用类型变量被创建时,它被初始化为null,表示该引用类型变量目前没有指向一个有效的对象。试图使用null引用类型变量会抛出一个NullReferenceException异常。相反,值类型变量总是包含一个符合它的类型的值,并且所有的字段都被初始化为0值。当访问一个值类型时,不可能产生NullReferenceException异常
5、当将一个值类型变量赋值给另一个值类型变量时,会进行一个“字段对字段”的拷贝。但当一个引用类型变量赋值给另一个引用类型变量时,只会拷贝内存地址
6、因为前面一点,两个或多个引用类型变量可以指向托管堆中的同一个对象。这样对一个变量的操作将会影响到其它的变量引用的对象。另一方面,每个值类型变量都有一份自己的“对象”数据拷贝,对一个值类型变量的操作不可能影响到另一个
7、因为未装箱值类型没有分配在托管堆上,所以一旦定义该类型实例的方法不再处于活动状态,为它们分配的存储空间就会立即释放。这意味着值类型在内存被回收时不可能收到任何通知(通过Finalize方法)
装箱(boxing)(隐式),拆箱/拷贝(unboxing/copy)(显式)
装箱操作的步骤:
1、从托管堆中为新生成的引用类型分配内存
2、将值类型实例的字段拷贝到托管堆上新分配对象的内存中
3、返回托管堆中新分配对象的地址
拆箱和装箱并不是严格意义上的互反操作。拆箱操作的代价要比装箱操作小许多。拆箱操作仅仅是获取指向对象中包含的值类型部分(数据字段)的指针而已,它不会像装箱操作那样涉及到任何内存字节的拷贝。然而,紧接着拆箱之后典型的操作往往就是字段拷贝,这两个操作合起来与装箱操作才成为真正的互反操作
拆箱操作的步骤:
1、如果该引用为null,将会抛出一个NullReferenceException异常
2、如果该引用指向的对象不是一个期望的值类型的已装箱对象,将会抛出一个InvalidCastException异常
3、一个指向包含在已装箱对象中值类型部分的指针被返回
当对一个对象执行拆箱操作时,转型的结果必须是它原来未装箱时的类型:
Int32 x=5;
Object o=x;//对x进行装箱
Int16 y=(Int16)o;//抛出一个InvalidCastException异常
装箱实例解析1:
Int32 v=5;
Object o=v;//对v进行装箱
v=123;
Console.WriteLine(v+","+(Int32)o);//对v,(Int32)o进行装箱
Console.WriteLine(v+","+o);//对v进行装箱
Console.WriteLine(v.ToString()+","+o);//不进行装箱
装箱实例解析2:
Int32 v=5;
Console.WriteLine("{0},{1},{2}",v,v,v);//进行3次装箱
Object o=v;//对v进行装箱
Console.WriteLine("{0},{1},{2}",o,o,o);不进行装箱
装箱实例解析3:
struct Point:ICloneable{
public Int32 x,y;
//重写从System.ValueType继承来的ToString方法
public override String ToString(){
return String.Formate("({0},{1})",x,y);
}
//实现ICloneable接口的Clone方法
public Object Clone(){
return MemberwiseClone();
}
}
Point p;
p.x=10;
p.y=20;
Console.WriteLine(p.ToString());//不进行装行(调用自身实现的方法)
Console.WriteLine(p.GetType());//进行装箱(调用基类实现的方法)
Point p2=(Point)p.Clone();//不进行装行(调用自身实现的方法)
ICloneable c=p2;//进行装箱(接口类型总是引用类型)
Object o=c.Clone();//不进行装行(c已经经过装箱了)
Equals方法必须遵循的4条规则:
1、Equals方法必须是自反的,也就是说,x.Equals(x)必须返回true
2、Equals方法必须是对称的,也就是说,x.Equals(y)和y.Equals(x)必须返回同样的值
3、Equals方法必须是可传递的,也就是说,如果x.Equals(y)和y.Equals(z)都返回true,那么x.Equals(z)也必须返回true
4、Equals方法必须是前后一致的,也就是说,如果两个对象的值没有发生改变,多次调用Equals方法的返回值应该相同
正确实现Equals的方法(Object没有这么实现):
1、如果obj参数为null,就返回false
2、如果this和obj参数引用的是不同类型的对象,就返回false
3、针对类型定义的每个实例字段,将this对象中的值与obj对象中的值进行比较。任何字段不相等,就返回false
4、调用基类的Equals方法,以便比较它定义的任何字段。如果基类的Equals方法返回false,就返回false,否则返回true
实现Equals方法的3种模式:
1、为基类没有重写Object.Equals方法的引用类型实现Equals
class MyRefType:BaseType{
RefType refobj;//引用类型
ValType valobj;//值类型
public override Boolean Equals(Object obj){
if(obj==null) return false;
//不能用obj is MyRefType来判断,
//因为is操作符在判断一个对象与它的任何一个基类型之间的关系时,都会返回true
if(this.GetType()!=obj.GetType()) return false;
MyRefType other=(MyRefType)obj;
//不能用refobj.Equals(other.refobj),
//因为当refobj为null时会抛出NullReferenceException异常,
//而静态方法Object.Equals为我们做了正确的检查
if(!Object.Equals(refobj,other.refobj)) return false;
if(!valobj.Equals(other.valobj)) return false;
return true;
}
public static Boolean opeartor==(MyRefType o1,MyRefType o2){
return Object.Equals(o1,o2);
}
public static Boolean opeartor!=(MyRefType o1,MyRefType o2){
return !(o1==o2);
}
}
2、为基类重写了Object.Equals方法的引用类型实现Equals
class MyRefType:BaseType{
RefType refobj;//引用类型
ValType valobj;//值类型
public override Boolean Equals(Object obj){
//首先让基类型比较其中的字段,
//如果base.Equals会导致调用Object.Equals方法,那么就不应该再调用它
//如上面第1种实现就是这种情况
if(!base.Equals(obj)) return false;
if(obj==null) return false;
//不能用obj is MyRefType来判断,
//因为is操作符在判断一个对象与它的任何一个基类型之间的关系时,都会返回true
if(this.GetType()!=obj.GetType()) return false;
MyRefType other=(MyRefType)obj;
//不能用refobj.Equals(other.refobj),
//因为当refobj为null时会抛出NullReferenceException异常,
//而静态方法Object.Equals为我们做了正确的检查
if(!Object.Equals(refobj,other.refobj)) return false;
if(!valobj.Equals(other.valobj)) return false;
return true;
}
public static Boolean opeartor==(MyRefType o1,MyRefType o2){
return Object.Equals(o1,o2);
}
public static Boolean opeartor!=(MyRefType o1,MyRefType o2){
return !(o1==o2);
}
}
3、为值类型实现Equals方法
class MyValType:BaseType{
RefType refobj;//引用类型
ValType valobj;//值类型
public override Boolean Equals(Object obj){
if(!(obj is MyValType)) return false;
MyValType other=(MyValType)obj;
//不能用refobj.Equals(other.refobj),
//因为当refobj为null时会抛出NullReferenceException异常,
//而静态方法Object.Equals为我们做了正确的检查
if(!Object.Equals(refobj,other.refobj)) return false;
if(!valobj.Equals(other.valobj)) return false;
return true;
}
public static Boolean opeartor==(MyValType v1,MyValType v2){
return v1.Equals(v2);
}
public static Boolean opeartor!=(MyValType v1,MyValType v2){
return !(v1==v2);
}
}
判引用相等:一般用Object.RefereceEquals方法实现,而不用==操作符,因为==操作符可能被重载过了
选择散列码算法应该尽力遵循的原则:
1、算法应该使所得的数值有一个良好的随机分布,这样散列表可以获得最佳的性能
2、算法还可以调用基类型的GetHashCode方法,并将其返回值包含在我们自己的算法种。但一般不应该调用Object或者ValueType的GetHashCode方法,因为这两个类型的GetHashCode方法实现都不会获得高性能的散列算法
3、算法应该使用至少一个实例字段
4、理想情况下,我们在算法中使用的字段都应该是恒定不变的;也就是说在构造对象时字段被初始化后,他们就不应该再在对象的生存期内有任何改变
5、算法应该执行的尽可能快
6、有着相同值的对象应该返回相同的散列码
Equals,IEquatable<T>,==,!=
CompareTo,IComparable,IComparable<T>,<,<=,>,>=
浙公网安备 33010602011771号