读书笔记—CLR via C#字符串及文本

前言

这本书这几年零零散散读过两三遍了,作为经典书籍,应该重复读反复读,既然我现在开始写博了,我也准备把以前觉得经典的好书重读细读一遍,并且将笔记整理到博客中,好记性不如烂笔头,同时也在写的过程中也可以加深自己理解的深度,当然同时也和技术社区的朋友们共享

字符(System.Char)

  • 字符在.NET 中表示成16位Unicode代码值
  • System.Char.MinValue = '\0' MaxValue = '\uffff'
  • 实例方法GetUnicodeCategory,返回System.Globalization.UnicodeCategory枚举(包含控制字符、货币符号、小写字母、大些字母、标点符号、数学符号或者其他由Unicode定义的字符)
  • 简化接口为IsDigit, IsLetter, IsUpper, IsLower, IsPunctuation, IsLetterOrDigit, IsControl, IsNumber, IsSeparator, IsSurrogate等
  • 以上Is简化接口内部都调用GetUnicodeCategory,并简单返回true或false
  • ToLowerInvariant或ToUpperInvariant忽略语言文化 ToLower和ToUpper则使用调用线程关联的语言文化信息
  • CompareTo忽略语言文化结果,GetNumericValue返回字符的数值形式
  • 与数值进行转型
    • 转型(强制类型转换)效率最高,直接使用编译器IL指令。不必调用额外的方法。可指定checked或unchecked
    • Convert,强制使用checked方式。如果发现数据丢失,就会抛出OverflowException
    • 使用IConvertible 接口,数值类型都实现了此接口,效率最差,因为使用接口会进行装箱。对于无法转换会InvalidCastException

字符串(System.String)

  • String代表不可变的顺序字符集,String代表不可变的顺序字符集
  • CLR用特殊的方式构造文本常量String对象,构造函数允许使用字符指针作为参数构造
  • 对于非文本常量字符串使用+操作符,连接则会在运行时进行,对于连接多个字符串,避免使用+操作符,它会在堆上创建多个字符串对象,增加GC的负载影响性能
  • 字符串不可变,所以操作或访问字符串时不会发生线程同步问题
  • CLR通过“字符串留用”机制让多个相同的string共享同一个string对象。节省性能
  • 字符串显式留用
    • Intern,获取一个String,获得它的哈希吗,并在内部哈希表中检查是否有相匹配的。如果存在一个完全相同的,返回对这个已经存在的String对象的一个引用。如果不存在,创建副本,将副本添加道内部哈希表,返回对副本的引用
    • IsInterned,也获取一个String,并在内部哈希表中查找它。如果找到,返回留用对象的引用。然而如果没有,则会返回null,它不会将字符串添加到哈希表中
  • 程序集加载时,CLR默认会留用程序集的元数据中描述的所有文本常量字符串
  • 程序集标记 assembly attribute/flag System.Runtime.CompilerServices.CompilationRelaxationsAttribute(CompilationRelaxations.NoStringInterning) 特性,指定程序集默认不进行字符串留用,一般CLR会忽略该属性
  • 编译器只在模块的元数据中将文本常量字符串写入一次,引用该字符串的所有代码都会被修改,以引用元数据中的同一个字符串。这样可以减少模块和程序集的大小。其实C/C++编译器多年来也一直采用这个技术,称为字符串池。字符串池时提升字符串性能的一种有效方式
  • 代理项字符串,使用System.Globalization.StringInfo类型处理。提供功能将字符串拆分为文本元素并循环访问这些文本元素
  • string对象的一些接口,比如Copy和CopyTo,会创建新的字符串对象,确保引用(指针)不同,即使字符串包含相同字符内容,还有Insert,Remove,PadLeft,Replace,Split等等,都是返回一个新的字符串对象
  • string.ToString() 返回对同一个对象(this)的引用

