最近面试被问到一个问题,在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其实是指向堆里面的同一个位置。