众所周知,在.NET中String是引用类型,具有不可变性,当一个String对象被修改、插入、连接、截断时,新的String对象就将被分配,这会直接影响到性能。但在实际开发中经常碰到的情况是,一个String对象的最终生成需要经过一个组装的过程,而在这个组装过程中必将会产生很多临时的String对象,而这些String对象将会在堆上分配,需要GC来回收,这些动作都会对程序性能产生巨大的影响。事实上,在String的组装过程中,其临时产生的String对象实例都不是最终需要的,因此可以说是没有必要分配的。
鉴于此,在.NET中提供了StringBuilder,其设计思想源于构造器(Builder)设计模式,致力于解决复杂对象的构造问题。对于String对象,正需要这样的构造器来进行组装。StringBuilder类型在最终生成String对象之前,将不会产生任何String对象,这很好地解决了字符串操作的性能问题。
以下代码展示了使用StringBuilder和不适用StringBuilder的性能差异:(这里的性能检测工具使用了老赵的CodeTimer类)
public class Program
{
private const String item = "一个项目";
private const String split = ";";
static void Main(string[] args)
{
int number = 10000;
// 使用StringBuilder
CodeTimer.Time("使用StringBuilder: ", 1, () =>
{
UseStringBuilder(number);
});
// 不使用StringBuilder
CodeTimer.Time("使用不使用StringBuilder: : ", 1, () =>
{
NotUseStringBuilder(number);
});
Console.ReadKey();
}
static String UseStringBuilder(int number)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < number; i++)
{
sb.Append(item);
sb.Append(split);
}
sb.Remove(sb.Length - 1, 1);
return sb.ToString();
}
static String NotUseStringBuilder(int number)
{
String result = "";
for (int i = 0; i < number; i++)
{
result += item;
result += split;
}
return result;
}
}
上述代码的运行结果如下图所示,可以看出由于StringBuilder不会产生任何的中间字符串变量,因此效率上优秀不少!
看到StringBuilder这么优秀,不禁想发出一句:卧槽,牛逼!
于是,我们拿起我们的锤子(Reflector)撕碎StringBuilder的外套,看看里面到底装了什么?
我们发现,在StringBuilder中定义了一个字符数组m_ChunkChars,它保存StringBuilder所管理着的字符串中的字符。
经过对StringBuilder默认构造方法的分析,系统默认初始化m_ChunkChars的长度为16(0x10),当新追加进来的字符串长度与旧有字符串长度之和大于该字符数组容量时,新创建字符数组的容量会增加到2n+1(假如当前字符数组容量为2n)。
此外,StringBuilder内部还有一个同为StringBuilder类型的m_ChunkPrevious,它是内部的一个StringBuilder对象,前面提到当追加的字符串长度和旧字符串长度之合大于字符数组m_ChunkChars的最大容量时,会根据当前的(this)StringBuilder创建一个新的StringBuilder对象,将m_ChunkPrevious指向新创建的StringBuilder对象。
下面是StringBuilder中实现扩容的核心代码:
private void ExpandByABlock(int minBlockCharCount)
{
......
int num = Math.Max(minBlockCharCount, Math.Min(this.Length, 0x1f40));
this.m_ChunkPrevious = new StringBuilder(this);
this.m_ChunkOffset += this.m_ChunkLength;
this.m_ChunkLength = 0;
......
this.m_ChunkChars = new char[num];
}
可以看出,初始化m_ChunkPrevious在前,创建新的字符数组m_ChunkChars在后,最后才是复制字符到数组m_ChunkChars中(更新当前的m_ChunkChars)。归根结底,StringBuilder是在内部以字符数组m_ChunkChars为基础维护一个链表m_ChunkPrevious,该链表如下图所示:
在最终的ToString方法中,当前的StringBuilder对象会根据这个链表以及记录的长度和偏移变量去生成最终的一个String对象实例,StringBuilder的内部实现中使用了一些指针操作,其内部原理有兴趣的园友可以自己去通过反编译工具查看源代码。
2String和Byte[]对象之间如何转换?在实际开发中,经常会对数据进行处理,不可避免地会遇到字符串和字节数组相互转换的需求。字符串和字节数组的转换,事实上是代表了现实世界信息和数字世界信息之间的转换,要了解其中的机制,需要先对比特、直接以及编码这三个概念有所了解。
(1)比特:bit是一个位,计算机内物理保存的最基本单元,一个bit就是一个二进制位;
(2)字节:byte由8个bit构成,其值可以由一个0~255的整数表示;
(3)编码:编码是数字信息和现实信息的转换机制,一种编码通常就定义了一种字符集和转换的原则,常用的编码方式包括UTF8、GB2312、Unicode等。
下图直观地展示了比特、字节、编码和字符串的关系:
从上图可以看出,字节数组和字符串的转换必然涉及到某种编码方式,不同的编码方式由不同的转换结果。在C#中,可以使用System.Text.Encoding来管理常用的编码。
下面的代码展示了如何在字节数组和字符串之间进行转换(分别使用UTF8、GB2312以及Unicode三种编码方式):
-
public class Program { public static void Main(string[] args) { string s = "我是字符串,I am a string!"; // 字节数组 -> 字符串 Byte[] utf8 = StringToByte(s, Encoding.UTF8); Byte[] gb2312 = StringToByte(s, Encoding.GetEncoding("GB2312")); Byte[] unicode = StringToByte(s, Encoding.Unicode); Console.WriteLine(utf8.Length); Console.WriteLine(gb2312.Length); Console.WriteLine(unicode.Length); // 字符串 -> 字符数组 Console.WriteLine(ByteToString(utf8, Encoding.UTF8)); Console.WriteLine(ByteToString(gb2312, Encoding.GetEncoding("GB2312"))); Console.WriteLine(ByteToString(unicode, Encoding.Unicode)); Console.ReadKey(); } // 字符串 -> 字节数组 static Byte[] StringToByte(string str, Encoding encoding) { if (string.IsNullOrEmpty(str)) { return null; } return encoding.GetBytes(str); } // 字节数组 -> 字符串 static string ByteToString(Byte[] bytes, Encoding encoding) { if (bytes == null || bytes.Length
关注打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?