【C#进阶系列】13 字符、字符串和文本编码

本来写了蛮多的,结果因为重启了一下机器导致写的东西都没了。

然后再回想之前写了什么,反而更像是把知识提炼了一番。

关于字符

字符什么的只要记住.net里面都用的Unicode编码就好。字符和数字之间转换用强制转换是最简单且高效的,

字符串是引用类型,存在与堆上,然而同一般的对象用newobj这个IL指令创建不同,字符串由ldstr指令创建。(load string)

关于字符串

字符串是不可变的,所有的String的方法都是创建一个新的字符串。

用+号去拼接字符串,会在堆上创建多个string对象,而堆上的对象考虑到垃圾回收就会影响性能,所以建议用StringBuilder去拼接。

字符串比较

虽然String提供了一堆比较方法,并且《CLR via C#》这本书的作者也推荐用这些比较,因为==和!=这种比较方式调用者并没有显式指出用什么规则来比较,而如果显示地指出以什么规则来比较,代码容易阅读和维护。

 var str1 = "字符串1";
 var str2 = "字符串2";
 bool result1= str1.Equals(str2, StringComparison.OrdinalIgnoreCase);//使用序号排列(就是说对本地语言文化不敏感),并忽略大小写来比较。
 bool result2 = str1 == str2;//常用的比较

然而,于我而言,确实是==更加明了,这个就看个人了。这些用方法比较的时候确实在有些多语言文化的场景比较好用,然而对于一般场景,我个人认为==更好一点,起码我自己看起来更好阅读和理解。

以StringComparison.Ordinal规则比较的话,CLR会快速先比较字符数量,数量相同才继续比较单独字符。而如果执行语言文化敏感的话,即使数量不同也有可能相等,所以一开始就会比较单个字符,这样就很耗性能。

System.StringComparer类也能执行字符串比较,它适用于大量不同字符串反复执行同一种比较。

字符串留用

CLR可通过一个String对象共享多个完全一致的String内容,这样就减少了字符串数量,节省内存,这就是字符串留用。

在.NET 4.5中自然会在程序集加载时对代码中的字面量字符串进行字符串留用,然而之前的版本就需要手动了。

String.Intern方法就是字符串留用的方法,将字符串加入一个哈希表中,如果哈希表中有就不加入,没有就加入。

这样当然可以减少内存,因为以后只引用一个字符串对象。但是要明白留存字符串这个操作也是需要消耗性能的。所以具体情况具体分析,还是需要慎重使用字符串留存。

字符串池

对于所有的字面量字符串中,相同内容的字符串,实际上都是引用的字符串池中的一个字符串。这是在C#编译器编译的时候就已经弄好的。

高效率构造字符串——StringBuilder

StringBuilder从字面意义上就很好理解了,字符串拼接什么的就用它好了。可以认为里面就是一个字符数组。

然而要理解StringBuilder的以下概念

  • 最大容量(MaxCapacity)
    • 指定了字符串中的最大字符数。默认值是Int32.MaxValue(约20亿)。
    • 一般不用理会,除非是要限制一个字符串的最大字符数。
  • 容量(Capacity)
    • 前面说到,可以将StringBuilder里面认为是一个字符数组,那么容量就指定了当前字符数组的长度。
    • 为什么说是当前呢?因为如果字符串拼接后超过了这个容量值,那么容量就会自动*2,且用新容量来分配新数组,并将原始数组中的字符串复制到新数组中。随后原始数据被垃圾回收。
    • 所以看到这里你就应该很明白一点,用StringBuilder最好在开始的时候自己预估一个容量,最起码不要让他频繁扩容,要不然真是坑,还不如用String。
  • 字符数组
    • 也就是StringBuilder里面由Char结构构成的数组。
    • 它的长度用Length获得

一般用用Append和AppendFormat进行追加字符串,当然也有其它的操作,只要明白里面操作的是一个数组就好。

虽然本书还介绍了一些字符串格式化和解析字符串的方式,然而

字符串编码——字符和字节的相互转换

对于使用汉字的我们使用字符串的话用Unicode没什么影响,因为汉字就占两个字节,然而对于英文字符实际上仅仅用一个字节就够了,但是在Unicode中还是会占两个字节,其中一个字节用于表示这个英文字符,另一个字节干脆就是\0。

所以一些英文翻译啊什么的,或者一大段英文文章的传送,那么将这些Unicode字符串编码成压缩的字节数组传送起来更有效率。

通常也就是用System.IO.BinaryWriter或者System.IO.StreamWriter时,需要进行编码,相应的读取时也需要解码。

一般不指定一种编码方案,那么就默认为UTF-8。(可以简单理解为中文两个字节,英文一个字节)

还有一种常用编码方案是UTF-16,也就是中英文都是两个字节,也被称为Unicode编码。(对于汉字而言,其实用UTF-16编码,比UTF-8更快)

其它的编码方式就不说了,对于我们而言基本上都是坑。

当我们进行编码时尽量用Encoding.Unicode获取编码方案构造对象,而不是用System.Text.UnicodeEncoding这种。

因为前者如果之前有请求会直接返回上次请求的对象给你,不会为每个请求构造新的对象。

而后者每次都会在托管堆中创建新的对象,所以会对性能有所影响。然而在System.Text中的这些派生自Encoding的编码类有特殊的构造器可以在对无效序列解码时抛出异常,所以如果要保证安全性,防范无效输入那么用后面这种比较好。

获取了这些编码方案构造对象后就可以利用GetBytes和GetString来将字符串转换为字节数组和将字节数组转换为字符串。

字节流的编码

就是通过System.Net.Sockets.NetworkStream对象读取一个UTF-16编码字符串,因为这种字节流通常以数据块形式传输,而如果一次从流中读取5个字节,而不是2的倍数的字节数,那么就可能会造成数据损坏。

所以可以用Encoding.Unicode.GetDecoder()获取一个新的构造对象,这个对象含有GetChars和GetCharCount两个方法。调用GetChars时它会尽可能多的解码,如果解码数组的字节不足以完成一个字符时,那么剩余的字符会保存到这个Decoder内部,下次调用它时,此Decoder会利用之前剩余的字节,再加上传给它的字节数组来进行解码。从流中读取Decoder对象的作用很大。

相反的编码一样。

以下为简单的Decoder解码示例

string strTroy = "奇葩";
Byte[] bytesTroy = Encoding.Unicode.GetBytes(strTroy);//形成长度为4的字节数组
Byte[] b1 = { bytesTroy[0], bytesTroy[1], bytesTroy[2] };//一个奇,半个葩
Byte[] b2 = { bytesTroy[3] };//半个葩
//以上操作算是模拟了按数据块获取,接下来
var decoder = Encoding.Unicode.GetDecoder();
char[] result=new char[10];//解码后的字符数组
var charindex = decoder.GetCharCount(b1, 0, b1.Length);//若解码b1能形成的字符个数
decoder.GetChars(b1,0, b1.Length, result, 0, false);//第一个0为从b1第0个位置开始解码,第二个0是从result的第0个位置开始写入
decoder.GetChars(b2,0, b2.Length, result, charindex, false);
Console.WriteLine(string.Join("",result));//奇葩

安全字符串

System.Security.SecureString类,就是一个更安全的字符串类。

构造这个类的对象后,会在内部分配一个非托管内存块,以避开垃圾回收器。

和String对象不同,SecureString对象在回收后加密字符串的内容将不再存在于内存中。

当然这样的字符串如果不是信用卡啊密码什么的就不需要,毕竟会有性能影响。

你可能感兴趣的:(【C#进阶系列】13 字符、字符串和文本编码)