您的位置:首页 > 产品设计 > UI/UE

20101203 学习记录:关于StringBuilder/String

2010-12-10 10:39 573 查看
一直被告诫说StringBuilder效率比string高啦什么的,可是我知道StringBuilder也是有容量的,超过默认值一样需要重新分配空间啊~

可是很多时候我们是不确定字符串的长度的,那是随便大致设一个Capacity还是放任不管让它自己按需重新分配空间呢?

总是重新分配空间的话,那岂不是跟string没啥区别了?? @_@....

一般,都会为StringBuilder设定一个Capacity么??? ....

1. 字符串中变量的替换

StringBuilder的AppendFormat方法跟String.Format相似,用带格式文本替换字符串中传递的格式说明符

string.Format(" {0} ", paramter)

StringBuilder.AppendFormat(" {0} ",paramter);

2. ToString方法

第一次调用ToString()方法非常高效,直接返回StringBuilder内字符串的引用。每调用一次ToString(),获得的String对象引用都会变化。

大家通过“+”来连接不同的字符串,使用中是如此的便捷,但是其负面影响就是其执行效率很低;而StringBuilder执行相同的功能时速度将提升一千倍以上;当你在一个庞大的循环内使用它的时候,就会明显的感觉到速度的分升了。

它的命名空间是:System.Text
定义格式:Dim StringBuildrObject As New Text.StringBuilder
常用方法:

通过用一个重载的构造函数方法初始化变量,可以创建 StringBuilder 类的新实例,正如下面的示例中所阐释的那样。
Dim MyStringBuilder As New StringBuilder("Hello World!")

[align=left]设置容量和长度[/align]
[align=left]虽然 StringBuilder 对象是动态对象,允许扩充它所封装的字符串中字符的数量,但是您可以为它可容纳的最大字符数指定一个值。此值称为该对象的容量,不应将它与当前 StringBuilder 对象容纳的字符串长度混淆在一起。例如,可以创建 StringBuilder 类的带有字符串“Hello”(长度为 5)的一个新实例,同时可以指定该对象的最大容量为 25。当修改 StringBuilder 时,在达到容量之前,它不会为其自己重新分配空间。当达到容量时,将自动分配新的空间且容量翻倍。可以使用重载的构造函数之一来指定 StringBuilder 类的容量。下面的示例指定可以将 MyStringBuilder对象扩充到最大 25 个空白。[/align]
[align=left]Dim MyStringBuilder As New StringBuilder("Hello World!", 25)[/align]
[align=left] [/align]
[align=left]另外,可以使用读/写 Capacity 属性来设置对象的最大长度。下面的示例使用 Capacity 属性来定义对象的最大长度。[/align]
[align=left]MyStringBuilder.Capacity = 25[/align]
[align=left] [/align]
[align=left]EnsureCapacity方法可用来检查当前 StringBuilder 的容量。如果容量大于传递的值,则不进行任何更改;但是,如果容量小于传递的值,则会更改当前的容量以使其与传递的值匹配。[/align]
[align=left]也可以查看或设置 Length 属性。如果将 Length 属性设置为大于 Capacity 属性的值,则自动将 Capacity 属性更改为与 Length 属性相同的值。如果将 Length 属性设置为小于当前 StringBuilder 对象内的字符串长度的值,则会缩短该字符串。[/align]
[align=left] [/align]
[align=left]修改 StringBuilder 字符串[/align]
[align=left]下表列出了可以用来修改 StringBuilder 的内容的方法。[/align]
[align=left]方法名 [/align]
[align=left]使用 [/align]
[align=left]StringBuilder.Append [/align]
[align=left]将信息追加到当前 StringBuilder 的结尾。[/align]
[align=left]StringBuilder.AppendFormat [/align]
[align=left]用带格式文本替换字符串中传递的格式说明符。[/align]
[align=left]StringBuilder.Insert [/align]
[align=left]将字符串或对象插入到当前 StringBuilder 对象的指定索引处。[/align]
[align=left]StringBuilder.Remove [/align]
[align=left]从当前 StringBuilder 对象中移除指定数量的字符。[/align]
[align=left]StringBuilder.Replace [/align]
[align=left]替换指定索引处的指定字符。[/align]
[align=left] [/align]
Append
Append方法可用来将文本或对象的字符串表示形式添加到由当前 StringBuilder 对象表示的字符串的结尾处。下面的示例将一个 StringBuilder 对象初始化为“Hello World”,然后将一些文本追加到该对象的结尾处。将根据需要自动分配空间。

