Index:
(3)字符串、集合与流
(4)委托、事件、反射与特性
(5)多线程开发基础
一、字符串处理
1.1 StringBuilder类型有什么作用?
众所周知,在.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的内部实现中使用了一些指针操作,其内部原理有兴趣的园友可以自己去通过反编译工具查看源代码。
1.2 String和Byte[]对象之间如何相互转换?
在实际开发中,经常会对数据进行处理,不可避免地会遇到字符串和字节数组相互转换的需求。字符串和字节数组的转换,事实上是代表了现实世界信息和数字世界信息之间的转换,要了解其中的机制,需要先对比特、直接以及编码这三个概念有所了解。
(1)比特:bit是一个位,计算机内物理保存的最基本单元,一个bit就是一个二进制位;
(2)字节:byte由8个bit构成,其值可以由一个0~255的整数表示;
(3)编码:编码是数字信息和现实信息的转换机制,一种编码通常就定义了一种字符集和转换的原则,常用的编码方式包括UTF8、GB2312、Unicode等。
下图直观地展示了比特、字节、编码和字符串的关系:
从上图可以看出,字节数组和字符串的转换必然涉及到某种编码方式,不同的编码方式由不同的转换结果。在C#中,可以使用System.Text.Encoding来管理常用的编码。
下面的代码展示了如何在字节数组和字符串之间进行转换(分别使用UTF8、GB2312以及Unicode三种编码方式):
class Program { 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 <= 0) { return string.Empty; } return encoding.GetString(bytes); } }
上述代码的运行结果如下图所示:
我们也可以从上图中看出,不同的编码方式产生的字节数组的长度各不相同。
1.3 BASE64编码的作用以及C#中对其的支持
和传统的编码不同,BASE64编码的设计致力于混淆那些8位字节的数据流(解决网络传输中的明码问题),在网络传输、邮件等系统中被广泛应用。需要明确的是:BASE64不属于加密机制,但它却是把明码变成了一种很难识别的形式。
BASE64的算法如下:
BASE64把所有的位分开,并且重新组合成字节,新的字节只包含6位,最后在每个字节前添加两个0,组成了新的字节数组。例如:一个字节数组只包含三个字节(每个字节又有8位比特),对其进行BASE64编码时会将其分配到4个新的字节中(为什么是4个呢?计算3*8/6=4),其中每个字节只填充低6位,最后把高2位置为零。
下图清晰地展示了上面所讲到的BASE64的算法示例:
在.NET中,BASE64编码的应用也很多,例如在ASP.NET WebForm中,默认为我们生成了一个ViewState来保持状态,如下图所示:
这里的ViewState其实就是服务器在返回给浏览器前进行了一次BASE64编码,我们可以通过一些解码工具进行反BASE64编码查看其中的奥秘:
那么,问题来了?在.NET中开发中,怎样来进行BASE64的编码和解码呢,.NET基类库中提供了一个Convert类,其中有两个静态方法提供了BASE64的编码和解码,但要注意的是:Convert类型在转换失败时会直接抛出异常,我们需要在开发中注意对潜在异常的处理(比如使用is或as来进行高效的类型转换)。下面的代码展示了其用法:
class Program { static void Main(string[] args) { string test = "abcde "; // 生成UTF8字节数组 byte[] bytes = Encoding.UTF8.GetBytes(test); // 转换成Base64字符串 string base64 = BytesToBase64(bytes); Console.WriteLine(base64); // 转换回UTF8字节数组 bytes = Base64ToBytes(base64); Console.WriteLine(Encoding.UTF8.GetString(bytes)); Console.ReadKey(); } // Bytes to Base64 static string BytesToBase64(byte[] bytes) { try { return Convert.ToBase64String(bytes); } catch { return null; } } // Base64 to Bytes static Byte[] Base64ToBytes(string base64) { try { return Convert.FromBase64String(base64); } catch { return null; } } }
上面代码的执行结果如下图所示:
1.4 简述SecureString安全字符串的特点和用法
也许很多人都是第一次知道还有SecureString这样一个类型,我也不例外。SecureString并不是一个常用的类型,但在一些拥有特殊需求的额场合,它就会有很大的作用。顾名思义,SecureString意为安全的字符串,它被设计用来保存一些机密的字符串,完成传统字符串所不能做到的工作。
(1)传统字符串以明码的形式被分配在内存中,一个简单的内存读写软件就可以轻易地捕获这些字符串,而在这某些机密系统中是不被允许的。也许我们会觉得对字符串加密就可以解决类似问题,But,事实总是残酷的,对字符串加密时字符串已经以明码方式驻留在内存中很久了!对于该问题唯一的解决办法就是在字符串的获得过程中直接进行加密,SecureString的设计初衷就是解决该类问题。
(2)为了保证安全性,SecureString是被分配在非托管内存上的(而普通String是被分配在托管内存中的),并且SecureString的对象从分配的一开始就以加密的形式存在,我们所有对于SecureString的操作(无论是增删查改)都是逐字符进行的。
逐字符机制:在进行这些操作时,驻留在非托管内存中的字符串就会被解密,然后进行具体操作,最后再进行加密。不可否认的是,在具体操作的过程中有小段时间字符串是处于明码状态的,但逐字符的机制让这段时间维持在非常短的区间内,以保证破解程序很难有机会读取明码的字符串。
(3)为了保证资源释放,SecureString实现了标准的Dispose模式(Finalize+Dispose双管齐下,因为上面提到它是被分配到非托管内存中的),保证每个对象在作用域退出后都可以被释放掉。
内存释放方式:将其对象内存全部置为0,而不是仅仅告诉CLR这一块内存可以分配,当然这样做仍然是为了确保安全。熟悉C/C++的朋友可能就会很熟悉,这不就是 memset 函数干的事情嘛!下面这段C代码便使用了memset函数将内存区域置为0:
// 下面申请的20个字节的内存有可能被别人用过 char chs[20]; // memset内存初始化:memset(void *,要填充的数据,要填充的字节个数) memset(chs,0,sizeof(chs));
看完了SecureString的原理,现在我们通过下面的代码来熟悉一下在.NET中的基本用法:
using System; using System.Runtime.InteropServices; using System.Security; namespace UseSecureString { class Program { static void Main(string[] args) { // 使用using语句保证Dispose方法被及时调用 using (SecureString ss = new SecureString()) { // 只能逐字符地操作SecureString对象 ss.AppendChar('e'); ss.AppendChar('i'); ss.AppendChar('s'); ss.AppendChar('o'); ss.AppendChar('n'); ss.InsertAt(1, 'd'); // 打印SecureStrign对象 PrintSecureString(ss); } Console.ReadKey(); } // 打印SecureString对象 public unsafe static void PrintSecureString(SecureString ss) { char* buffer = null; try { // 只能逐字符地访问SecureString对象 buffer = (char*)Marshal.SecureStringToCoTaskMemUnicode(ss); for (int i = 0; *(buffer + i) != '