- A+
字符串是日常编码中最常用的引用类型了,可能没有之一,加上字符串的不可变性、驻留性,很容易产生性能问题,因此必须全面了解一下。
01、字符与字符编码
1.1、字符Char
字符 char 表示为 Unicode字符,在C#中用 UTF-16 编码表示,占用2个字节(16位)大小,字面量用单引号''
包裹。
char c = 'A'; Console.WriteLine(char.IsDigit('3')); Console.WriteLine(char.IsNumber('1')); Console.WriteLine(char.IsLetter('A')); Console.WriteLine(char.IsLower('a')); Console.WriteLine(char.IsUpper('A')); Console.WriteLine(char.GetUnicodeCategory('A')); //获取字符分类
- char 是值类型(结构体),以16位整数形式存储,
char
可隐式转换为int
。 - 字符串可以看做是
char
序列(数组),字符串是引用类型。
string str = "Hello World"; Console.WriteLine(str[0]); //H Console.WriteLine(str[10]); //d Console.WriteLine(str[0].GetType().Name); //Char
1.2、字符集Unicode与字符编码
一般情况下字符串长度string.Length
就是可见的文本字符数量,但这并不绝对相等。大多数字符都是一个char组成,然而有些字符无法用一个char表示,如表情、不常用字符等,他们会用两个char(4个字节)来表示。
"a".Length.Dump(); //1 "?".Length.Dump(); //2 "?".Length.Dump(); //2 "⏰".Length.Dump(); //1 "你好".Length.Dump(); //2 "臢".Length.Dump(); //1 $"{(int)'A':X4}".Dump(); //0041 //上面的dump() 是一个扩展方法,作用同Console.WritLine()
Unicode 是国际标准、通用字符集,涵盖了世界上几乎所有的文字、符号,可以满足跨平台、跨语言的文本信息编码。Unicode 有100W+个字符地址空间,地址范围是 0x0000 - 0x10FFFF,每个字符都有自己的编码,目前已分配了大约10W+个。通常使用“U+”后跟一个十六进制数来表示,例如字母A
的Unicode码点是U+0041
。
Unicode 字符集中包含多个分类(平面):其中最常用的就是基本平面,大部分常用字符都在这里面。
- ?基本多文种平面(BMP,Basic Multilingual Plane):Unicode 的BMP区域几乎包含了所有常用的字符,如几十种主流语言,及30000+的汉字,BMP区域的字符都只需要1个
char
(2个字节)表示。 - ?辅助平面(SMP):包含其他不常使用的字符,如一些历史文字、音乐符号、数学符号和表情符号等。该区域大多用两个
char
(4个字节)表示一个符号。
Unicode 是一种字符集,而实际在计算机上存储时需要用一个确定的编码方案,常见的就是UTF-8、UTF-16、UTF32。
- UTF-16:2个字节表示BMP中的字符,其他字符会需要4个字节,C#、Java语言内部就是使用的UTF-16来表示的字符串。
- UTF-8:变长编码,使用1到4个字节来表示一个Unicode字符,在互联网使用广泛。特别是存储 ASCII 为主的内容时,变长编码可以显著节约存储空间。
?ASCII 字符集只包含 128个 基础字符,涵盖键盘上的字母、数字、常用符号。Unicode 是包含 ASCII字符集的,最前面128 个字符就是。在UTF-8编码中 ASCII字符只需要1个字节。
02、String基础
字符串 string 是一个不可变(不可修改)的字符序列(数组),为引用类型,字面量用双引号""
包裹。
string s1 = "sam"; string s2 = new string('1',5);//11111 Console.WriteLine(s2[0]); //像数组一样操作字符串中的字符 string s3 = ""; string s4 = string.Empty; //效果同上 //相等比较 object s1= "Hello".Substring(0,2); object s2 = "Hello".Substring(0,2); (s1==s2).Dump(); //False (s1.Equals(s2)).Dump(); //True
- 字符串是引用类型,因此可以用
null
表示,不过一般空字符建议用string.Empty
(或""
)表示。 - 字符串可以当做 字符数组一样操作,只是不能修改。
- 字符串的相等为值比较,只要字符序列相同即可。例外情况请是如果用
object
做==
比较,只会比较引用地址。
? 字符串在存储、转换为字节码时需指定编码,一般默认为 UTF-8,这是广泛使用的编码类型,更节省空间。
2.1、字符串常用API
属性 | 特点/说明 |
---|---|
Length | 字符串中字符数量 |
索引器[int index] | 索引器,用索引获取字符,不可修改 |
?方法 | 特点/说明 |
StartsWith、EndsWith(String) | 判断开头、结尾是否匹配,"Hello".StartsWith("He") |
Equals(String) | 比较字符串是否相同 |
IndexOf() | 查找指定字符(串)的索引位置,从后往前查找 LastIndexOf |
Insert(Int32, String) | 指定位置插入字符串,‼️返回新字符串! |
PadLeft(Int32) | 指定字符宽度(数量)对齐,左侧填充,‼️返回新字符串!右侧填充 PadRight(Int32) |
Remove(Int32, Int32) | 删除指定位置、长度的字符,‼️返回新字符串! |
Replace(String, String) | 替换指定内容的字符(串),‼️返回新字符串! |
Substring(Int32, Int32) | 截取指定位置、长度的字符串,‼️返回新字符串! |
ToLower()、ToUpper() | 返回小写、大写形式的字符串,‼️返回新字符串! |
Trim() | 裁剪掉前后空格,‼️返回新字符串!有多个配套方法 TrimEnd、TrimStart |
Split(char) | 按分隔符分割字符串为多个子串,比较常用,不过性能不好,建议用Span代替。 |
?静态方法 | 特点/说明 |
Empty | 获取一个空字符串(同"" ) |
Compare(String, String) | 比较两个字符串,有很多重载,返回一个整数,0表示相同。 |
Concat (params string?[]) | 连接多个字符串,返回一个新的字符串,有很多重载,是比较基础的字符串连接函数。 |
Equals(str, StringComparison) | 比较字符串是否相同,可指定比较规则 StringComparison |
Format(String, Object[]) | 字符串格式化,远古时期常用的字符串格式化方式,现在多实用$插值 |
string Intern(String) | 获取“内部”字符串,先检查字符串池中是否存在,有则返回其引用,没有则添加并返回 |
string? IsInterned(String) | 判断是否在字符串池中,存在则返回其引用,没有则返回null |
IsNullOrEmpty(String) | 判断指定的字符串是否 null 、空字符"" /String.Empty ,返回bool |
IsNullOrWhiteSpace(String) | 判断指定的字符串是否 null 、空字符"" /String.Empty 、空格字符,返回bool |
Join(Char, String[]) | 用分隔符连接一个数组为一个字符串 |
2.2、字符串的不变性、驻留性
字符串是一种有一点点特别的引用类型,因为其不变性,所以在参数传递时有点像值类型。
- ?不变性:字符串一经创建,值不可变。对字符串的各种修改操作都会创建新的字符串对象,这一点要非常重视,应尽量避免,较少不必要的内存开销。
- ?驻留性:运行时将字符串值存储在“驻留池(字符串池)”中,相同值的字符串都复用同一地址。
不变性、驻留性 是.Net
对string 的性能优化,提升字符串的处理性能。如下示例中,s1、s2字符串是同一个引用。
string s1 = "hello"; string s2 = "hello"; Console.WriteLine(s1 == s2); //True Console.WriteLine(s1.Equals(s2)); //True Console.WriteLine(Object.ReferenceEquals(s1,s2)); //True
当然不是所有字符串都会驻留,那样驻留池不就撑爆了吗!一般只有两种情况下字符串会被驻留:
- 字面量的字符串,这在编译阶段就能确定的“字符串常量值”。相同值的字符串只会分配一次,后面的就会复用同一引用。
- 通过
string.Intern(string)
方法主动添加驻留池。
string st1 = "123" + "abc"; string st2 = "123abc"; string st3 = st2.Substring(0,3);
看看上面代码生成的IL代码:
- 常量的字符串
"123" + "abc"
连接被编译器优化了。 - 常量字符串使用指令“ldstr”加载的到栈,该指令会先查看驻留池中是否已存在,如果已存在则直接返回已有字符串对象的地址,否则就加入。
驻留的字符串(字符串池)在托管堆上存储,大家共享,内部其实是一个哈希表,存储被驻留的字符串和其内存地址。驻留池生命周期同进程,并不受GC管理,因此无法被回收。因此需要注意:
lock
锁不能用string,避免使用同一个锁(字符串引用)。- 避免创建字面量的大字符串,会常住内存无法释放,当然也不要滥用
string.Intern(string)
方法。
2.3、字符串的查找、比较
string 的 比较字符串 是默认包含文化和区分大小写的顺序比较,C#内置的一个字符串比较规则(枚举)StringComparison,可设置比较规则。在很多内置方法中使用,包括 String.Equals、String.Compare、String.IndexOf 和 String.StartsWith等。
? 微软官方建议在使用上述字符串比较方法中明确指定 StringComparison 参数值,而不是默认的比较规则。
public enum StringComparison { CurrentCulture, CurrentCultureIgnoreCase, InvariantCulture, InvariantCultureIgnoreCase, Ordinal, OrdinalIgnoreCase } void Main() { string.Equals("ABC","abc",StringComparison.Ordinal); //Fasle string.Equals("ABC","abc",StringComparison.OrdinalIgnoreCase); //True string.Compare("ABC","abc",StringComparison.Ordinal); //-32 string.Compare("ABC","abc",StringComparison.OrdinalIgnoreCase);//0 }
枚举值 | 说明 |
---|---|
CurrentCulture | 本地语言区域规则,适用于给用户显示的内容 |
CurrentCultureIgnoreCase | 同上+忽略大小写 |
InvariantCulture | 固定语言区域,适用于存储的数据 |
InvariantCultureIgnoreCase | 同上+忽略大小写 |
Ordinal | 二进制值顺序比较字符串,比较快⚡ |
OrdinalIgnoreCase | 同上+忽略大小写 |
如果单纯从性能角度考虑,考虑语言文化的字符串比较其实比较慢,来测试对比一下。测试代码:
string s1 = "hellohellohellohello"; string s2 = "helloHelloHelloHello"; public bool Equals() => s1.Equals(s2);//False public bool Equals_CurrentCulture() => s1.Equals(s2,StringComparison.CurrentCulture);//False public bool Equals_CurrentCultureIgnoreCase() => s1.Equals(s2,StringComparison.CurrentCultureIgnoreCase);//True public bool Equals_InvariantCulture() => s1.Equals(s2,StringComparison.InvariantCulture);//False public bool Equals_InvariantCultureIgnoreCase() => s1.Equals(s2,StringComparison.InvariantCultureIgnoreCase);//True public bool Equals_Ordinal() => s1.Equals(s2,StringComparison.Ordinal);//False public bool Equals_OrdinalIgnoreCase() => s1.Equals(s2,StringComparison.OrdinalIgnoreCase);//True public bool Equals_Span() => s1.AsSpan() == s2.AsSpan();//False
- 上面7个方法 分别测试了
Equals
的默认版本、及带参 StringComparison 的不同比较规则的性能。 - 最后加了一个使用
Span
的相等比较,更多关于Span的资料查看《高性能的Span、Memory》。
?测结结论:
Span
最快,其次无参Equals()
版本、Ordinal
,他们都是只比较二进制值,不考虑文化信息。- 个人理解,如果不考虑一些比较特别的语言(如瑞典语、土耳其语、 阿塞拜疆语等),只是针对英文、中文的字符串,一般不用考虑文化语义。
Equals()
默认是不考虑文化语义的字符值比较,但有些比较方法就不一定能了,比如StartsWith
、Compare
默认的是带文化语义的CurrentCulture
规则,因此推荐主动配置 StringComparison 参数。
2.4、字符串转义
转义字符:反斜杠“”
转义序列 | 字符名称 | Unicode 编码 |
---|---|---|
' | 单引号 | 0x0027 |
" | 双引号 | 0x0022 |