彻底研究String 转

 

彻底研究String

String是很常用的类型,但有的同学在使用过程中存在一些误区,导致效率低下,在此对其机制进行一个彻底的讨论,水平有限,如有不同的见解请留言讨论。

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实例可以跟其他基本类型一样,直接赋值即可。
string str = " Hello String! " ;
相应的IL代码为:
ldstr " Hello 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 s1 = " Hello String! " ;
string s2 = s1.ToUpper();
Console.WriteLine(
string .ReferenceEquals(s1, s2));
ReferenceEquals方法可以判断两个变量引用的是不是同一个对象,由于String的不变性,上面的代码会输出false。
字符串的不变性不会导致线程同步问题,也就是它是线程安全的。
字符串的长度、字符串的字符索引都是只读的,对其改变会出现编译错误。
string str = " Hello String! " ;
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 s1 = " Hello String! " ;
string s2 = " Hello String! " ;
Console.WriteLine(
string .ReferenceEquals(s1, s2));
上面代码的输出为:true,正是字符串驻留的体现。String类型有两个静态方法与字符串驻留操作相关。
public static string Intern(string str)
如果 str 的值已经存在在字符串驻留池,则返回该字符串的引用;否则返回含有 str 值的字符串的新引用。
public static string IsInterned(string str)
如果 str 存在在字符串驻留池中,则返回字符串驻留池中该字符串的引用;否则返回null。
string s1 = " Hello " ;
string s2 = s1 + " String! " ;
Console.WriteLine(
string .IsInterned(s1) ?? " null " ); // 输出:Hello
Console.WriteLine( string .IsInterned(s2) ?? " null " ); // 输出:null
要显式关闭字符串驻留机制,FCL提供了一个特性:
public class CompilationRelaxationsAttribute : Attribute
用CompilationRelaxations.NoStringInterning枚举来指定关闭字符串驻留机制。这个特性是应用在程序集级别的,其使用语法为:
[assembly:CompilationRelaxations(CompilationRelaxations.NoStringInterning)]
事实上,C#编译器默认的是关闭了字符串驻留机制的,因为在程序执行过程中或许会产生大量的临时字符串,如果都加入到程序集的字符串驻留池,驻留字符串的查找会耗费大量时间。字符串一旦加入驻留池,其生命周期跟该应用域的生命周期相同,在应用域存在的过程中,驻留的字符串会一直占用大量的内存。因此在运行时,字符串驻留机制弊大于利。在某些时候(如处理数据量大文本时)为了优化性能可以自己控制字符串加入驻留池。
string s1 = new string ( ' a ' , 10000 );
string .Intern(s1);
string s2 = new string ( ' a ' , 10000 );
Console.WriteLine(
string .IsInterned(s2) ?? " null " );
上面的代码输出10000个'a',10000个'a'的字符串在内存中只有一份拷贝,如果把代码行2注释掉,10000个'a'的字符串会在内存中存在两份拷贝,代码会输出null。
有的同学会问,既然编译器关闭字符串驻留,为何前面的例子的字符串会驻留?原因是在编译前定义的字符串直接量会存在在程序集的元数据中,运行时它门反正要进入内存,不如把它们加入应用域的字符串驻留池提高性能。当然,不能过于依赖默认的字符串驻留机制,说不定以后的CLR版本会目前默认的字符串驻留机制进行改变。
在程序代码中出现string s = "Hello" + " String!";编译器会把"Hello" + " String!"作为"Hello String!"来处理,这是编译器优化的结果,也就是编译中已经存在"Hello String!"直接量,并加入到程序集元数据中,因此会看到"Hello String!"已经驻留。
字符串驻留池是应用域内CLR维护控制的,其数据结构是哈希表(Hashtable),其中键是字符串的直接量,值是该字符串的引用,查找一个字符串是否已驻留时,先查找与该字符串长度相同的驻留字符串,其他的忽略,找到相同长度的字符串后再逐字符比较(二进制值),如果相同,返回驻留字符串的引用,否则,返回null。
在使用字符串时有个疑问:在与不安全代码互操作是会不会破坏字符串的不变性?
下面的代码回答了这个问题:
unsafe
{
    
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
}
ch的地址可能每次都与上面不一样,但两次输出结果相同。

字符串拷贝操作

对一个对象进行拷贝可以调用object保护的MemberwiseClone()方法,要实现深层次拷贝,可实现ICloneable接口,但string类型实现了ICloneable接口,但实现的却是浅层拷贝。在一些极特殊的情况下,要返回含有相同值的字符串,可以用String.Copy方法。
string s = " Hello String! " ;
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

你可能感兴趣的:(String)