string的深入理解

时间:2022-03-03 15:58:50

本文只是个人总结见解,勿喷

首先肯定的是string是引用类型

string s_a = "yhc";

string s_b = s_a;

if(s_a.Equals(s_b))

    Console.WriteLine("相同?");

else

Console.WriteLine("不相同");

 

输出是“相同”,让s_b=s_a,本质是让s_b指向了yhc在堆上的存储位置,此时s_a和s_b都指向了yhc在堆上的存储位置。


理论上说如果修改s_a的值,例如s_a=”wq”,b也会跟着变化,但是实际上不是的,在c#里面,如果修改其中一个string对象的值比如s_a,系统会为s_a再重新申请一块内存用来存放修改后的s_a的值wq,而b依然指向于a曾今的内存位置。

 

这就是为什么在如果要频繁对string操作的时候建议用stringbuilder,因为stringbuilder类似c++中的标准库string,是可以动态增加内存大小的,而c#中的string是固定大小,你string一个对象就给你分配一个固定大小的内存空间,这个对象一旦创建就无法更改,如果你要往里面增加内容,我就必须新建一个string对象,把值拷贝进来,然后把之前的string对象删除,频繁操作string会带来效率的降低,毕竟创建对象,分配内存,删除对象这个过程需要耗时耗资源。


我们可以通过比较内存地址来断定这一结论(ReferenceEquals静态方法用于比较对象的引用是否一致,而不是简单的值),如果s_a和s_b指向的是同一地址,这里的比较应该返回相同
s_a = "wq";
if (ReferenceEquals(s_a,s_b))

Console.WriteLine("相同");

else

Console.WriteLine("不相同");


答案很明显,会输出“不相同”,这说明系统为s_a重新分配了新地址的内存空间,当然你也许会说为什么不用==,因为string的= =被重写后,其实比较的还是string的值,(当然如果你一定要用= =也是可以的,只不过需要装箱成object类型,让==不再为string的特例重写比较,即if(((object)s_a)==( (object)s_b))),但是= =在比较其他对象的时候可不是这样了,这是string这个引用类型表现出值类型特征的一点,其实还有一个地方也表现了string类型的值类型特征表现,就是在string类型作为参数传递的时候,传递的是string对象的引用地址,但是在传递过去的方法里面修改string对象的值却不影响本体,这是因为系统根据传送过来的地址重新构造分配了一个全新的string对象,比如调用方法 void fun(string a);并把s_a传递作为参数过来的时候,a会被系统重新分配一个地址,而不是指向s_a的地址,这是引用类型的一个特例。比如A是一个普通类,比如
A b=new A();
b.name=”yhc”;
fun(b);

//代码运行到此,如果输出,此时输出a的name值:为wq
void fun(A a)

{

//代码运行到此,如果输出,此时输出a的name值:为yhc

a.name=”wq”;//因为参数传递的是b的引用,对a的操作就是对b的操作

//代码运行到此,如果输出,此时输出a的name值:为wq

}


但是如果是这样:

A b=new A();
b.name=”yhc”;
fun(b);

//代码运行到此,如果输出,此时输出a的name值:为yhc
void fun(A a)

{

//代码运行到此,如果输出,此时输出a的name值:为yhc

a=new A();

a.name=”wq”;

//代码运行到此,如果输出,此时输出a的name值:为wq

}


在执行玩fun(b)后的输出,就不再是wq了,因为在方法fun内部,我们把a指向了一个新建立在堆上的对象A,这个new对象在离开函数方法后会被GC垃圾回收机制回收,什么时候来回收就不知道了,当然如果是建立在堆栈上的资源会被立刻释放,但是关于建立在堆上的对象的GC回收也有特例,就是那些非托管对象,比如什么SqlConnection数据库连接、文件句柄、网络连接什么的,这些对象在方法内部new后,离开方法后不会被GC回收。

 

上面说的跑远里,我们回到之前,当然有人会说用ReferenceEquals来比较不够深刻和准确性,因为s_a改为wq后,值也发生了变化,用ReferenceEquals比较无法判断是值不同还是引用不同,于是我们如下修改:按照string修改后系统会自动为其分配新的内存空间的原理我们把s_a修改成yhc,即虽然修改了,但是值还是保持原来的yhc,只为了测试这个修改操作有没有重新分配内存空间

s_a = "yhc";
if (ReferenceEquals(s_a,s_b))

Console.WriteLine("相同");

else

Console.WriteLine("不相同");
我们都以为输出会是“不相同”,但是,但是结果让我们失望,系统输出“相同”,原因是微软的CLR使用了优化技术,叫做字符串驻留技术,这个技术的原理大家可能都知道,就是CLR初始化的时候会创建一个内部散列表,表为键值形式,键为字符串,比如这里的yhc,值为yhc在堆内存上存储的地址,初始化的时候散列表肯定为空了,即时编译器(JIT)编译时先去散列表找字符串yhc,第一次找没找到yhc,其就会在堆上开辟空间来存放yhc,然后把存放的地址和yhc这个字符串存储在散列表中,然后第二次又去找,就是这里的修改s_a的值为yhc,发现在散列表的键中找到了yhc这个值,于是就不再分配内存来存放修改后的yhc,而是让s_a依然指向之前这个yhc在堆上存放的空间,到此也就是说,你修改后的s_a=“yhc”,虽然修改了,但是其值没有变,还是yhc,所以被编译器的优化机制优化了,所以这个测试还是测试不出来我们要的效果。

 

上面测试缺陷的原理我们知道了,但是我们差点忘了我们到底是要测试什么,我们要测试的是s_a或者s_b修改后,系统为其重新分配内存空间,我们要比较修改后两者的空间地址是否一致,于是总结上面教训如下操作:

s_a=string.Copy(s_b)
if (ReferenceEquals(s_a,s_b))

Console.WriteLine("相同");

else

Console.WriteLine("不相同");

 

这里,s_a=string.Copy(s_b),string.Copy方法是创建一个与指定的s_b具有相同值的 System.String 的实例。注意是新实例,用这个方法则编译器不会做上面的字符串驻留技术优化,即此时s_a再次被修改了,修改的值是拷贝了s_b的的值(s_b的值为yhc),即此时s_a的值也是yhc,这时候系统输出“不相同”了,这就验证了我们string对象被修改后,系统会为他重新分配内存空间。

 

完整测试代码如下:
测试很简单,但是深究很多
string s_a = "yhc";

string s_b = s_a;

s_a = string.Copy(s_b);           

if (ReferenceEquals(s_a,s_b))

     Console.WriteLine("相同");

else

     Console.WriteLine("不相同");

Console.Read();