字符串构造器(System.Text.StringBuilder)

  • 字符串构造器,动态构造字符串。包含由Char结构组成的数组字段,高效率缩短字符串或更改字符串中的字符,如果字符串太大,SB自动分配一个新的更大的数组,复制字符,并开始使用新数组。前一个数组会被垃圾回收。建议在可预见的情况下指定StringBuilder的容量
  • 内部方法EnsureCapacity,其实List<T>等集合也有这么一个方法。负责维护容量和自动扩容
  • StringBuilder的很多方法都是返回同一个引用,所以方便链式调用 sb.Append().Replace().Remove()...

字符串显示与文化

  • 在ToString方法的实现中,为了使调用者能选择格式和语言文化,类型应该实现System.IFormattable接口。基元数值基本都实现了这个接口
  • 格式化一个数字时,ToString会检查为formatProvider参数传递的值,如果传递的是null,ToString会通过读取Thread.CurrentThread.CurrentCulture属性判断与调用线程关联的语言文化。如果一个类型实现了该接口,就认为类型的一个实例能提供符合语言文化的格式信息,与调用线程关联的语言文化应被忽略。一般使用的实现为 CultureInfo,注意属性对象为NumberFormatInfo和DateTimeFormatInfo。针对数字和时间进行格式化。(format默认为常规格式,formatProvider默认为调用线程的语言文化信息。)
  • 如果不针对具体语言文化格式化,应该调用CultureInfo.InvariantCulture, 语言中立
  • FCL的IFormatProvider接口实现,1 CultureInfo 2 NumberFormatInfo 3 DateTimeFormatInfo
  • 当使用自定义ICustomFormatter时,最好使用string.Format的方式构造字符串,不要用ToString,因为像字符串或日期都对Format做了限制
  • 一般数值或日期的ToString中的Provider,使用CultureInfo来代替就OK,或者自己提供DateTimeFormatInfo或NumberFormatInfo,实际上前者包含了后面两者的属性定义,在GetFormat中判断调用对象的类型返回后面两者中的一个
  • 自定义格式需要实现IFormatProvider和ICustomFormatter,然后把参数传递给执行自定义格式的设置操作方法,比如String.Format

字符串解析和转换

  • Convert,Convert基本上是一个封装,在内部调用ToString或者Parse等方法。内部主要接口就是Number.Parse(NumberStyles)和DateTime.Parse(DateTimeStyles)等
  • Parse和TryParse方法