Dim MyStringBuilder As New StringBuilder("Hello World!")
MyStringBuilder.Append(" What a beautiful day.")
Console.WriteLine(MyStringBuilder)
此示例将
Hello World! What a beautiful day.
显示到控制台。

AppendLine
即在添加一个字符串后再添加一个VbCrlf(回车);
AppendFormat
AppendFormat方法将文本添加到 StringBuilder 的末尾,而且实现了 IFormattable 接口,因此可接受格式化部分中描述的标准格式字符串。可以使用此方法来自定义变量的格式并将这些值追加到 StringBuilder 的后面。下面的示例使用 AppendFormat 方法,将一个设置为货币值格式的整数值放到 StringBuilder 的末尾。
Dim MyInt As Integer = 25
Dim MyStringBuilder As New StringBuilder("Your total is ")
MyStringBuilder.AppendFormat("{0:C} ", MyInt)
Console.WriteLine(MyStringBuilder)
此示例将
Your total is $25.00
显示到控制台。

Insert
Insert方法将字符串或对象添加到当前 StringBuilder 中的指定位置。下面的示例使用此方法将一个单词插入到 StringBuilder 的第六个位置。
Dim MyStringBuilder As New StringBuilder("Hello World!")
MyStringBuilder.Insert(6, "Beautiful ")
Console.WriteLine(MyStringBuilder)

此示例将
Hello Beautiful World!
显示到控制台。

Remove
可以使用 Remove 方法从当前 StringBuilder 中移除指定数量的字符,移除过程从指定的从零开始的索引处开始。下面的示例使用 Remove 方法缩短 StringBuilder
Dim MyStringBuilder As New StringBuilder("Hello World!")
MyStringBuilder.Remove(5, 7)
Console.WriteLine(MyStringBuilder)

[align=left]此示例将 Hello显示到控制台。[/align]
Replace
使用 Replace 方法,可以用另一个指定的字符来替换 StringBuilder 对象内的字符。下面的示例使用 Replace 方法来搜索 StringBuilder 对象,查找所有的感叹号字符 (!),并用问号字符 (?) 来替换它们。
Dim MyStringBuilder As New StringBuilder("Hello World!")
MyStringBuilder.Replace("!"c, "?"c)
Console.WriteLine(MyStringBuilder)

[align=left]此示例将 Hello World?显示到控制台。[/align]

From:
http://blog.csdn.net/sand_gold/archive/2006/12/07/1433288.aspx

彻底研究StringBuilder

上一篇《彻底研究String》 介绍了String类型的一些性质。.NET对String的优化,能高效安全的完成一些操作,但正是这些优化导致了在进行某些操作时会占用大量的资源, 如拼接字符串、修改字符串等等,高效地完成这些操作的替代类型是StringBuilder。(每次都想"彻底研究",但每次都发现研究的不彻底,写出来 的更不彻底,以至于不敢用"彻底研究"等字眼了,但为与上一篇对应,暂且再亵渎一次"彻底研究"!)

上篇勘误

上一篇《彻底研究String》中"在与不安全代码互操作是会不会破坏字符串的不变性?"这个问题的答案的分析验证是错误的,在这里向大家道歉!已经更新。

public sealed class StringBuilder : ISerializable
位于:System.Text命名空间中。

StringBuilder 仅实现ISerializable接口,直接派生自Object,相对于String类型其功能不太完善,如ToUpper、SubString、 foreach遍历每个字符等等,后面介绍如何扩展其功能。它是密封类型,不能通过派生它的子类来改变其行为。

StringBuilder能够动态高效的构建字符串、修改字符串,不能保证所有实例成员都是线程安全的,尽管在类型定义中加入了很多线程安全的控制,如果要确保其线程安全,须手工实现线程同步机制。

