装箱与值类型虽然很容易理解,但是在实际使用中,并不总是能100%用对

    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#这类神作,没事拿来翻翻,不断加深印象还是很有必要的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏菩提树下的杨过

java学习:字符串比较“==”与“equals”的差异及与c#的区别

.net中,其字符串特有的驻留机制,保证了在同一进程中,相同字符序列的字符串,只有一个实例,这样能避免相同内容的字符串重复实例化,以减少性能开销。 先来回顾一下...

2568
来自专栏技术博客

编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]

  本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:

933
来自专栏GreenLeaves

C# 自定义类型通过实现IFormattable接口,来输出指定的格式和语言文化的字符串(例:DateTime)

在开发一些国际化的应用时,应用程序需要调用与当前线程不同的语言文化来格式化字符串.

1223
来自专栏技术博客

C#多线程

根据上一节中http://www.cnblogs.com/aehyok/archive/2013/05/02/3054615.html对多线程的入门了解。本节就...

862
来自专栏跟着阿笨一起玩NET

嘿,原来不认识你,想不到你这么好用—说说.NET中被我忽视的方法

下面就说说被我忽视过的方法。当然,每个人的编程经历,涉猎面及对.NET的认知程度都不一样。所以,这只是一家之言,肯定有很多不足之处,欢迎大家批评指正。

691
来自专栏跟着阿笨一起玩NET

DataGridView绑定BindingList<T>带数据排序的类

本文章转载:http://yuyingying1986.blog.hexun.com/30905610_d.html

651
来自专栏林德熙的博客

win10 uwp unix timestamp 时间戳 转 DateTime

有时候需要把网络的 unix timestamp 转为 C# 的 DateTime ,在 UWP 可以如何转换?

661
来自专栏用户3030674的专栏

Java中Json解析

首先准备一个JSON格式的字符串 * String JsonStr = "{object:{persons:" + "[{name:'呵呵',im...

1942
来自专栏林德熙的博客

C# 16 进制字符串转 int

最近在写硬件,发现有一些测试是做 16 进制的字符串,需要把他转换为整形才可以处理。 本文告诉大家如何从 16 进制转整形。

1731
来自专栏前端侠2.0

学习表达式树笔记 原

文章地址:  http://www.cnblogs.com/Ninputer/archive/2009/08/28/expression_tree1.html

1002

扫码关注云+社区

领取腾讯云代金券