从C#中的引用类型到String

来源:互联网 发布:淘宝助理有什么作用 编辑:程序博客网 时间:2024/06/05 10:03

          最近面试被问到一个问题,在C#里面String是值类型还是引用类型,当时想都没想就说是引用类型。后来面试官又接着问为啥,我就给愣住了,随口说了个可以为Null就不会了。下来仔细想了想这个问题。

         首先,要说明string为什么是引用类型,先来考虑下什么是引用类型,什么是值类型,以及他们的区别是什么。在网上随便搜搜基本上的解释都是,值类型存储在栈上,引用类型存储在堆上。这句话基本上说明不了什么。

         在仔细想想,为什么当初设计C#语言的时候要分为值类型和引用类型呢。全都搞成一样不是更简单么。可能就是因为在c#里面取消了c++里的指针,而通过引用类型可以实现之前指针可以实现的功能。(不太懂指针,所以就不多说了)我能想到的区别就是在函数传参的时候,比如我有一个函数

void main(int b)        {            b = 1;            b++;            int c = 101;            b = c;        }

我在调用他时传了一个参数a ,  可以这样写  

int a = 100;main(a);

结果就是运行完这个函数后,a的值不会改变,那么其实我是把a的值(比如说100)给了函数,然后函数里面可以用这个值进行计算之类的,不管函数里面怎么变都不会改变a本身,这也就是函数传参时的形参和实参的区别,不管参数是什么类型的(值类型或者引用类型),在给一个函数传参的时候都是把实参(比如上面的a)的值取出来赋给形参(b),形参其实就是一个局部变量,在函数被调用时创建,在函数运行完后销毁。但如果有一天我写了一个程序

int a = 100;a = 10;Console.Write(a);
我想把a=10;这句写在一个函数里,方便以后重复调用(是不是有点多此一举....),如果按照上面的写法肯定是不行的,因为函数里的形参的改变是不会影响外面的a。在c++里面好像可以用取地址符(&)传参实现。但是标准的c#里面没有这个东西了,怎么办呢。这个时候就要用到引用类型了。可以这么写把我想要改变的变量封装到一个类里面。

class A        {           public   int a1 = 0;           public int a2 = 0;           public int a3 = 0;        }        static void Main(string[] args)        {            A a = new A();            a.a1 = 1;            a.a2 = 2;            a.a3 = 3;            Console.WriteLine(a.a1 + " " + a.a2 + " " + a.a3);            main(a);            Console.WriteLine(a.a1 + " " + a.a2 + " " + a.a3);            Console.ReadLine();        }        static void main(A b)        {            b.a1 = 10;            b.a2 = 100;            b.a3 = 200;        }
所有的类都是引用类型的。在传参的时候虽然也是通过复制值传递,但是由于引用类型里面存的其实是一个地址,所以传递值的时候其实也是传递的地址值,但在实际操作的时候会根据地址找的具体的值(a1,a2,a3)的位置然后操作。在内存里过程如图:


上面代码里首先实例化一个对象a,a的具体值(a1,a2,a3)会存在堆里(假设地址是0001),然后在栈里的一个位置存下这个地址0001,a就是指向栈里的这个位置。当把a当参数传给函数时,函数会实例化一个形参b(在栈里,并没有在堆里分配新的空间)然后将a的值(0001)复制给b。由于传的值是一个地址0001,所以在实际操作b的时候就是操作堆里0001位置的值。也就相当于操作了a的值。

其实在函数传参的时候还有引用传递的情况,运用关键字ref和out。

但这些都是书上说的理论,感觉总有点不真实,于是写一段代码验证下:

class A        {            public int a1 = 0;            public int a2 = 0;            public int a3 = 0;        }        static A a = new A();        static int aa ;        static void Main(string[] args)        {            a.a1 = 1;            a.a2 = 2;            a.a3 = 3;            aa = 11;            Console.WriteLine(a.a1 + " " + a.a2 + " " + a.a3);            main(a,aa);            Console.WriteLine(a.a1 + " " + a.a2 + " " + a.a3);            Console.ReadLine();        }        static void main(A b,int bb)        {            <strong>Console.WriteLine ( Object.ReferenceEquals(b, a));          Console.WriteLine ( Object.ReferenceEquals(bb, aa));</strong>            b.a1 = 10;            b.a2 = 100;            b.a3 = 200;        }
运行的结果是:

Object.ReferenceEquals方法用来判断两个对象是不是引用同一个东西。

可以看到值类型A在函数传参以后指向的还是以前的对象,而值类型int则不是。

然后再试试string类型

static string  str;        static void Main(string[] args)        {            str = "aaa";            main(str);            Console.ReadLine();        }        static void main(string a)        {            Console.WriteLine ( Object.ReferenceEquals(a, str));        }
结果果然还是true,看来string确定是引用类型无疑了。

关于string类型还有一些很神奇的,比如string类型的值是readonly的,如果要改变一个string对象的值,那么其实是新建了一个string对象。

            string str = "aaa";            string str1 = str;            Console.WriteLine(Object.ReferenceEquals(str1, str));            str1 = "aaaa";            Console.WriteLine(Object.ReferenceEquals(str1, str));            Console.ReadLine();
结果是true  和false   所以在频繁改变字符串值的时候还是用stringbuilder比较好。

还有一点比较神奇的是,CLR会自动给string类型建立一张记录表,每当新初始化一个string对象时,先查记录表,如果已经存在相同的字符串,则直接指向它的位置,这样就不用再次分配空间了。例如

            string str = "aaa";            string str1 = "aaa";            Console.WriteLine(Object.ReferenceEquals(str1, str));
这样的代码结果也是true,str和str1其实是指向堆里面的同一个位置。




0 0
原创粉丝点击