StringBuilder内部维护的是一个String对象,在String类型所在的程序外部,String表现出不变性,但在String类型的内部,定义了一些改变String对象的方法,声明为internal,这些方法供StringBuilder等调用。



最大容量(MaxCapacity):

默认的最大容量等于int.MaxValue。

StringBuilder sb = new StringBuilder();

Console.WriteLine(sb.MaxCapacity); // 2147483647
MaxCapacity是只读属性,其取值范围为1~int.MaxValue,如果想设定自己的最大容量,StringBuild提供了一个构造方法:

public StringBuilder(int capacity, int maxCapacity)
在构造StringBuilder对象时设定最大容量值,最大值一旦设定,将不可改变,如果Append或其它操作使Length大于MaxCapacity将抛出异常。

容量(Capacity):返回StringBuilder的当前容量,其取值范围为:0~MaxCapacity,这个属性是可读写的,若设置的值小于Length将抛出异常。

长度(Length):

返回StringBuilder内部维护的当前String对象的长度,取值范围:0~MaxCapacity,可变属性。

int.MaxValue >= MaxCapacity >= Capacity >= Length >=0 (MaxCapacity >= 1)

MaxCapacity只是一个Capacity和Length的范围约束,Capacity是StringBuilder内部字符串实际分配内存的大小,Length是StringBuilder内部字符串的有效字符的数量。

Capacity的变化规律



StringBuilder的Capacity属性的取值范围是:0~MaxCapacity;默认大小为:16。

StringBuilder sb = new StringBuilder();

Console.WriteLine(sb.Capacity); // 16
当以构造方法StringBuilder(String str) 创建StringBuilder对象时,Capacity的值可用下面的伪代码表示:

IF str.Length < 16 THEN sb.Capacity = 16

ELSE

BEGIN

sb.Capacity = 16

WHILE str.Length < sb.Capacity

sb.Capacity *= 2

END

示例:

StringBuilder sb = new StringBuilder(new string('a', 16));

Console.WriteLine(sb.Capacity); // 16

StringBuilder sb1 = new StringBuilder(new string('a', 18));

Console.WriteLine(sb1.Capacity); // 32

Append等扩充字符串操作时,如果结果字符串的长度大于Capacity,则Capacity加倍;如果加倍后的Capacity还不足以容纳结果字符串,则Capacity的值等于结果字符串的长度。

StringBuilder sb1 = new StringBuilder(new string('a', 18));

Console.WriteLine(sb1.Capacity); // 32

Console.WriteLine(sb1.Append(new string('b', 18)).Capacity); // 64 (32 * 2)

Console.WriteLine(sb1.Append(new string('c', 100)).Capacity); // 136 (result.Length)

Console.WriteLine(sb1.Append(new string('d', 100)).Capacity); // 272 (136 * 2)

实现原理:StringBuilder 维护一个长度等于Capacity的字符串(可以看作字符数组),当Capacity长度的字符串不足以容纳结果字符串时,StringBuilder开 辟新的长度为经过上面的规则计算好的Capacity的内存区域,将原字符串复制到新的内存区域再进行操作,原字符串区域交给GC回收。因此这里也涉及到 内存的分配与回收,使用StringBuilder时最好估算一下所需容量,用这个容量初始化Capacity,提高性能。

StringBuilder内字符串的垃圾数据

字符串在内存中是顺序存储的。StringBuilder内部字符串的可用长度是Capacity,有效字符数是Length。刚刚构造StringBuilder后Length到Capacity的范围,都保留内存中的原垃圾数据。

初始化后显式增大Capacity大小后,增大的部分内存保留原垃圾数据。

系统自动扩大容量,Length到Capacity的部分将清空内存中原垃圾数据为'\0'。

设置StringBuilder长度时,若新设置的Lenght小于原Length,字符串将被截断,新Length到原Length的部分填充空字符'\0';若新设置的Lenght大于原Length,原Lenght到新Length的部分填充空字符'\0'。

由于字符串在内存中是顺序存储的,可用下面的方法查看StringBuilder内部字符串内存中的数据:

unsafe static void ShowContent(StringBuilder sb)

