public struct Point { private int m_x, m_y; public Point(int x, int y) { m_x = x; m_y = y; } public override string ToString() { return string.Format("{0},{1}", m_x, m_y); } }
上面是一个值类型的定义,下面创建一个实例,用在控制台上输出一些信息:
Point p = new Point(1, 1); Console.WriteLine(p);
这与
Point p = new Point(1, 1); Console.WriteLine(p.ToString());
这二者在输出结果上完全一样,也许很多人象我一样,在平时工作中随意使用,也不会去管它有什么不同?
但其实,Console.WriteLine(p)是会产生装箱(box)指令的!
原因很简单:Console.WriteLine的所有重载版本中,并没有一个Console.WriteLine(Point p)的版本,所以默认会调用Console.WriteLine(Object o)这个版本,p会装箱成Object,返回一个在堆上的引用。
而Console.WriteLine(p.ToString())则会调用Console.WriteLine(String s)这个重载版本,p.ToString()已经是一个String了,所以无需装箱。
继续来看一段稍微长一点的代码:
using System; namespace boxTest { class Program { static void Main(string[] args) { int i = 1; test(5); Console.WriteLine(i);//1 object obj = 1; test(obj); Console.WriteLine(obj);//1 string s = "1"; test(s); Console.WriteLine(s);//"1" P1 p1 = new P1(1); test(p1); Console.WriteLine(p1.X);//1 P2 p2 = new P2(1); test(p2); Console.WriteLine(p2.X);//5 Console.Read(); } static void test(int i) { i = 5; } static void test(object o) { o = 5; } static void test(string s) { s = "5"; } static void test(P1 p) { p.X = 5; } static void test(P2 p) { p.X = 5; } } internal struct P1 { private int _x; public P1(int x) { _x = x; } public int X { set { _x = value; } get { return _x; } } } internal class P2 { private int _x; public P2(int x) { _x = x; } public int X { set { _x = value; } get { return _x; } } } }
上面代码的5次输出结果,您都猜对了吗?
第1次输出:因为i是值类型,参数传递默认是按值传递的,也就是说test方法体里的参数i是一个全新的副本,跟外界没关系,方法调用完后,方法体内的i自动被清理,不影响方法体外的i
第2次输出:虽然Object是引用类型,参数传递也是按引用传递的,但是方法体内o=5的赋值,使o指向了一个全新的"已装箱的5",这时o与方法体外的obj已经是二个不同的对象了,有怀疑的同学,可用Object.ReferenceEquals方法输出验证,如下面这样
static void test(object o) { object o1 = o; Console.WriteLine(Object.ReferenceEquals(o1, o));//true o = 5; Console.WriteLine(Object.ReferenceEquals(o1, o));//false }
但是在test(Object o)调用完成后,main方法后面还要继续使用obj(因为有Console.WriteLine(obj)),所以obj此时也不会被列为垃圾回收的目标。test方法调用结束后,方法体内部的对象o,因不再使用将等候GC回收。
第3次输出:String虽然也是引用类型,但是String的处理机制有别于其它引用类型(这个话题展开就可再写一篇文章了,建议不清楚的同学去CLR VIR C#中的"字符、字符串和文本处理"相关内容),在test(String s)内对s赋值为新字符串时,同样会生成一个新的对象,因此也不会影响到test方法体外的值。但是:跟第2次输出不同的是,test(String s)调用结束后,字符串"5"却不会被立即回收(即:字符串驻留机制),如果下次有人需要再次使用字符串"5",将直接返回这个对象的引用,这一点可通过观察对象的HashCode看出端倪:
using System; namespace boxTest { class Program { static void Main(string[] args) { string s = "1"; test(s); string s1 = "1"; string s2 = "5"; Console.WriteLine("{0},{1},{2}", s.GetHashCode(), s1.GetHashCode(), s2.GetHashCode()); Console.Read(); } static void test(string s) { Console.WriteLine("{0}", s.GetHashCode()); s = "5"; Console.WriteLine("{0}", s.GetHashCode()); } } }
输出结果为:
-842352753
-842352757
-842352753,-842352753,-842352757
第4次输出:struct类型的P1是值类型,类似第1次输出中的解释一样,按值传递,方法体内修改的只是副本的值,也不会影响test体外的值.
第5次输出:class类型的P2是引用类型,参数传递的其实是p2的地址(即指针),而且在test方法体内并未对p2重新赋值(指没有类似p2 = new P2(1)类似的代码),而只是修改了p2的属性X,方法调用结束后,p2引用指向的地址没有改变,但是这个地址中对应的值X已经变了,所以输出5.
最后再来二个CLR VIR C#原书示例的简化版
using System; namespace boxTest { class Program { static void Main(string[] args) { P p1 = new P(1); Console.WriteLine(p1);//1 p1.ChangeX(2); Console.WriteLine(p1);//2 object o = p1; ((P)o).ChangeX(5); Console.WriteLine(o);//这里将输出2,而不是5 ! //解释:((P)o).ChangeX(5); //其实相当于 P p2 = (P)o; p2.ChangeX(5); //所以根本没改变p1中的_x值(因为P是值类型,p2与p1在内存中对应的是二个不同的地址,相互并不干扰), //然后临时生成的p2因为不再被使用,Main方法执行完成后,会自动清理 Console.Read(); } } struct P { private int _x; public P(int i) { _x = i; } public void ChangeX(int x) { _x = x; } public override string ToString() { return string.Format("{0}", _x); } } }
using System; namespace boxTest { class Program { static void Main(string[] args) { P p1 = new P(1); Console.WriteLine(p1);//1 p1.ChangeX(2); Console.WriteLine(p1);//2 object o = p1; ((IChangeX)o).ChangeX(5); Console.WriteLine(o);//这里将输出5 //解释: ((IChangeX)o).ChangeX(5); 相当于 //IChangeX _temp = (IChangeX)o; //_temp.ChangeX(5); //因为接口实际上返回的是引用(算是引用类型), //所以这时_temp与o指向的是同一个内存地址,修改_temp就相当于修改o Console.Read(); } } struct P :IChangeX { private int _x; public P(int i) { _x = i; } public void ChangeX(int x) { _x = x; } public override string ToString() { return string.Format("{0}", _x); } } interface IChangeX { void ChangeX(int x); } }
让struct实现一个接口以后,情况就变了,同样大家看注释,不解释。
要想写出高性能的代码,每个细节都要意识到背后发生的事情。所以象CLR VIR C#这类神作,没事拿来翻翻,不断加深印象还是很有必要的。