
字符串是日常编码中最常用的引用类型了,可能没有之一,加上字符串的不可变性、驻留性,很容易产生性能问题,因此必须全面了解一下。
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 |
| \0 | null | 0x0000 |
| \b | backspace | 0x0008 |
| \f | 换页 | 0x000c |
| \n | 换行 | 0x000a |
| \r | 回车 | 0x000d |
| \t | 水平制表符 | 0x0009 |

03、🚩字符串连接的8种方式
字符串连接(组装)的使用是非常频繁的,.net中提供了多种姿势来实现,各有特点。
| 连接方法 | 示例/说明 |
|---|---|
| 直接相加 | "hello"+str,其实编译后为 string.concat ("hello", str) |
| 连接函数:string.concat() | 字符串相加一般就是被编译为调用string.concat()方法,有很多重载,支持任意多个参数 |
| 集合连接函数:string.join() | 将(集合)参数连接为一个字符串,string.join('-',1,2,3); //1-2-3 |
| 格式化:string.format() | 传统的字符串格式化手艺,string.format("name:{0},age:{1}",str,18) |
| $ 字符串插值 | 用花括号{var}引用变量、表达式,强大、方便,$"hello {name} !" |
@逐字文本字面量 |
支持转义符号、换行符,常用于文件路径、多行字符:@$"c:\\users\\{name}\\downloads" |
"""原始字符串字面量 |
c# 11,三个双冒号包围,支持多行文本的原始字面量。 |
stringbuilder |
当处理大量字符串连接操作时,推荐使用stringbuilder,效果更优。 |
字面量字符串的相加会被编译器优化,直接合并为一个字符串。
var str1 = "hello " + "world" + " !"; var str2 = datetime.now.year + "年" + datetime.now.month + "月"; //编译后的代码: string str1 = "hello world !"; string str2 = string.concat (datetime.now.year.tostring (), "年", datetime.now.month.tostring (), "月");
3.1、字符串格式化 string.format
string.format 方法是早期比较常用的字符串组织方式,后来$字符串插值 问世后就逐步被打入冷宫了。
string.format("{0}+{1} = {2}",1,2,3); //1+2 = 3
string.format("hello {0},{0}","sam"); //hello sam,sam
string.format("it is now {0:yyyy-mm-dd} at {0:hh:mm:ss}", datetime.now); //it is now 2024-01-17 at 10:56:33
string.format("买了{0}个桔子,共花了{1:c2}。", 4,25.445); //买了4个桔子,共花了¥25.45。
基本语法规则就是用 {index}来占位,在后面的参数中给出值。
- 索引位置从0开始,必须连续递增,可以重复。
- 索引的位置对应后面参数的顺序位置,必须对应,参数不能少(抛出异常),可以多。
- 字符串格式规则参考后文《字符串格式总结》。
3.2、$字符串插值
字符串插值的格式:$"{<interpolationexpression>}",大括号中可以是一个变量,一个(简单)表达式语句,还支持设置格式。功能强大、使用方便,老人孩子都爱用!
{}字符转义,用两个{{}}即可,如果只有一边,则用单引号'{{',即输出为{。- 使用三元运算符
?表达式,用括号包起来即可,因为“:”在插值字符串中有特殊含义,即格式化。 - 字符串格式规则参考后文《字符串格式总结》。
var name = "sam";
console.writeline($"hello {name}!"); //hello sam!
console.writeline($"日期:{datetime.now.adddays(1):yyyy-mm-dd hh:mm:ss}"); //日期:2024-01-18 23:21:55!
console.writeline($"threadid:{environment.currentmanagedthreadid:0000}"); //threadid:0001
console.writeline($"length:{name.length}"); //length:3
console.writeline($"length:{(name.length>3?"ok":"error")}"); //length:error
3.3、@字符串支持任意字符
@标记的字符串为字面量字符串 ,不需要使用转义字符了,可搭配$字符串插值使用。文件路径地址都会用到@,两个冒号表示一个冒号,@"a""b" ==a"b。
var path= @"d:\gapp\linqpad 8\x64";
var file = $@"d:\gapp\linqpad 8\x64\{datetime.now:d}";
var maxtext = @"hi all:
第一行
换行
";
3.4、👍🏻stringbuilder
stringbuilder 字符串修理工程师,顾名思义,就是专门用来组装字符串的,可以看做是一个可变长字符集合。适用于把很多字符串组装到一起的场景,避免了大量临时字符串对象的创建,可显著提升性能。
var sb = new stringbuilder(100);
sb.append("sam");
sb[0] = 'f'; //fam
sb.appendline("age");
sb.append("age").append(environment.newline); //效果同上
sb.insert(2,"---");
sb.replace("age","age");
var result = sb.tostring(); //获取结果
| 属性 | 特点/说明 |
|---|---|
| capacity | 获取、设置字符容量(实际占用内存),默认16,当内容增多容量不足时,会自动扩容。 |
| maxcapacity | 获取最大容量,20亿字符 |
| length | 实际字符内容的长度,可赋值,设置0则清空已有字符内容,但并不影响 capacity。 |
| chars[int32] | 索引器,可获取、设置字符 |
| 🔸方法 | 特点/说明 |
| stringbuilder(int32) | 构造函数,参数指定初始容量capacity |
| append(value) | 追加字符,很多重载版本,类似还有appendformat、appendjoin |
| appendline | 追加字符后,再追加一个换行符 |
| insert (int index, value) | 指定位置插入字符内容 |
| replace(char, char) | 查找替换字符(字符串)内容,会替换所有找到的字符内容 |
| tostring() | 将 stringbuilder 输出为一个字符串,一般是stringbuilder的命运终点。 |
- 各种
append方法都返回自身,可用来链式编程。 stringbuilder默认容量为16,内部有一个char数组m_chunkchars(缓冲区)来存储字符内容,如下stringbuilder构造函数源码:
public stringbuilder()
{
m_maxcapacity = int.maxvalue;
m_chunkchars = new char[16];
}
- 当不断追加字符串,容量不足会自动扩容,扩容的过程其实就是创建更大的字符数组(容量翻倍),把原来的值拷贝过来,这个过程会涉及数组对象创建、内存拷贝。
📢 一般使用
stringbuilder建议尽量给一个合理的默认容量大小,尽量避免、减少频繁的扩容。
04、🚩字符串格式化大全
📢字符串格式语法:
{index/interpolationexpression [,alignment][:formatstring]}
,alignment可选,设置字符串的对齐长度,如果位数不够则空格补齐,正数部补左边,负数补右边。:formatstring指定格式规则。一次只能指定一个格式规则,可和,alignment共存。
//,alignment 示例
var name = "sam";
$"name:{name,6}."; //字符长度6,前面补齐空格 //name: sam.
$"name:{name,-6}."; //字符长度6,后面补齐空格 //name:sam .
"1123+1 = {(1223+1),6:#,#.##}"; //1123+1 = 1,224
string.format("1123+1 = {0,6:#,#.##}",1223+1); //1123+1 = 1,224
4.1、数值格式
🚩标准数值格式:
| 🔸数值格式 | 说明 |
|---|---|
| e3/e3 | 科学计数法(指数),数字"3"为小数精度,$"{12345.2:e3}" //1.235e+004,e+4表示10的4次方;如果是e-4则表示为小数(除以10的四次方) 1e-4 = 0.0001 |
| f4 | 定点格式,小数精度为"4",位数不够后面补0,支持所有数值类型,$"{123.22f:f4}" //123.2200 |
| g4 | 定点格式f+指数e的结合版,最多"4"个有效数字,超过就用科学计数法。"{123:g2}" //1.2e+02,$"{123:g4}" //123 |
| c3 | 货币格式(支持千分位),数字“3”为小数位数,$"{123.346:c2}" //¥123.35 |
| p2 | 百分比格式,数字乘以100后转换为百分数,数字“2”为小数位数,$"{0.2:p2}" //20.00% |
| n6 | 数字格式化(支持千分位),小数位数为6,不够后面补0,$"{123:n6}" //123.000000 |
| d6 | 整数定长格式,不够前面补0,只支持整数,$"{123:d6}" //000123 |
| b | 输出为二进制格式,仅支持整数+.net8,精度为字符串位数,不够补0,$"{123:b}" //1111011 |
| x/x | 输出为十六进制格式,仅支持整数+,精度为字符串位数,不够补0,$"{12:x4}" //000c |
🚩自定义的数值格式:
| 🔸数值格式符号 | 说明 |
|---|---|
# |
数字占位符,不强制占位,$"{123:#,###.##}" //123 |
0 |
数字(0)占位符,强制占位,不够补0。$"{123:0000.00}" //0123.00 |
. |
小数点, |
, |
千分位, |
, |
倍数符号,也是逗号,在末尾、小数点前为倍数符号,除以1000,可多个。$"{12000:#,}" //12 |
% |
百分数,乘一百+%,$"{0.2:00.00%}" //20.00% |
e/e |
指数(科学计数),$"{10.1234:0.00e0}" //1.01e1;$"{0.01234:0.00e0}" //1.23e-2 |
\\ |
转义字符, |
📢热知识:小数格式化截断时都会四舍五入,(int)double 强转换是直接截断整数部分,相当于向下取整。
🔊冷知识:土耳其文化中的小数点为“逗号”,而非“点”。
4.2、日期时间格式
| 🔸日期格式-自定义 | 说明(datetime 和 datetimeoffset) |
|---|---|
| yyyy | 年份,yyyy //2024,yy //24 |
| mm | 2位数的月份,1个m就不会补0 了,3/4个m为月份名称。m //4,mm //04,mmm //4月,mmmm //四月 |
| dd | 2位数的日,3/4个d为星期。d //8,dd //08,ddd //周一,dddd //星期一 |
| hh | 2位数的小时(24小时制) |
| hh | 2位数的小时(12小时制) |
| mm | 2位数的分钟 |
| ss | 2位数的秒 |
| f | 为1/10秒单位,ff为1/100秒单位,以此类推,fff就表示毫秒 |
| tt | am/pm 指示符 |
| 组合使用 | 以上可组合使用,可穿插任意字符,$"{datetime.now:yyyy年mm月dd日 hh:mm:ss}" |
| 🔸日期格式-简写 | 说明 |
| d、d | d长日期,d短日期,$"{datetime.now:d}" //2024年1月18日 |
| f、f | 完整日期/时间模式,f长时间,f短时间,$"{datetime.now:f}" //2024年1月18日 22:45:34 |
| t、t | t长时间,t短时间,$"{datetime.now:t}" //22:45:42 |
| m/m | 月日模式,$"{datetime.now:m}" //1月18日 |
| y/y | 年月模式,$"{datetime.now:y}" //2024年1月 |
4.3、其他格式
| 🔸枚举格式 | 说明 |
|---|---|
| g/g,f/f | 枚举的字符串名称,其中f用于flags,$"{utype.user:g}" //user |
| d/d | 十进制枚举值,$"{utype.user:d}" //2 |
| x/x | 十六进制枚举值,$"{utype.user:x}" //00000002 |
| 🔸其他 | 说明 |
| iformattable | 自定义的格式化接口,使用自定义的 iformatprovider 来实现格式化输出tostring() |
| numberstyles | 用于解析数字符串(parse)时指定的解析格式 |
| datetimestyles | 同上,用于时间日期的解析 |
🚩格式msdn参考资料:
- 所有整型和浮点类型。 (请参阅 标准数字格式字符串 和 自定义数值格式字符串。)
- datetime 和 datetimeoffset。 (请参阅 标准日期和时间格式字符串 和 自定义日期和时间格式字符串。)
- 所有枚举类型。 (请参阅 枚举格式字符串.)
- timespan 值。 (请参阅 标准 timespan 格式字符串 和 自定义 timespan 格式字符串。)
- guid。 (请参阅 guid.tostring(string) 方法。)
06、高性能字符串实践
提高string处理性能的核心就是:尽量减少临时字符串对象的创建。
- 高频常用字符串(非字面量)可考虑主动驻留字符串,
string.intern(name)。 - 字符串的比较、查找,优先用span,或者尽量使用无文化语义的比较
stringcomparison.ordinal。 - 大量字符串连接使用stringbuilder,且尽量给定一个合适的容量大小,避免频繁的扩容。
- 少量字符串连接用字符串插值即可,创建stringbuilder也是有成本的。
- 如果有大量stringbuilder 的使用,可以考虑用stringbuildercache,或池化stringbuilder。
6.1、比较字符串
- 字符串查找、拆分字符串、解析字符串,推荐使用span,参考《高性能的span、memory》。
- 查找、比较字符串,尽量指定
stringcomparison为ordinal或ordinalignorecase,采用无文化特征的比较性能更快。
string str1="a",str2 = "b";
//这种方式会产生新的字符串,不推荐
if(str1.tolower() == str2.tolower()){}
//推荐写法
if(string.compare(str1, str2, true)==0){}
if(string.equals(str1,str2,stringcomparison.ordinal)){}

6.2、字符串真的不能修改吗?
字符串其实也是可以修改的,当然是用非常规手段。
- 用
ref获取指定字符的引用地址(指针地址)。
static void main(string[] args)
{
var str1 = "hello";
var str2 = "hello";
//修改第0位
ref var c1 = ref memorymarshal.getreference<char>(str1);
c1 = 'h';
//修改第一位
ref var c2 = ref memorymarshal.getreference<char>(str1.asspan(1));
c2 = 'e';
console.writeline(str1);//输出:hello
console.writeline(str2);//输出:hello
}
- 直接使用指针修改字符值。
void main()
{
var str1 = "hello";
var str2 = "hello";
unsafe
{
fixed (char* c = str2)
{
c[0] = 'h';
c[1] = 'e';
}
}
console.writeline(str1); //hello
console.writeline(str2); //hello
}
参考资料
- c# 文档
- 《c#8.0 in a nutshell》
- .net面试题解析(03)-string与字符串操作
- .net 中的字符编码
©️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀
发表评论