{

fixed(char* ch = sb.ToString())

{

for(int i = 0; i < sb.Capacity; i++)

{

Console.Write((int)ch[i] + " ");

}

}

}

ToString方法

由下面的示例代码可以看出,每调用一次ToString(),获得的String对象引用都会变化。

static void Main(string[] args)

{

StringBuilder sb = new StringBuilder("Hello StringBuilder!");

ShowAddress(sb.ToString()); // 20656316

ShowAddress(sb.ToString()); // 20666276

ShowAddress(sb.ToString()); // 20666372

ShowAddress(sb.ToString()); // 20666468

ShowAddress(sb.ToString()); // 20666564

ShowAddress(sb.ToString()); // 20666660

ShowAddress(sb.ToString()); // 20666756

ShowAddress(sb.ToString()); // 20666852

ShowAddress(sb.ToString()); // 20666948

ShowAddress(sb.ToString()); // 20667044

}

public unsafe static void ShowAddress(string s)

{

fixed (char* p = s)

{

Console.WriteLine((int)p);

}

}

为得出原因,Reflector查看,是如下代码:

public override string ToString()

{

string stringValue = this.m_StringValue;

if (this.m_currentThread != Thread.InternalGetCurrentThread())

{

return string.InternalCopy(stringValue);

}

if ((2 * stringValue.Length) < stringValue.ArrayLength)

{

return string.InternalCopy(stringValue);

}

stringValue.ClearPostNullChar();

this.m_currentThread = IntPtr.Zero;

return stringValue;

}

1.看来ToString()方法对线程安全进行控制了,如果不是当前线程访问,返回字符串的拷贝。

2.ArrayLength应该就是Capacity了,如果长度小于Capacity的1/2,为优化性能,返回字符串的新的拷贝。

3.以上条件不满足,返回StringBuilder内部字符串。但调用一次ToString()后,执行了this.m_currentThread = IntPtr.Zero; ,如果再紧接着执行ToString()会返回新的字符串。

要返回StringBuilder内部字符串的真实地址,可用反射或序列化取得StringBuilder内的字符串的引用,再取字符串的地址,StringBuilder内字符串声明为:

internal volatile string m_StringValue;

StringBuilder sb = new StringBuilder("Hello StringBuilder!");

ShowAddress(sb.ToString()); // 21075152

ShowAddress(sb.ToString()); // 21117944

ShowAddress(sb.ToString()); // 21118040

ShowAddress(sb.ToString()); // 21075152

SerializationInfo info = new SerializationInfo(

typeof(StringBuilder), new FormatterConverter());

((ISerializable)sb).GetObjectData(

info, new StreamingContext());

String s = info.GetString("m_StringValue");

ShowAddress(s); // 21075152

可见,第一次调用ToString()方法非常高效,直接返回StringBuilder内字符串的引用。如果没有重新取得m_currentThread,接下来的调用会拷贝构造新的字符串。

CLR会记录该StringBuilder维护的String已被引用,如果试图对其修改,StringBuilder会重新分配内存区域,将原字符串拷贝到新的内存区域然后进行修改。

ToString 重载的另一个版本原型为:public string ToString(int startIndex, int length)会构造新的字符串,新字符串值为:起始索引为startIndex,长度为length的子字符串,这个重载能实现String中的 SubString的功能。

EnsureCapacity

确保最小的容量不小于给定的数值。如果给定的数值值小于目前的Capacity,则忽略给定的数值;如果给定的数值大于Capacity,则设置Capacity为给定的数值。

AppendFormat

有时感觉StringBuilder连接字符串不如String连接方便,如果用AppendFormat会方便很多,使用方法跟String.Format相似,这个方法保证了字符串连接的优雅与高效。

不知道大家有没有注意到,Append、AppendFormat、AppendLine、Insert、Remove、Replace等方法对StringBuild对象操作完成后都返回StringBuilder自身,这样的设计便于进行一连串的操作。

如:

StringBuilder sb0 = new StringBuilder("abc",10);

string s = sb0.Append("abc").

Replace("ca", "--").

Insert(0, "String:").

ToString();

扩展StringBuilder的操作

