为了避免本文误导大家,首先声明:在面向对象语言中探讨这些没有太大的意义,但是它可以帮助我们更好的理解.NET语言特性。本文以C#为例,会涉及.NET中的克隆(浅复制)。
关于这个讨论,是由合作开发引起的。当时在DBHelper层里使用了new关键字创建一个数据库表对象(DataTable),而我在数据访问层(DAL)接收数据库表对象时没有加关键字new,只是声明了一个类型为DataTable的“对象变量”:DataTable dt;而没有这样声明:DataTable dt = new DataTable();究竟这样写对不对呢?
废话不多说,直接说原理:地球人都知道,要想实例化一个对象,必须用关键字new,例如:DataTable dt = new DataTable();。这个new就起到一个分配内存的作用,当编译器读到关键字new时,便在内存中开辟一块空间,用来放置类的实例,而dt中存放的仅仅是这块内存空间的首地址,存放的并不是对象,一个对象怎么可能存在一个变量里?其实,这个dt默认是int32型的,它存放的仅仅是一个地址,也可以说成指针。但是如果我们不用new,直接DataTable dt;,这样仅仅是声明了一个变量,他的类型为DataTable,说明这个变量可以容纳DataTable实例的首地址,但实际上dt仅仅是一个int32型的变量。所以类似:
DataTable dt1 = new DataTable();//实例化类 DataTable dt2; //定义一个对象,但是不实例化 dt2 = dt1; //把dt1中存放的实例的首地址,传给dt2
这样的写法,我们发现dt1和dt2都可以完美的完成功能,表面上看有两个实例,但实际上只有一个实例!dt2只不过是引用了dt1中存放的实例地址!dt1和dt2在使用同一个实例,使用同一块内存地址。所以,它们是互相影响的,假如dt1被强制销毁,那么dt2也无法正常使用(有兴趣的读者可以自己试一下,在这就不举例了)。其实,我们可以很不正规的说:对象是指内存中的某片区域,而并不是你定义的“对象变量”。
如果你学过C语言,能轻易理解我上边所说的。但是又有人说:这其实就是.NET中的克隆---浅复制。这是大错特错的!当时我也没仔细想,就冒然认同了,但后来仔细一想,发现这根本是两个概念。克隆(浅复制)的定义:“创建当前对象的浅表副本,方法是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。如果字段是值类型的,则对该字段执行逐位复制。如果字段是引用类型,则复制引用但不复制引用的对象”。至此,读者可以发现,克隆实际上是创建了一个“不完整的对象”,和上边所讲的引用地址根本不是一回事。克隆首先就创建了一个新对象,也就是已经开辟了内存空间,然后,把被克隆对象中数值类型的字段(堆栈数据,可以简单的理解为数值类型局部变量)复制到新对象的内存空间,对于被克隆对象中的引用类型字段(也就是对象),只复制引用(复制其所在的内存地址),并不开辟内存空间,这个原理和上边讲的是一致的。简单的说:克隆复制了数值型数据,引用了对象。而上边提到的写法是完全引用。
为了更加具有说服力,让代码运行结果说明一切:
clsTestB类:
class clsTestB { private string strByVal;//数值型字段 public string byval { get { return strByVal; } set { strByVal = value; } } }
clsTestA类,实现了克隆接口:
class clsTestA:ICloneable { private string strByVal; //数值型字段 private clsTestB clstestb = new clsTestB(); //引用型字段 //byval属性访问数值型字段 public string byval { get { return strByVal; } set { strByVal = value; } } //byref属性访问引用型字段 public string byref { get { return clstestb.byval; } set { clstestb.byval = value; } } //实现克隆接口 public Object Clone() { return (Object)this.MemberwiseClone(); } }
clsTestA clstest1 = new clsTestA(); //创建clsTestA类新实例 clsTestA clstest2; //定义clsTestA类“对象变量” clstest1.byval = "0"; //设定clstest1数值属性初始值 clstest1.byref = "0"; //设定clstest1引用属性初始值 clstest2 = clstest1;// clstest2完全引用clstest1指向的地址,并没有创建新实例 clstest1.byval = "byval";//改变clstest1数值属性初始值 clstest1.byref = "byref";//改变clstest1引用属性初始值 MessageBox.Show(clstest2.byval + "|" + clstest2.byref);
这段代码输出结果为:byval|byref。分析:clstest2引用clstest1后,clstest1改变了数值属性和引用属性,clstest2的相应属性也随之改变,说明无论是数值型字段还是引用型字段,clstest2完全引用clstest1和clstest2其实就是一个东西。
克隆示例代码:
clsTestA clstest1 = new clsTestA(); //创建clsTestA类新实例 clsTestA clstest2; //定义clsTestA类“对象变量” clstest1.byval = "0"; //设定clstest1数值属性初始值 clstest1.byref = "0"; //设定clstest1引用属性初始值 clstest2 = (clsTestA)clstest1.Clone();//clstest1通过克隆对clstest2赋值 clstest1.byval = "byval";//改变clstest1数值属性初始值 clstest1.byref = "byref";//改变clstest1引用属性初始值 MessageBox.Show(clstest2.byval + "|" + clstest2.byref);
这段代码输出结果为:0|byref。分析:clstest2是通过clstest1克隆而来的,clstest2已经是一个新实例,clstest2中的数值型属性由clstest1中复制过来,已经不再受clstest1影响;而clstest2中的引用型属性是直接引用的clstest1,所以当clstest1中的引用属性改变时,会影响clstest2中的相应属性。
综上所述,我们甚至还可以这样写:DataTable dt1 = new DataTable();//实例化类 DataTable dt2; //定义一个对象,但是不实例化 DataTable dt3; //定义一个对象,但是不实例化 dt2 = dt1; //把dt1中存放的实例的首地址,传给dt2 dt3 = dt2; //把dt2中存放的实例的首地址,传给dt3
这样的写法完全是可以的!只要实例化一次,以后即使不实例化,也可以直接引用。有人可能会有疑问:当我们使用dt3时,dt1被销毁了怎么办?您尽管放心,.NET的托管机制是不会让一个正在被引用的内存地址释放的。dt1、dt2、dt3有任何一个“对象”正在被使用,他们所指向的地址就不会被释放。(如果想了解为什么不会被释放,请参阅《Windows核心编程》)
当然,我们要养成一个良好的习惯,实例化对象都要加new。毕竟我们使用的是面向对象的语言,而不是底层语言,不必过多考虑这些问题,实例化对象的时候记得加上一个new,保证不会出问题!