C#.Net筑基-String字符串超全总结 [深度好文]
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222717613-1706548302.png" alt="image.png" loading="lazy"></p><p>字符串是日常编码中最常用的引用类型了,可能没有之一,加上字符串的不可变性、驻留性,很容易产生性能问题,因此必须全面了解一下。</p>
<hr>
<h1 id="01字符与字符编码">01、字符与字符编码</h1>
<h2 id="11字符char">1.1、字符Char</h2>
<p>字符 char 表示为 Unicode字符,在C#中用 UTF-16 编码表示,占用2个字节(16位)大小,字面量用单引号<code>''</code>包裹。</p>
<pre><code class="language-csharp">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')); //获取字符分类
</code></pre>
<ul>
<li>char 是值类型(结构体),以16位整数形式存储,<code>char</code>可隐式转换为<code>int</code>。</li>
<li>字符串可以看做是<code>char</code>序列(数组),字符串是引用类型。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222718157-879102695.png" alt="image.png" loading="lazy"></p>
<pre><code class="language-csharp">string str = "Hello World";
Console.WriteLine(str);//H
Console.WriteLine(str); //d
Console.WriteLine(str.GetType().Name); //Char
</code></pre>
<h2 id="12字符集unicode与字符编码">1.2、字符集Unicode与字符编码</h2>
<p>一般情况下字符串长度<code>string.Length</code> 就是可见的文本字符数量,但这并不绝对相等。大多数字符都是一个char组成,然而有些字符无法用一个char表示,如表情、不常用字符等,他们会用两个char(4个字节)来表示。</p>
<pre><code class="language-csharp">"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()
</code></pre>
<p><strong>Unicode</strong> 是国际标准、通用字符集,涵盖了世界上几乎所有的文字、符号,可以满足跨平台、跨语言的文本信息编码。Unicode 有100W+个字符地址空间,地址范围是 0x0000 - 0x10FFFF,每个字符都有自己的编码,目前已分配了大约10W+个。通常使用“U+”后跟一个十六进制数来表示,例如字母<code>A</code>的Unicode码点是<code>U+0041</code>。</p>
<p>Unicode 字符集中包含多个分类(平面):其中最常用的就是基本平面,大部分常用字符都在这里面。</p>
<ul>
<li>🔸<strong>基本多文种平面(BMP,Basic Multilingual Plane)</strong>:Unicode 的BMP区域几乎包含了所有常用的字符,如几十种主流语言,及30000+的汉字,BMP区域的字符都只需要1个<code>char</code>(2个字节)表示。</li>
<li>🔸<strong>辅助平面(SMP)</strong>:包含其他不常使用的字符,如一些历史文字、音乐符号、数学符号和表情符号等。该区域大多用两个<code>char</code>(4个字节)表示一个符号。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222717658-31958774.png" alt="image.png" loading="lazy"></p>
<p>Unicode 是一种字符集,而实际在计算机上存储时需要用一个确定的编码方案,常见的就是UTF-8、UTF-16、UTF32。</p>
<ul>
<li><strong>UTF-16</strong>:2个字节表示BMP中的字符,其他字符会需要4个字节,C#、Java语言内部就是使用的UTF-16来表示的字符串。</li>
<li><strong>UTF-8</strong>:变长编码,使用1到4个字节来表示一个Unicode字符,在互联网使用广泛。特别是存储 ASCII 为主的内容时,变长编码可以显著节约存储空间。</li>
</ul>
<blockquote>
<p>📢ASCII 字符集只包含 128个 基础字符,涵盖键盘上的字母、数字、常用符号。Unicode 是包含 ASCII字符集的,最前面128 个字符就是。在UTF-8编码中 ASCII字符只需要1个字节。</p>
</blockquote>
<hr>
<h1 id="02string基础">02、String基础</h1>
<p>字符串 string 是一个不可变(不可修改)的字符序列(数组),为引用类型,字面量用双引号<code>""</code>包裹。</p>
<pre><code class="language-csharp">string s1 = "sam";
string s2 = new string('1',5);//11111
Console.WriteLine(s2); //像数组一样操作字符串中的字符
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
</code></pre>
<ul>
<li>字符串是引用类型,因此可以用<code>null</code>表示,不过一般空字符建议用<code>string.Empty</code>(或<code>""</code>)表示。</li>
<li>字符串可以当做 字符数组一样操作,只是不能修改。</li>
<li>字符串的相等为<strong>值比较</strong>,只要字符序列相同即可。例外情况请是如果用<code>object</code> 做<code>==</code>比较,只会比较引用地址。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222718176-1420917591.png" alt="image.png" loading="lazy"></p>
<blockquote>
<p>🚩 字符串在存储、转换为字节码时需指定编码,一般默认为 UTF-8,这是广泛使用的编码类型,更节省空间。</p>
</blockquote>
<h2 id="21字符串常用api">2.1、字符串常用API</h2>
<table>
<thead>
<tr>
<th><strong>属性</strong></th>
<th><strong>特点/说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Length</td>
<td>字符串中字符数量</td>
</tr>
<tr>
<td>索引器</td>
<td>索引器,用索引获取字符,不可修改</td>
</tr>
<tr>
<td><strong>🔸方法</strong></td>
<td><strong>特点/说明</strong></td>
</tr>
<tr>
<td>StartsWith、EndsWith(String)</td>
<td>判断开头、结尾是否匹配,<code>"Hello".StartsWith("He")</code></td>
</tr>
<tr>
<td>Equals(String)</td>
<td>比较字符串是否相同</td>
</tr>
<tr>
<td>IndexOf()</td>
<td>查找指定字符(串)的索引位置,从后往前查找 LastIndexOf</td>
</tr>
<tr>
<td>Insert(Int32, String)</td>
<td>指定位置插入字符串,‼️返回新字符串!</td>
</tr>
<tr>
<td>PadLeft(Int32)</td>
<td>指定字符宽度(数量)对齐,左侧填充,‼️返回新字符串!右侧填充 PadRight(Int32)</td>
</tr>
<tr>
<td>Remove(Int32, Int32)</td>
<td>删除指定位置、长度的字符,‼️返回新字符串!</td>
</tr>
<tr>
<td><strong>Replace</strong>(String, String)</td>
<td>替换指定内容的字符(串),‼️返回新字符串!</td>
</tr>
<tr>
<td><strong>Substring</strong>(Int32, Int32)</td>
<td>截取指定位置、长度的字符串,‼️返回新字符串!</td>
</tr>
<tr>
<td>ToLower()、ToUpper()</td>
<td>返回小写、大写形式的字符串,‼️返回新字符串!</td>
</tr>
<tr>
<td><strong>Trim</strong>()</td>
<td>裁剪掉前后空格,‼️返回新字符串!有多个配套方法 TrimEnd、TrimStart</td>
</tr>
<tr>
<td>Split(char)</td>
<td>按分隔符分割字符串为多个子串,比较常用,不过性能不好,建议用Span代替。</td>
</tr>
<tr>
<td><strong>🔸静态方法</strong></td>
<td><strong>特点/说明</strong></td>
</tr>
<tr>
<td><strong>Empty</strong></td>
<td>获取一个空字符串(同<code>""</code>)</td>
</tr>
<tr>
<td>Compare(String, String)</td>
<td>比较两个字符串,有很多重载,返回一个整数,0表示相同。</td>
</tr>
<tr>
<td><strong>Concat</strong> (params string?[])</td>
<td>连接多个字符串,返回一个新的字符串,有很多重载,是比较基础的字符串连接函数。</td>
</tr>
<tr>
<td>Equals(str, StringComparison)</td>
<td>比较字符串是否相同,可指定比较规则 StringComparison</td>
</tr>
<tr>
<td><strong>Format</strong>(String, Object[])</td>
<td>字符串格式化,远古时期常用的字符串格式化方式,现在多实用$插值</td>
</tr>
<tr>
<td>string Intern(String)</td>
<td>获取“内部”字符串,先检查<strong>字符串池</strong>中是否存在,有则返回其引用,没有则添加并返回</td>
</tr>
<tr>
<td>string? IsInterned(String)</td>
<td>判断是否在<strong>字符串池</strong>中,存在则返回其引用,没有则返回<code>null</code></td>
</tr>
<tr>
<td><strong>IsNullOrEmpty</strong>(String)</td>
<td>判断指定的字符串是否 <code>null</code> 、空字符<code>""</code>/<code>String.Empty</code>,返回bool</td>
</tr>
<tr>
<td><strong>IsNullOrWhiteSpace</strong>(String)</td>
<td>判断指定的字符串是否 <code>null</code> 、空字符<code>""</code>/<code>String.Empty</code>、空格字符,返回bool</td>
</tr>
<tr>
<td><strong>Join</strong>(Char, String[])</td>
<td>用分隔符连接一个数组为一个字符串</td>
</tr>
</tbody>
</table>
<h2 id="22字符串的不变性驻留性">2.2、字符串的不变性、驻留性</h2>
<p>字符串是一种有一点点特别的引用类型,因为其不变性,所以在参数传递时有点像值类型。</p>
<ul>
<li><strong>🔸不变性:</strong>字符串一经创建,值不可变。对字符串的各种修改操作都会创建新的字符串对象,这一点要非常重视,应尽量避免,较少不必要的内存开销。</li>
<li><strong>🔸驻留性</strong>:运行时将字符串值存储在“驻留池(字符串池)”中,相同值的字符串都复用同一地址。</li>
</ul>
<p>不变性、驻留性 是<code>.Net</code>对string 的性能优化,提升字符串的处理性能。如下示例中,s1、s2字符串是同一个引用。</p>
<pre><code class="language-csharp">string s1 = "hello";
string s2 = "hello";
Console.WriteLine(s1 == s2); //True
Console.WriteLine(s1.Equals(s2)); //True
Console.WriteLine(Object.ReferenceEquals(s1,s2)); //True
</code></pre>
<p>当然不是所有字符串都会驻留,那样驻留池不就撑爆了吗!一般只有两种情况下字符串会被驻留:</p>
<ul>
<li>字面量的字符串,这在编译阶段就能确定的“字符串常量值”。相同值的字符串只会分配一次,后面的就会复用同一引用。</li>
<li>通过 <code>string.Intern(string)</code> 方法主动添加驻留池。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222717642-1619359457.png" alt="image.png" loading="lazy"></p>
<pre><code class="language-csharp">string st1 = "123" + "abc";
string st2 = "123abc";
string st3 = st2.Substring(0,3);
</code></pre>
<p>看看上面代码生成的IL代码:</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222718177-527927078.png" alt="image.png" loading="lazy"></p>
<ul>
<li>常量的字符串<code>"123" + "abc"</code>连接被编译器优化了。</li>
<li>常量字符串使用指令“ldstr”加载的到栈,该指令会先查看驻留池中是否已存在,如果已存在则直接返回已有字符串对象的地址,否则就加入。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222717635-1585290077.png" alt="image.png" loading="lazy"></p>
<p>驻留的字符串(字符串池)在托管堆上存储,大家共享,内部其实是一个哈希表,存储被驻留的字符串和其内存地址。驻留池生命周期同进程,并不受GC管理,因此无法被回收。因此需要注意:</p>
<ul>
<li><code>lock</code>锁不能用string,避免使用同一个锁(字符串引用)。</li>
<li>避免创建字面量的大字符串,会常住内存无法释放,当然也不要滥用<code>string.Intern(string)</code> 方法。</li>
</ul>
<h2 id="23字符串的查找比较">2.3、字符串的查找、比较</h2>
<p>string 的 比较字符串 是默认包含文化和区分大小写的顺序比较,C#内置的一个字符串比较规则(枚举)StringComparison,可设置比较规则。在很多内置方法中使用,包括 String.Equals、String.Compare、String.IndexOf 和 String.StartsWith等。</p>
<blockquote>
<p>📢 微软官方建议在使用上述字符串比较方法中明确指定 StringComparison 参数值,而不是默认的比较规则。</p>
</blockquote>
<pre><code class="language-csharp">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
}
</code></pre>
<table>
<thead>
<tr>
<th><strong>枚举值</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>CurrentCulture</td>
<td>本地语言区域规则,适用于给用户显示的内容</td>
</tr>
<tr>
<td>CurrentCultureIgnoreCase</td>
<td>同上+忽略大小写</td>
</tr>
<tr>
<td>InvariantCulture</td>
<td>固定语言区域,适用于存储的数据</td>
</tr>
<tr>
<td>InvariantCultureIgnoreCase</td>
<td>同上+忽略大小写</td>
</tr>
<tr>
<td>Ordinal</td>
<td>二进制值顺序比较字符串,比较快⚡</td>
</tr>
<tr>
<td>OrdinalIgnoreCase</td>
<td>同上+忽略大小写</td>
</tr>
</tbody>
</table>
<p>如果单纯从性能角度考虑,考虑语言文化的字符串比较其实比较慢,来测试对比一下。测试代码:</p>
<pre><code class="language-csharp">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
</code></pre>
<ul>
<li>上面7个方法 分别测试了<code>Equals</code>的默认版本、及带参 StringComparison 的不同比较规则的性能。</li>
<li>最后加了一个使用<code>Span</code> 的相等比较,更多关于Span的资料查看《高性能的Span、Memory》。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222718228-2103611779.png" alt="image.png" loading="lazy"></p>
<p><strong>🚩测结结论</strong>:</p>
<ul>
<li><code>Span</code>最快,其次无参<code>Equals()</code>版本、<code>Ordinal</code>,他们都是只比较二进制值,不考虑文化信息。</li>
<li>个人理解,如果不考虑一些比较特别的语言(如瑞典语、土耳其语、 阿塞拜疆语等),只是针对英文、中文的字符串,一般不用考虑文化语义。</li>
<li><code>Equals()</code>默认是不考虑文化语义的字符值比较,但有些比较方法就不一定能了,比如<code>StartsWith</code>、<code>Compare</code> 默认的是带文化语义的<code>CurrentCulture</code>规则,因此推荐主动配置 StringComparison 参数。</li>
</ul>
<h2 id="24字符串转义">2.4、字符串转义\</h2>
<p>转义字符:反斜杠“\”</p>
<table>
<thead>
<tr>
<th><strong>转义序列</strong></th>
<th><strong>字符名称</strong></th>
<th><strong>Unicode 编码</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>\'</td>
<td>单引号</td>
<td>0x0027</td>
</tr>
<tr>
<td>\"</td>
<td>双引号</td>
<td>0x0022</td>
</tr>
<tr>
<td>\0</td>
<td>null</td>
<td>0x0000</td>
</tr>
<tr>
<td>\b</td>
<td>Backspace</td>
<td>0x0008</td>
</tr>
<tr>
<td>\f</td>
<td>换页</td>
<td>0x000C</td>
</tr>
<tr>
<td>\n</td>
<td>换行</td>
<td>0x000A</td>
</tr>
<tr>
<td>\r</td>
<td>回车</td>
<td>0x000D</td>
</tr>
<tr>
<td>\t</td>
<td>水平制表符</td>
<td>0x0009</td>
</tr>
</tbody>
</table>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222717633-410867785.png" alt="image.png" loading="lazy"></p>
<hr>
<h1 id="03字符串连接的8种方式">03、🚩字符串连接的8种方式</h1>
<p>字符串连接(组装)的使用是非常频繁的,.Net中提供了多种姿势来实现,各有特点。</p>
<table>
<thead>
<tr>
<th><strong>连接方法</strong></th>
<th><strong>示例/说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>直接相加</td>
<td><code>"hello"+str</code>,其实编译后为 <code>string.Concat ("hello", str)</code></td>
</tr>
<tr>
<td>连接函数:String.Concat()</td>
<td>字符串相加一般就是被编译为调用<code>String.Concat()</code>方法,有很多重载,支持任意多个参数</td>
</tr>
<tr>
<td>集合连接函数:String.Join()</td>
<td>将(集合)参数连接为一个字符串,<code>string.Join('-',1,2,3); //1-2-3</code></td>
</tr>
<tr>
<td>格式化:String.Format()</td>
<td>传统的字符串格式化手艺,<code>string.Format("name:{0},age:{1}",str,18)</code></td>
</tr>
<tr>
<td>$ 字符串插值</td>
<td>用花括号<code>{var}</code>引用变量、表达式,强大、方便,<code>$"Hello {name} !"</code></td>
</tr>
<tr>
<td><code>@</code>逐字文本字面量</td>
<td>支持转义符号、换行符,常用于文件路径、多行字符:<code>@$"C:\\Users\\{name}\\Downloads"</code></td>
</tr>
<tr>
<td><code>"""</code>原始字符串字面量</td>
<td>C# 11,三个双冒号包围,支持多行文本的原始字面量。</td>
</tr>
<tr>
<td><code>StringBuilder</code></td>
<td>当处理大量字符串连接操作时,推荐使用<code>StringBuilder</code>,效果更优。</td>
</tr>
</tbody>
</table>
<p>字面量字符串的相加会被编译器优化,直接合并为一个字符串。</p>
<pre><code class="language-csharp">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 (), "月");
</code></pre>
<h2 id="31字符串格式化-stringformat">3.1、字符串格式化 String.Format</h2>
<p>String.Format 方法是早期比较常用的字符串组织方式,后来<code>$</code>字符串插值 问世后就逐步被打入冷宫了。</p>
<pre><code class="language-csharp">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。
</code></pre>
<p>基本语法规则就是用 <code>{index}</code>来占位,在后面的参数中给出值。</p>
<ul>
<li>索引位置从0开始,必须连续递增,可以重复。</li>
<li>索引的位置对应后面参数的顺序位置,必须对应,参数不能少(抛出异常),可以多。</li>
<li>字符串格式规则参考后文《字符串格式总结》。</li>
</ul>
<h2 id="32字符串插值">3.2、$字符串插值</h2>
<p>字符串插值的格式:<code>$"{<interpolationExpression>}"</code>,大括号中可以是一个变量,一个(简单)表达式语句,还支持设置格式。功能强大、使用方便,老人孩子都爱用!</p>
<ul>
<li><code>{}</code>字符转义,用两个<code>{{}}</code>即可,如果只有一边,则用单引号<code>'{{'</code>,即输出为<code>{</code>。</li>
<li>使用三元运算符<code>?</code>表达式,用括号包起来即可,因为“<code>:</code>”在插值字符串中有特殊含义,即格式化。</li>
<li>字符串格式规则参考后文《字符串格式总结》。</li>
</ul>
<pre><code class="language-csharp">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
</code></pre>
<h2 id="33字符串支持任意字符">3.3、@字符串支持任意字符</h2>
<p><code>@</code>标记的字符串为字面量字符串 ,不需要使用转义字符了,可搭配<code>$</code>字符串插值使用。文件路径地址都会用到<code>@</code>,两个冒号表示一个冒号,<code>@"a""b"</code> ==<code>a"b</code>。</p>
<pre><code class="language-csharp">var path= @"D:\GApp\LINQPad 8\x64";
var file = $@"D:\GApp\LINQPad 8\x64\{DateTime.Now:D}";
var maxText = @"Hi All:
第一行
换行
";
</code></pre>
<h2 id="34stringbuilder">3.4、👍🏻StringBuilder</h2>
<p>StringBuilder 字符串修理工程师,顾名思义,就是专门用来组装字符串的,可以看做是一个可变长字符集合。适用于把很多字符串组装到一起的场景,避免了大量临时字符串对象的创建,可显著提升性能。</p>
<pre><code class="language-csharp">var sb = new StringBuilder(100);
sb.Append("sam");
sb = 'F';//Fam
sb.AppendLine("age");
sb.Append("age").Append(Environment.NewLine); //效果同上
sb.Insert(2,"---");
sb.Replace("age","Age");
var result = sb.ToString(); //获取结果
</code></pre>
<table>
<thead>
<tr>
<th><strong>属性</strong></th>
<th><strong>特点/说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Capacity</td>
<td>获取、设置字符容量(实际占用内存),默认16,当内容增多容量不足时,会自动扩容。</td>
</tr>
<tr>
<td>MaxCapacity</td>
<td>获取最大容量,20亿字符</td>
</tr>
<tr>
<td>Length</td>
<td>实际字符内容的长度,可赋值,设置<code>0</code>则清空已有字符内容,但并不影响 Capacity。</td>
</tr>
<tr>
<td>Chars</td>
<td>索引器,可获取、设置字符</td>
</tr>
<tr>
<td><strong>🔸方法</strong></td>
<td><strong>特点/说明</strong></td>
</tr>
<tr>
<td>StringBuilder(Int32)</td>
<td>构造函数,参数指定初始容量capacity</td>
</tr>
<tr>
<td>Append(value)</td>
<td>追加字符,很多重载版本,类似还有AppendFormat、AppendJoin</td>
</tr>
<tr>
<td>AppendLine</td>
<td>追加字符后,再追加一个换行符</td>
</tr>
<tr>
<td>Insert (int index, value)</td>
<td>指定位置插入字符内容</td>
</tr>
<tr>
<td>Replace(Char, Char)</td>
<td>查找替换字符(字符串)内容,会替换所有找到的字符内容</td>
</tr>
<tr>
<td>ToString()</td>
<td>将 StringBuilder 输出为一个字符串,一般是<code>StringBuilder</code>的命运终点。</td>
</tr>
</tbody>
</table>
<ul>
<li>各种<code>Append</code>方法都返回自身,可用来链式编程。</li>
<li><code>StringBuilder</code> 默认容量为16,内部有一个<code>char</code>数组<code>m_ChunkChars</code>(缓冲区)来存储字符内容,如下<code>StringBuilder</code>构造函数源码:</li>
</ul>
<pre><code class="language-csharp">public StringBuilder()
{
m_MaxCapacity = int.MaxValue;
m_ChunkChars = new char;
}
</code></pre>
<ul>
<li>当不断追加字符串,容量不足会自动扩容,扩容的过程其实就是创建更大的字符数组(容量翻倍),把原来的值拷贝过来,这个过程会涉及数组对象创建、内存拷贝。</li>
</ul>
<blockquote>
<p>📢 一般使用<code>StringBuilder</code> 建议尽量给一个合理的默认容量大小,尽量避免、减少频繁的扩容。</p>
</blockquote>
<hr>
<h1 id="04字符串格式化大全">04、🚩字符串格式化大全</h1>
<blockquote>
<p>📢字符串格式语法:<code>{index/interpolationExpression [,alignment][:formatString]}</code></p>
</blockquote>
<ul>
<li><code>,alignment</code>可选,设置字符串的对齐长度,如果位数不够则空格补齐,正数部补左边,负数补右边。</li>
<li><code>:formatString</code>指定格式规则。一次只能指定一个格式规则,可和<code>,alignment</code>共存。</li>
</ul>
<pre><code class="language-csharp">//,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
</code></pre>
<h2 id="41数值格式"><strong>4.1、</strong>数值格式</h2>
<p><strong>🚩标准数值格式</strong>:</p>
<table>
<thead>
<tr>
<th><strong>🔸数值格式</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>E3/e3</td>
<td>科学计数法(指数),数字"3"为小数精度,<code>$"{12345.2:E3}"</code>//1.235E+004,<code>E+4</code>表示10的4次方;如果是<code>E-4</code>则表示为小数(除以10的四次方) <code>1E-4 = 0.0001</code></td>
</tr>
<tr>
<td>F4</td>
<td>定点格式,小数精度为"4",位数不够后面补0,支持所有数值类型,<code>$"{123.22F:F4}"</code>//123.2200</td>
</tr>
<tr>
<td>G4</td>
<td>定点格式<code>F</code>+指数<code>E</code>的结合版,最多"4"个有效数字,超过就用科学计数法。<code>"{123:G2}"</code> //1.2E+02,<code>$"{123:G4}"</code> //123</td>
</tr>
<tr>
<td>C3</td>
<td>货币格式(支持千分位),数字“3”为小数位数,<code>$"{123.346:C2}"</code> //¥123.35</td>
</tr>
<tr>
<td>P2</td>
<td>百分比格式,数字乘以100后转换为百分数,数字“2”为小数位数,<code>$"{0.2:P2}"</code> //20.00%</td>
</tr>
<tr>
<td>N6</td>
<td>数字格式化(支持千分位),小数位数为6,不够后面补0,<code>$"{123:N6}" //123.000000</code></td>
</tr>
<tr>
<td>D6</td>
<td>整数定长格式,不够前面补0,只支持整数,<code>$"{123:D6}" //000123</code></td>
</tr>
<tr>
<td>B</td>
<td>输出为二进制格式,仅支持整数+.Net8,精度为字符串位数,不够补0,<code>$"{123:B}" //1111011</code></td>
</tr>
<tr>
<td>X/x</td>
<td>输出为十六进制格式,仅支持整数+,精度为字符串位数,不够补0,<code>$"{12:X4}"</code> //000C</td>
</tr>
</tbody>
</table>
<p><strong>🚩自定义的数值格式</strong>:</p>
<table>
<thead>
<tr>
<th><strong>🔸数值格式符号</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#</code></td>
<td>数字占位符,不强制占位,<code>$"{123:#,###.##}"</code> //123</td>
</tr>
<tr>
<td><code>0</code></td>
<td>数字(0)占位符,强制占位,不够补0。<code>$"{123:0000.00}"</code> //0123.00</td>
</tr>
<tr>
<td><code>.</code></td>
<td>小数点,</td>
</tr>
<tr>
<td><code>,</code></td>
<td>千分位,</td>
</tr>
<tr>
<td><code>,</code></td>
<td>倍数符号,也是逗号,在末尾、小数点前为倍数符号,除以1000,可多个。<code>$"{12000:#,}"</code> //12</td>
</tr>
<tr>
<td><code>%</code></td>
<td>百分数,乘一百+%,<code>$"{0.2:00.00%}"</code> //20.00%</td>
</tr>
<tr>
<td><code>E</code>/<code>e</code></td>
<td>指数(科学计数),<code>$"{10.1234:0.00e0}"</code> //1.01e1;<code>$"{0.01234:0.00e0}"</code> //1.23e-2</td>
</tr>
<tr>
<td><code>\\</code></td>
<td>转义字符,</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>📢热知识</strong>:小数格式化截断时都会四舍五入,(int)double 强转换是直接截断整数部分,相当于向下取整。<br>
<strong>🔊冷知识</strong>:土耳其文化中的小数点为“逗号”,而非“点”。</p>
</blockquote>
<h2 id="42日期时间格式">4.2、日期时间格式</h2>
<table>
<thead>
<tr>
<th><strong>🔸日期格式-自定义</strong></th>
<th><strong>说明(</strong><strong>DateTime</strong> 和 <strong>DateTimeOffset</strong><strong>)</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>yyyy</td>
<td>年份,<code>yyyy</code> //2024,<code>yy</code> //24</td>
</tr>
<tr>
<td>MM</td>
<td>2位数的月份,1个M就不会补0 了,3/4个M为月份名称。<code>M</code> //4,<code>MM</code> //04,<code>MMM</code> //4月,<code>MMMM</code> //四月</td>
</tr>
<tr>
<td>dd</td>
<td>2位数的日,3/4个d为星期。<code>d</code> //8,<code>dd</code> //08,<code>ddd</code> //周一,<code>dddd</code> //星期一</td>
</tr>
<tr>
<td>HH</td>
<td>2位数的小时(24小时制)</td>
</tr>
<tr>
<td>hh</td>
<td>2位数的小时(12小时制)</td>
</tr>
<tr>
<td>mm</td>
<td>2位数的分钟</td>
</tr>
<tr>
<td>ss</td>
<td>2位数的秒</td>
</tr>
<tr>
<td>f</td>
<td>为1/10秒单位,<code>ff</code>为1/100秒单位,以此类推,<code>fff</code>就表示毫秒</td>
</tr>
<tr>
<td>tt</td>
<td>AM/PM 指示符</td>
</tr>
<tr>
<td>组合使用</td>
<td>以上可组合使用,可穿插任意字符,<code>$"{DateTime.Now:yyyy年MM月dd日 HH:mm:ss}"</code></td>
</tr>
<tr>
<td><strong>🔸日期格式-简写</strong></td>
<td><strong>说明</strong></td>
</tr>
<tr>
<td>D、d</td>
<td><code>D</code>长日期,<code>d</code>短日期,<code>$"{DateTime.Now:D}"</code> //2024年1月18日</td>
</tr>
<tr>
<td>F、f</td>
<td>完整日期/时间模式,<code>F</code>长时间,<code>f</code>短时间,<code>$"{DateTime.Now:F}"</code> //2024年1月18日 22:45:34</td>
</tr>
<tr>
<td>T、t</td>
<td><code>T</code>长时间,<code>t</code>短时间,<code>$"{DateTime.Now:T}"</code> //22:45:42</td>
</tr>
<tr>
<td>M/m</td>
<td>月日模式,<code>$"{DateTime.Now:M}"</code> //1月18日</td>
</tr>
<tr>
<td>Y/y</td>
<td>年月模式,<code>$"{DateTime.Now:Y}"</code> //2024年1月</td>
</tr>
</tbody>
</table>
<h2 id="43其他格式">4.3、其他格式</h2>
<table>
<thead>
<tr>
<th><strong>🔸枚举格式</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>G/g,F/f</td>
<td>枚举的字符串名称,其中F用于Flags,<code>$"{UType.User:G}"</code> //User</td>
</tr>
<tr>
<td>D/d</td>
<td>十进制枚举值,<code>$"{UType.User:D}"</code> //2</td>
</tr>
<tr>
<td>X/x</td>
<td>十六进制枚举值,<code>$"{UType.User:X}"</code> //00000002</td>
</tr>
<tr>
<td><strong>🔸其他</strong></td>
<td><strong>说明</strong></td>
</tr>
<tr>
<td>IFormattable</td>
<td>自定义的格式化接口,使用自定义的 IFormatProvider 来实现格式化输出ToString()</td>
</tr>
<tr>
<td>NumberStyles</td>
<td>用于解析数字符串(Parse)时指定的解析格式</td>
</tr>
<tr>
<td>DateTimeStyles</td>
<td>同上,用于时间日期的解析</td>
</tr>
</tbody>
</table>
<hr>
<p><strong>🚩格式MSDN参考资料</strong>:</p>
<ul>
<li>所有整型和浮点类型。 (请参阅 标准数字格式字符串 和 自定义数值格式字符串。)</li>
<li>DateTime 和 DateTimeOffset。 (请参阅 标准日期和时间格式字符串 和 自定义日期和时间格式字符串。)</li>
<li>所有枚举类型。 (请参阅 枚举格式字符串.)</li>
<li>TimeSpan 值。 (请参阅 标准 TimeSpan 格式字符串 和 自定义 TimeSpan 格式字符串。)</li>
<li>GUID。 (请参阅 Guid.ToString(String) 方法。)</li>
</ul>
<hr>
<h1 id="06高性能字符串实践">06、高性能字符串实践</h1>
<p>提高string处理性能的核心就是:<strong>尽量减少临时字符串对象的创建</strong>。</p>
<ul>
<li>高频常用字符串(非字面量)可考虑主动驻留字符串,<code>string.Intern(name)</code>。</li>
<li>字符串的比较、查找,优先用Span,或者尽量使用无文化语义的比较<code>StringComparison.Ordinal</code>。</li>
<li>大量字符串连接使用StringBuilder,且尽量给定一个合适的容量大小,避免频繁的扩容。</li>
<li>少量字符串连接用字符串插值即可,创建StringBuilder也是有成本的。</li>
<li>如果有大量StringBuilder 的使用,可以考虑用StringBuilderCache,或池化StringBuilder。</li>
</ul>
<h2 id="61比较字符串">6.1、比较字符串</h2>
<ul>
<li>字符串查找、拆分字符串、解析字符串,推荐使用Span,参考《高性能的Span、Memory》。</li>
<li>查找、比较字符串,尽量指定 <code>StringComparison</code> 为<code>Ordinal</code> 或 <code>OrdinalIgnoreCase</code>,采用无文化特征的比较性能更快。</li>
</ul>
<pre><code class="language-csharp">string str1="a",str2 = "b";
//这种方式会产生新的字符串,不推荐
if(str1.ToLower() == str2.ToLower()){}
//推荐写法
if(string.Compare(str1, str2, true)==0){}
if(string.Equals(str1,str2,StringComparison.Ordinal)){}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/151257/202405/151257-20240529222718226-2127316226.png" alt="image.png" loading="lazy"></p>
<h2 id="62字符串真的不能修改吗">6.2、字符串真的不能修改吗?</h2>
<p>字符串其实也是可以修改的,当然是用非常规手段。</p>
<ul>
<li>用<code>ref</code>获取指定字符的引用地址(指针地址)。</li>
</ul>
<pre><code class="language-csharp">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
}
</code></pre>
<ul>
<li>直接使用指针修改字符值。</li>
</ul>
<pre><code class="language-csharp">void Main()
{
var str1 = "hello";
var str2 = "hello";
unsafe
{
fixed (char* c = str2)
{
c = 'H';
c = 'E';
}
}
Console.WriteLine(str1); //HEllo
Console.WriteLine(str2); //HEllo
}
</code></pre>
<hr>
<h1 id="参考资料">参考资料</h1>
<ul>
<li>C# 文档</li>
<li>《C#8.0 In a Nutshell》</li>
<li>.NET面试题解析(03)-string与字符串操作</li>
<li>.NET 中的字符编码</li>
</ul>
<hr>
<blockquote>
<p><strong>©️版权申明</strong>:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!<em>原文编辑地址-语雀</em></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/anding/p/18221262
頁:
[1]