字符编码

  • UTF-16时将每个16位字符编码为2个字节,不对字符产生影响,也不会压缩,性能出色
  • UTF-8将部分字符编码为1个字节,部分编码为2个字节。部分编码为3甚至4个字节,对应区间段: 0x0080/0x0080~0x07FF/0x0800/surrogate pairs
  • UTF-32使用4字节来编码,每个字符都是4个字节,不需要考虑代理项的问题,通常在内部使用
  • UTF-7编码,用于旧式系统。使用7位值表示,此编码方案已被Unicode协会淘汰
  • ASCII编码方案将16位字符编码成ASCII字符;值小于0x0080的16位字符被转换成单字节。超过0x007F的任何字符都不能被转换,否则字符的值会丢失。由于字符串完全由ASCII范围(0x00~0x7F)内的字符构成,ASCII编码方案就能将数据压缩到原来的一般,而且速度非常快(高位字节会被直接截掉。)不过如果字符在ASCII范围之外,编码就不适合,因为字符的值会丢失
  • 编码接口,System.Text.Encoding 例如:Byte[] encodedBytes = encodingUTF8.GetBytes(s); BitConverter.ToString(encodedBytes)
  • Encoding.Default属性返回一个对象,它使用用户当前的代码页进行编码/解码。当前用户页编码在控制面板的“区域和语言选项”对话框中,通过“非Unicode程序中所使用的当前语言”的选项来指定,通常不鼓励开发人员使用Default属性,否则程序的行为随着机器的设置而变
  • 分配字节时可以利用方法Encoding派生类的GetByteCount方法,它能统计对一组字符进行编码所产生的字节数,同时不实际进行编码
  • GetCharCount()返回解码得到的字符数,同时也不实际进行编码。可以节省内存和重用数组
  • 以上两个方法性能不佳,如果要性能更加,使用GetMaxByteCount和GetMaxCharCount方法,返回最坏情况下的值
  • 针对文件流或者网络流,如果需要跨块处理的字节流,则需要使用Encoder(通过GetEncoder获得)和Decoder(通过GetDecoder获得),它们会维护连续调用间的状态信息,它会尽可能多的解码字节数组,如果字节数组包含的字节不足以完成一个字符,剩余的字节会保存到Decoder对象内部。下次调用时,它们会利用之前剩余的字节再加上新的字节。
  • 中文的全角和半角问题,因为所有的字符在CLR中都是以Unicode-16编码的,这个问题就比较好处理了,全角和半角的值它们相差65248,除了空格相差12256。所以全角的字符若是想转换成半角除空格减12256外,其他相减65248便是相应的半角。全角空格为12288,半角空格为32。其他字符半角(33-126)与全角(65281-65374)的对应关系是:均相差65248
  • BOM,BOM全称是Byte Order Mark,即字节顺序标记,是一段二进制,用于标识一个文本是用什么编码的,比如当用Notepad打开一个文本时,如果文本里包括这一段BOM,那么它就能判断是采用哪一种编码方式,并用相应的解码方式,就会正确打开文本不会有乱码。如果没有这一段BOM,Notepad会默认以ANSI打开,这种会有乱码的可能性。可以通过Encoding的方法GetPreamble()来判断这编码有没有BOM,目前CLR中只有下面5个Encoding有BOM
  • Encoding.Convert可以对字节数组执行编码转换
  • 如果给定一个文本,我们不知道它的编码格式,解码时我们如何选择Encoding呢?答案是根据BOM来判断到底是哪种Unicode。UnicodeEncodings[i].GetPreamble()
  • 关于编码解码的一些文章参考:http://www.cnblogs.com/criedshy/
  • 字符集及编码:http://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html

Base64字符串编码和解码

  • Base-64字符串编码解码,也是一种流行的编码方案,使用System.Convert类的静态方法。
  • 具体方法包括 FromBase64String, FromBase64CharArray, ToBase64String, ToBase64CharArray

安全字符串

  • 字符串对象可能包含敏感数据,如果和不安全的非托管代码交互,可能造成机密数据的泄露。
  • System.Security.SecureString,构造时,在内部分配一个非托管内存块,其中包含一个字符数组,之所以要使用非托管内存块,是为了避开垃圾回收的“罗网”,这些字符串是经过加密的,能防范任何恶意的非安全非托管代码获取机密信息。提供AppendChar, InsertAt, RemoveAt, SetAt等方法。内部会解密字符串然后重新加密字符串。对性能会有影响。通过IDisposable接口可以进行销毁。其中一个字段引用SafeBuffer继承与CriticalFinalizerObject,所以字符串在垃圾回收时,字符内容会保证清零,缓冲区可以释放。而且和string不同,回收之后,加密字符串的内容将不再存在于内存中
  • 解密过程,(char*)Marshal.SecureStringToTaskMemUnicode(ss),指针释放Marshal.ZeroFreeCoTaskMemUnicode(char*)
  • Marshal类提供了一系列的方法永远操纵安全字符串,包含将字符串解密到缓冲区,以及清零并释放缓冲区
  • 关于安全这块,还可以关注以下几块的东西
    • System.Security.Cryptography.ProtectedData
    • 使用System.Security.Cryptography.ProtectedMemory类
    • Configuration cfg.AppSettings.SectionInformation.ProtectSection
    • 未完待续。。。可以在探讨.NET安全时继续探讨