这里引用博客园hypo106的一遍文章,原文章地址:.NET基础知识-string、StringBuilder、字符串操作 - hypo106 - 博客园 (cnblogs.com)
为了地址内容失效,在这里也把文章内容贴出来。(原作者如果觉得侵权的,可以联系删除)
字符串可以说是C#开发中最常用的类型了,也是对系统性能影响很关键的类型,熟练掌握字符串的操作非常重要。
认识string
string:是一个特殊的引用类型,使用上有点像值类型。之所以特殊,也主要是因为string太常用了,为了提高性能及开发方便,对string做了特殊处理,给予了一些专用特性。为了弥补string在字符串连接操作上的一些性能不足,便有了StringBuilder。
首先需要明确的,string是一个引用类型,其对象值存储在托管堆中。string的内部是一个char集合,他的长度Length就是字符char数组的字符个数。string不允许使用new string()的方式创建实例,而是另一种更简单的语法,直接赋值(string aa= “000”这一点也类似值类型)。
public static void DoStringTest() { string a = "123"; SetStringValue(a); Console.WriteLine(a); //123 Console.ReadKey(); } private static void SetStringValue(string t) { t += "abc"; }
输出结果:123
通过前面的值类型与引用类型的文章,我们知道string是一个引用类型,既然是一个引用类型,参数传递的是引用地址,那为什么不是输出“000111”呢?是不是很有值类型的特点呢!这一切的原因源于string类型的两个重要的特性:恒定性与驻留性
String的恒定性(不变性)
字符串是不可变的,字符串一经创建,就不会改变,任何改变都会产生新的字符串。比如下面的代码,堆上先创建了字符串s1=”a”,加上一个字符串“b”后,堆上会存在三个个字符串实例,如下图所示:
string s1 = "a"; s1 = s1 + "b";
上文中的”任何改变都会产生新的字符串“,包括字符串的一些操作函数,如str1.ToLower,Trim(),Remove(int startIndex, int count),ToUpper()等,都会产生新的字符串,因此在很多编程实践中,对于字符串忽略大小的比较:
if(str1.ToLower()==str2.ToLower()) //这种方式会产生新的字符串,不推荐 if(string.Compare(str1,str2,true)) //这种方式性能更好
String的驻留性
由于字符串的不变性,在大量使用字符串操作时,会导致创建大量的字符串对象,带来极大的性能损失。因此CLR又给string提供另外一个法宝,就是字符串驻留,先看看下面的代码,字符串s1、s2竟然是同一个对象!
var s1 = "123"; var s2 = "123"; Console.WriteLine(System.Object.Equals(s1, s2)); //输出 True Console.WriteLine(System.Object.ReferenceEquals(s1, s2)); //输出 True
相同的字符串在内存(堆)中只分配一次,第二次申请字符串时,发现已经有该字符串是,直接返回已有字符串的地址,这就是驻留的基本过程。
字符串驻留的基本原理:
如果大量的字符串都驻留到内存里,而得不到释放,不是很容易造成内存爆炸吗,当然不会了?因为不是任何字符串都会驻留,只有通过IL指令ldstr
创建的字符串才会留用。
字符串创建的有多种方式,如下面的代码:
var s1 = "123"; var s2 = s1 + "abc"; var s3 = string.Concat(s1, s2); var s4 = 123.ToString(); var s5 = s2.ToUpper();
其IL代码如下
在上面的代码中,出现两个字符串常量,“123”和“abc”,这个两个常量字符串在IL代码中都是通过IL指令ldstr
创建的,只有该指令创建的字符串才会被驻留,其他方式产生新的字符串都不会被驻留,也就不会共享字符串了,会被GC正常回收。
那该如何来验证字符串是否驻留呢,string类提供两个静态方法:
请看下面的示例代码:
认识StringBuilder
大量的编程实践和意见中,都说大量字符串连接操作,应该使用StringBuilder。相对于string的不可变,StringBuilder代表可变字符串,不会像字符串,在托管堆上频繁分配新对象,StringBuilder是个好同志。
首先StringBuilder内部同string一样,有一个char[]字符数组,负责维护字符串内容。因此,与char数组相关,就有两个很重要的属性:
StringBuilder之所以比string效率高,主要原因就是不会创建大量的新对象,StringBuilder在以下两种情况下会分配新对象:
追加字符串的过程:
简单来说,当StringBuilder的容量Capacity发生变化时,就会引起托管对象申请、内存复制等操作,带来不好的性能影响,因此设置合适的初始容量是非常必要的,尽量减少内存申请和对象创建。代码简单来验证一下:
为什么少量字符串不推荐使用StringBuilder呢?因为StringBuilder本身是有一定的开销的,少量字符串就不推荐使用了,使用String.Concat和String.Join更合适。
高效的使用字符串
string a=""; string b=NULL; string c=string.Empty 三者间的区别
static void Main(string[] args) { string a = ""; string b = null; string c = string.Empty; }
生成后使用ILDASM查看IL:
简单来说就是,通过ldstr将""给a,对b使用ldnull,对c使用ldsfld
ldstr的作用是:
Pushes a new object reference to a string literal stored in the metadata.
将指向保存在元数据中的字符串字面量的一个新的对象引用推入栈中。
此时这里的字面量就是一个空字符串。那么我们的a就引用的是这个空字符串
ldnull的作用是:
Pushes a null reference (typeO
) onto the evaluation stack.
将空引用推入栈中。
微软的文档中提到它”用于在被填充数据前初始化位置或是在其被弃用时使用“。我们此时无法调用任何方法,就相当于只是占了个坑,并没有引用可用的字符串,而且本身null是独立于类型的,它代表不了任何类型,自然也不能代表一个空字符串。
ldsfld的作用是:
Pushes the value of a static field onto the evaluation stack.
将静态字段的值推入栈中。
在这里就是把String.Empty的值给了c。实际上String.Empty就是代表了空字符串,也就是"",因此a和c两者的初始化可以说是等价的。
String.Empty 是string类的一个静态常量;String.Empty和string=””区别不大,因为String.Empty的内部实现是:
既然String.Empty和string=””一样,同样需要占用内存空间,为什么推荐优先使用String.Empty ?
string.Empty只是让代码好读,防止代码产生歧义,比如说:
string s = ""; string s = " "; 这个不细心看,很难看出是空字符串还是空格字符。
如果判断一个字符串是否是空串,使用
if(s==String.Empty)和if(s==””)的效率是一样的,但是最高效的写法是if(s.Length==0)
string.IsNullOrEmpty的内部实现方式:
有3种方法可以表示字符串为空,s.Length == 0 , s == string.Empty , s == "" ,三者的性能也随着逐渐降低