以前在园子里面讨论这两个类的文章有很多很多,并且还拿出了很多的测试报告,在什么情况下,谁比谁快,在什么情况下,该用谁不该用谁等等这些,我这里就不比较了,我就简单看看他们里面的内部实现,那就先看看String吧。
一:String类
说到String类,资料上都说是存在于堆上的一个不可CURD的一个不可变的字符集,当然看到这句话之后就想要看看是不是这样的,然后就
好奇的写了以下代码。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 string s = "123"; 6 } 7 }
method private hidebysig static void Main(string[l arqs) cil managed .entrypoint // Code size 8(5x8) .maxstack 1 .locals init ([6] string s) IL_5555: nop IL 5551:(1dstr "123" IL 5556: stloc.5 IL 9557: ret } // end of method program::Main
从上面的IL中也就仅仅发现一个ldstr指令,看得出clr把string做成了基元类型,也就没看到它具体转换成了什么样的方法,是不是调用了string
的构造函数,这个也不清楚,也就不知道具体怎么把这个有序字符集放到堆中,不过办法还是有的,我们随便挑一个方法看看,比如简单一点的
substring,我们看看它的源代码。
public string Substring int startIndex) substring return this.Substring(startIndex, this.Length - startIndex); DynamicallyInvokable,|TargetedPatching0ptOut("Performance critical to inline across NGen image boundaries"), SecuritySafeCrit public string Substring(int startIndex, int length) return this.InternalsubStringWithChecks(startIndex, length, false); [SecurityCritical] internal string InternalsubstringWithChecks(int startIndex, int length, bool fAlwaysCopy) if (startIndex<0){ thrownewArgumentdtOfRangeException("startIndex",Environment.GetResourceString("ArgumentOutofRange StartIndex")); if (startIndex > this.Length){ thrownew ArgumentOuOfRangeException("startIndex",Environment.GetResourceString("ArgumentOutOfRange StartIndexLargerTh if (length <@) throw new ArgumentOutfRangeException("length",Environment.GetResourceString("ArgumentOutOfRange NegativeLength")); if (startIndex > this.Length - length) throw new ArgumentOut0 RangeException("length",Environment.GetResourceString("ArgumentOutOfRange_IndexLength")); } if(length == 0) return string.Empty; return this.InternalsubString(startIndex, length, fAlwaysCopy);
然后我们找到了一个核心的方法,这个internalSubstring里面定义了两个指针ptr和ptr2,ptr则指向新申请的内存块的首地址,ptr2则指向原始
字符串的首地址,最后将ptr2的位置偏移startindex个位置,最后我们就找到了终极方法string.wstrcpy。
// string [SecurityCritical] private unsafe string InternalsubString(int startIndex, int length, bool fAlwaysCopy if(startIndex == 0 && length == this.Length &&!fAlwaysCopy) { return this; string text = string.FastAllocateString(length); 根据length申请合适的内存 fixed (char* ptr = &text.m firstChar) 空间大小 { fixed (char* ptr2 = &this.m firstChar) 将ptr指向新申请的text字符串的 { string.wstrcpy(ptr,ptr2 +(IntPtr)startIndex, length); 首地址。 } return text; -} 这个startindex是将内存 ptr2指向了当前字符串的内存首地址 地址向后偏移了starindex个位置
在string.wstrcpy方法里面,虽然看的迷迷糊糊,不过还是能看到类似这样的偏移操作,一点一点的将smem地址上的字符赋值给dmem中,
确实也就说明了在堆上是有序的字符集。
// string SecurityCritical] Finternal unsafe static void wstrcpy(char* dmem, char* smem, int charCount) if(charCount >0){ if((dmem & 2)!= 0){ *dmem = *smem; dmem +=(IntPtr)2/2; smem +=(IntPtr)2/2; charcount--; while(charCount >= 8){ *(int*)dmem =*(int*)smem; *(int*)(dmem +(IntPtr)4 / 2) = *(int*)(smem + (IntPtr)4 / 2);*(int*)(dmem +(IntPtr)8 / 2) = *(int*)(smem + (IntPtr)8 / 2);*(int*)(dmem +(IntPtr)12 / 2) = *(int*)(smem + (IntPtr)12 / 2): dmem +=(IntPtr)16 /2; smem +=(IntPtr)16/2: charCount -= 8;} if((charCount &4)!= 0){ *(int*)dmem =*(int*)smem; *(int*)(dmem +(IntPtr)4 / 2) = *(int*)(smem + (IntPtr)4 / 2); dmem +=(IntPtr)8/2; smem +=(IntPtr)8/2; if((charCount & 2) != 0){ *(int*)dmem = *(int*)smem; dmem +=(IntPtr)4/2; smem t=(IntPtr)4/2;
同样在上面的源代码中来说,substring操作并没有对原始字符串进行修改,而是把截取的值放到新申请的内存地址空间中,这也就说明了字符
串是不可修改的说法,当然如果设计者真的要做到原位修改,那肯定也是能做到的,为了佐证下,我再举一个经常用到的concat方法,不过在
FastAllocateString方法中,并没有看到他的源代码,所以只能说根据length申请合适的空间。
public static string Concat(string stro, string str1){ if(string.IsNullOrEmpty(str0)){ if(string.IsNullOrEmpty(str1)){ return string.Empty } return str1;} else{ if(string.IsNullOrEmpty(str1)) return str0; 根据str0.length+str2.length申请合适空间 int length = str0.Length; string text=string.FastAllocateString(length + str1.Length): string.FillstringChecked(text,0,str0); string.FillstringChecked(text,length,str1); } return text; 赋值第一部分
所以结论出来了: 当你对字符串进行大量操作的时候,会产生很多的新的字符串,这些字符串会大量零碎的占据着堆空间,大多都是生存期较短的,所以一般都是在堆的第一代上,所以会对gc产生了比较大回收压力。
二:StringBuilder
看这个类的话,还是看一下它的源代码,就抽一个Append吧,从下面这个截图中看出来几个有意思的地方。
<1> 原来StringBuilder里面维护的是一个m_ChunkChars的字符数组。
<2> 如果当前的字符串的length<2,会直接给chunkchars数组复制,length>2的时候看到的是刚才string类中经典的wstrcpy用法,而
这个时候ptr指向的是chunkChars[chunkLength]的首地址,而不像string中申请新的内存空间,所以从这里看,比string大大的节省
了内存空间。
public unsafe stringBuilder Append(string value if (value != null) { char[] chunkChars = this.m chunkchars; 原来里面维护了一个 int length = value.Length; int chunkLength = this.m_chunklength; m_ChunkChars的字符数组 int num = chunkLength + length; if (num < chunkChars.Length) if (length <= 2){ if (length >@){ chunkChars[chunkLength] =value[0]; if (length >1) chunkChars[chunkLength + 1] = value[1];}} else ( fixed(char* smem = value) fixed (char* ptr = &chunkChars[chunkLength]) string.wstrcpy(ptr,smem,length); } this.m ChunkLength =num;} else { this.AppendHelper(value):
好了,具体他们的性能比较我也不说了,大家看着他们的原理凑合着用吧,简单的看看也只能看到这了,再看就漏点了。