String是很常用的类型,但有的同学在使用过程中存在一些误区,导致效率低下,在此对其机制进行一个彻底的讨论,水平有限,如有不同的见解请留言讨论。
[SerializableAttribute]
[ComVisibleAttribute( true )]
public sealed class String : IComparable,
ICloneable, IConvertible, IComparable < string > , IEnumerable < char > ,
IEnumerable, IEquatable < string >
String的创建
String 是引用类型,其地址是在托管堆上分配的,而值类型的地址是在计算栈上分配的。String是密封的(sealed),因此不可以直接继承String类来创建另一个版本的String。
MS 对String类型进行了特殊的优化,以提高其效率和方便性。创建String实例可以跟其他基本类型一样,直接赋值即可。
IL中实例化一个String用的是ldstr(load string)指令,而不是调用通常构造类型的newobj指令。
ldstr指令负责字符串实例的初始化和在托管堆上的地址分配,返回指向该字符串地址的指针。
String类型提供了几个个构造方法,可以使用unsafe的char*或Sbyte*构造String对象,也可以由字符数组构造String对象等。
String的不变性
String的实例在corlib.dll外部来说是只读的(在corlib.dll内部有一些声明为internal的方法可以对String实例进行修改操作,这些方法供StringBuilder等使用),在其生命周期内是恒定不变的,对字符串的改变(ToUpper,SubString,拼接字符串等等)会导致新字符串对象的创建,旧字符串的回收,给GC造成压力。
string s2 = s1.ToUpper();
Console.WriteLine( string .ReferenceEquals(s1, s2));
字符串的不变性不会导致线程同步问题,也就是它是线程安全的。
字符串的长度、字符串的字符索引都是只读的,对其改变会出现编译错误。
str.Length = 10 ; // error: Property or indexer 'string.Length' cannot be assigned to -- it is read only
str[ 0 ] = ' h ' ; // error: Property or indexer 'string.this[int]' cannot be assigned to -- it is read
字符串驻留
字符串驻留又称为:字符串留用、字符串拘留等。
字符串驻留是指:在应用域(AppDomain)范围内将某些字符串放入驻留池内,此后应用程序创建字符串时,如果相同的字符串存在在驻留池,将直接返回驻留池内该相同字符串的引用,而不需要创建新的字符串实例。可见字符串驻留机制是建立在字符串不变性的基础之上的,如果没有字符串不变性这条属性,将产生不可预料的后果。
string s2 = " Hello String! " ;
Console.WriteLine( string .ReferenceEquals(s1, s2));
public static string Intern(string str)
如果 str 的值已经存在在字符串驻留池,则返回该字符串的引用;否则返回含有 str 值的字符串的新引用。
public static string IsInterned(string str)
如果 str 存在在字符串驻留池中,则返回字符串驻留池中该字符串的引用;否则返回null。
string s2 = s1 + " String! " ;
Console.WriteLine( string .IsInterned(s1) ?? " null " ); // 输出:Hello
Console.WriteLine( string .IsInterned(s2) ?? " null " ); // 输出:null
public class CompilationRelaxationsAttribute : Attribute
用CompilationRelaxations.NoStringInterning枚举来指定关闭字符串驻留机制。这个特性是应用在程序集级别的,其使用语法为:
string .Intern(s1);
string s2 = new string ( ' a ' , 10000 );
Console.WriteLine( string .IsInterned(s2) ?? " null " );
有的同学会问,既然编译器关闭字符串驻留,为何前面的例子的字符串会驻留?原因是在编译前定义的字符串直接量会存在在程序集的元数据中,运行时它门反正要进入内存,不如把它们加入应用域的字符串驻留池提高性能。当然,不能过于依赖默认的字符串驻留机制,说不定以后的CLR版本会目前默认的字符串驻留机制进行改变。
在程序代码中出现string s = "Hello" + " String!";编译器会把"Hello" + " String!"作为"Hello String!"来处理,这是编译器优化的结果,也就是编译中已经存在"Hello String!"直接量,并加入到程序集元数据中,因此会看到"Hello String!"已经驻留。
字符串驻留池是应用域内CLR维护控制的,其数据结构是哈希表(Hashtable),其中键是字符串的直接量,值是该字符串的引用,查找一个字符串是否已驻留时,先查找与该字符串长度相同的驻留字符串,其他的忽略,找到相同长度的字符串后再逐字符比较(二进制值),如果相同,返回驻留字符串的引用,否则,返回null。
在使用字符串时有个疑问:在与不安全代码互操作是会不会破坏字符串的不变性?
下面的代码回答了这个问题:
{
char * ch = stackalloc char [ 100 ];
for ( var i = 0 ; i < 100 - 1 ; i ++ )
{
ch[i] = ( char )(i + 1 );
}
ch[ 99 ] = ' \0 ' ;
string s = new string (ch);
Console.WriteLine(( long )ch); // 68479972
ch[ 2 ] = ' c ' ;
string s1 = new string (ch);
Console.WriteLine(( long )ch); // 68479972
Console.WriteLine( object .ReferenceEquals(s, s1)); // false
}
字符串拷贝操作
对一个对象进行拷贝可以调用object保护的MemberwiseClone()方法,要实现深层次拷贝,可实现ICloneable接口,但string类型实现了ICloneable接口,但实现的却是浅层拷贝。在一些极特殊的情况下,要返回含有相同值的字符串,可以用String.Copy方法。
string s1 = ( string )s.Clone();
string s2 = string .Copy(s);
string s3 = s.ToString();
string s4 = s.Substring( 0 );
Console.WriteLine( string .ReferenceEquals(s, s1)); // true
Console.WriteLine( string .ReferenceEquals(s, s2)); // false
Console.WriteLine( string .IsInterned(s2) ?? " null " ); // Hello String!
s2 = string .Intern(s2);
Console.WriteLine( string .ReferenceEquals(s, s2)); // true
Console.WriteLine( string .ReferenceEquals(s, s3)); // true
Console.WriteLine( string .ReferenceEquals(s, s4)); // true
上面的结果是否在你预料之内呢?
字符串连接操作
字符串连接是非常常见的操作,但每次连接,都导致新对象的产生,其步骤大体如下(.NET 的实现可能有一些差别):
s += s1;
1.分配足够多的临时存储空间temp。
2.将s复制temp的起始处,s1复制到temp的结束处。
3.释放s原来的空间,交给GC处理。
4.为s分配足够的空间,将temp复制到s的新的存储空间。
每次分配都牵涉到存储空间的分配和释放,如果字符串连接过多,会严重影响执行效率,因此最好用StringBuilder来处理(下一篇介绍)。
字符串比较
尽量使用String定义的比较操作的方法。许多种字符串比较的静态方法和实例方法以及这些方法的重载,如果与区域无关的比较建议使用StringComparison.Ordinal或StringComparison.OrdinalIgnoreCase选项。有区域有关的比较建议使用StringComparison.CurrentCulture或StringComparison.CurrentCultureIgnoreCase选项。尽量不要使用StringComparison.InvariantCulture或StringComparison.InvariantCultureIgnoreCase选项,因为这个选项会慢很多。
参考资料:
MSDN
Applied Microsoft .NET Framework Programming