【C#】质疑string,理解string,成为string

一个平平无奇的引用类型

首先我们知道,string属于引用类型。
对于一个平平无奇的引用类型,我们做下面的比较。

测试1:
在这里插入图片描述
在这里插入图片描述
结果显而易见,因为这段代码在堆上实际只创建了一个对象,无论_pointer_1、_pointer_2都是指向了这一个相同的对象,所以无论哪种比较,结果都是True。
而当我们实例化两份TestClass出来时,由于此时在堆上实际创建了两个对象,虽然这俩长得完全一样,但也得各论各的,因此无论哪种比较,结果都是False。

测试2:
在这里插入图片描述
在这里插入图片描述

因为我们知道,string属于引用类型。
那么对string做同样的测试,讲道理应该得到同样的结果,于是我们又做了下面的测试。

测试3:
在这里插入图片描述
在这里插入图片描述
然而结果并不是。可以看到,二者的确是不同的引用,但为什么进行==比较却返回true呢?这是因为string对 == 和 != 做了运算符重载。其实际是调用了string.Equals() 方法。而string.Equals()方法实际是对其对应的字符数组进行逐位比较(类似)。因此只要两个字符串字面量一致,那么不管他们是怎么来的,进行 == 比较时,都会认为是相同的。
在这里插入图片描述
这里插一句。我之前在网上曾经看到过 “比较字符串时不能直接用 == 比较,要用A.Equals(B),因为字符串的 == 比较更慢” 的说法。这显然是不对的,甚至如果非要纠结这一点性能差异的话,由于 == 在编译时被替换成的是静态方法string.Equals() ,理论上速度比 A.Equals(B) 反倒还要快那么一微微(call和callvirt的区别)。
在这里插入图片描述

一个出类拔萃的引用类型

除了使用new关键字之外,很多时候我们是直接通过以字面量赋值的方式创建一个字符串,那么这两种情况是否是一样的呢?不妨再做一次实验。

测试4:
在这里插入图片描述
在这里插入图片描述
结果我们发现,通过字面量直接赋值的方式创建的两个字符串,其索引是一样的。为啥呢?这就要从字符串的存储方式说起。

(叠甲:以下的内容是参考的JAVA的资料,C#可能不完全一样,但整体思想应该是一致的)
我们的程序在内存中有三个很重要的部分,分别是栈区、堆区和常量区,我们创建的引用对象一般都是存放在堆区中,在栈区存放的只是这个对象的地址。所以在测试1中,虽然我们声明了三个变量_testClass、_pointer_1、_pointer_2,但三者其实都是指向了堆区中的同一个对象,因此引用是相同的。而无论是测试2中的_testClass_1和_testClass_2,还是测试3中的_str_1和_str_2,都通过new关键字在堆区中创建了一个新对象,并各自指向自己创建的这个新对象,因此引用也各不相同。
而通过字面量创建字符串时略有不同。这是因为在常量区中有一个字符串常量池,其结构可以类似理解为一个散列表(我说类似哈),里面每一个桶对应一个链表,链表的每一个节点记录着这个节点对应的字符串字面量的哈希值(实际并不是哈希值,领会精神吧)以及这个字面量关联到的堆区对象。当我们用字面量创建字符串时,会先根据字面量计算出哈希值,然后去散列表里找到对应的桶,然后遍历链表找是否有相同哈希值的节点,如果有,就返回这个节点记录的堆区对象的地址,如果没有,才会真的在堆区创建新的字符串对象,同时在链表里新增一个节点作为记录(也就是将这个字面量添加到字符串常量池中)。
在这里插入图片描述
通过IL代码我们也可以看到,字面量创建字符串时,是通过ldstr实现的,而通过new关键字创建字符串则会显式通过newobj创建新对象。
在这里插入图片描述
另外,这个过程是在编译阶段就已经完成的,并不是在运行时才进行,比如下边这个实验。

测试5:
在这里插入图片描述
在这里插入图片描述
可以看到,_str_1拼接的结果“12”是可以在字符串常量池中找到的,而拼接的中间项“1”在常量池中并不存在。
在这里插入图片描述
通过IL代码也可以看到,实际ldstr的对象是字面量“12”。

既然是编译时进行的,那对于下面这个实验,也就好理解了。

测试6:
在这里插入图片描述
在这里插入图片描述
在这个测试中,_str_4和_str_5在编译后,实际都是ldstr “34”,因此只会创建一个堆区对象,因此两者引用相同。而_str_1和_str_3虽然结果都是“12”,但由于_str_3的拼接过程涉及到变量_str_2,因此在编译时并不会提前确定其结果,而是在运行时通过string的Concat方法动态执行的。
在这里插入图片描述

运行时动态执行生成的字符串都是不会放到常量池里的,当然你也可以手动将其放进去,比如像下面这样。
在这里插入图片描述
在这里插入图片描述
在_str_2动态拼接出“12”后,我们通过string.Intern()方法手动将其放到了常量池中,因此_str_3和_str_4就会指向同一个堆区对象。不过整体来讲意义不大,因为_str_2的拼接过程仍然产生了一个新对象。

简单提下StringBuilder

通过上面的内容我们应该已经基本明白为什么在写代码时非常不提倡重复进行大量字符串拼接,就是因为这会产生大量的新对象。那StringBuilder为什么在这种情况下会效率更高呢?
我们知道string虽然是引用类型,但却是不可变的引用类型,这样才不会出现修改了一个string之后所有其他的string都跟着改变的情况。string存储的值虽然是一个char数组,但当我们在运行时对string做任何修改的时候,并不会直接修改这个数组,而是会创建一个新的对象,新对象的char数组记录着新的值。所以如果我们对一个字符串进行疯狂拼接,其实就是在疯狂创建新对象。
StringBuilder也通过一个char数组来记录值,但通过Append或Insert等方法修改时,是直接对当前数组进行修改,在不考虑数组扩容的情况下,并不会带来额外创建对象的消耗。
说到这里也可以看出来,其实StringBuilder也并不是任何情况下都比直接修改字符串效率高。比如我的逻辑只需要对字符串进行一次拼接就完事了,直接用string + string固然会产生新对象,但是创建StringBuilder也会有消耗的啊。而且跟JAVA不同,C#的StringBuilder还是线程安全的,执行效率本身也没有那么高,所以在这种特别简单的情况下,还非得用StringBuilder来处理就得不偿失了。
在这里插入图片描述

写不动了,就这样吧。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值