StringBuilder 与String相比,非常多的操作没有实现,可以调用ToString()后再进行操作,但这样会影响效率;也可以用扩展方法来扩展其操作;既然上面能取 得StringBuilder成员m_StringValue的值,可以直接用所有String的方法来处理判断,但这样会造成一些问题,不推荐这样做。

扩展StringBuilder,如foreach遍历所有字符、每个字符字符转换为其对应的大写字符示例:

static class Program

{

static void Main(string[] args)

{

StringBuilder sb = new StringBuilder("Hello StringBuilder!");

foreach (var c in sb.GetEnumerator())

{

Console.WriteLine(c);

}

Console.WriteLine(sb.ToUpper());

Console.ReadKey();

}

static IEnumerable<Char> GetEnumerator(this StringBuilder sb)

{

for (var i = 0; i < sb.Length; i++)

{

yield return sb[i];

}

}

static StringBuilder ToUpper(this StringBuilder sb)

{

for (var i = 0; i < sb.Length; i++)

{

sb[i] = Char.ToUpper(sb[i]);

}

return sb;

}

}

在输出托管String起始地址时,刚开始想到的是托管String封送到非托管代码,用VC实现了一个类,但输出结果不是预期的,在查资料时发现了一篇很不错的文章,推荐给大家。

CLR 完全介绍—托管代码与非托管代码之间的封送处理( http://msdn.microsoft.com/zh-cn/magazine/cc164193.aspx )

后来发现没必要用非托管调用实现,不安全代码就可实现输出String对象的起始地址。

还有一篇介绍StringBuilder的ToString() 执行效率的:StringBuilder.ToString() vs Reflection( http://blogs.msdn.com/miah/archive/2007/10/20/stringbuilder-tostring-vs-reflection.aspx )

参考资料

MSDN

Applied Microsoft .NET Framework Programming

Author: 飘遥(周振兴)

Blog: http://Zxjay.cnblogs.com/

URL: /article/5183507.html

彻底研究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的地址可能每次都与上面不一样,但两次输出结果相同。


原来对这个问题的分析验证是错误的,在这里向大家道歉!
这里重新分析一下,并得出答案,希望这次是正确的。

构造字符数组,用字符数组构造字符串,输出字符串和字符数组的地址来分析比较直观。
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); // 90172320

fixed (char* str = s)

{

Console.WriteLine((long)str); // 21017516

}

ch[2] = 'c';

string s1 = new string(ch);

Console.WriteLine((long)ch); // 90172320

fixed (char* str = s1)

{

Console.WriteLine((long)str); // 21059776

}

Console.WriteLine(object.ReferenceEquals(s, s1)); // false

}
上面的代码可以看出:由字符数组创建字符串时复制字符数组的内容来创建新的字符串,并非原先理解的在字符数组的基础上创建。

继续上面的问题,用下面的代码验证并得出答案。

static void Main(string[] args)

{

unsafe

{

string s = "Hello String!";

string s1 = s;

Console.WriteLine(string.

ReferenceEquals(s, s1)); // True

Console.WriteLine(s); // Hello String!

Console.WriteLine(s1); // Hello String!

ShowAddress(s); // 20656236

ShowAddress(s1); // 20656236

ChangeString(s); // 20656236

ShowAddress(s); // 20656236

ShowAddress(s1); // 20656236

Console.WriteLine(string.

ReferenceEquals(s, s1)); // True

Console.WriteLine(s); // HeZlo String!

Console.WriteLine(s1); // HeZlo String!

}

Console.ReadKey();

}

// 输出字符串的起始地址

public unsafe static void ShowAddress(string s)

{

fixed (char* p = s)

{

Console.WriteLine((int)p);

}

}

// 更改字符串的第三个字符为Z

public unsafe static void ChangeString(string s)

{

fixed (char* p = s)

{

p[2] = 'Z';

Console.WriteLine((int)p);

}

}
在与不安全代码互操作是会不会破坏字符串的不变性?通过上面的例子可以看出,答案是会!

字符串拷贝操作

对 一个对象进行拷贝可以调用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

Author: 飘遥(周振兴)

Blog: http://Zxjay.cnblogs.com/

URL: /article/5183506.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: