文章目录
- string和StringBuilder的区别,两者性能的比较
- 1. 不可变性 vs 可变性
- 2. 性能
- 3. 使用场景
- 总结
- 注意点和建议
- 常见误区
- 深入提问
- 1.Immutable 性质
- 性能影响的本质
- 性能问题的触发场景
- 2.内存管理
- 内存分配的差异
- 1. string 的“快闪”式分配
- 2. StringBuilder 的“缓冲区”策略
- 对垃圾回收(GC)的影响
- 3.使用场景
- 必须使用 StringBuilder 的场景
- 1. 迭代次数不确定的循环拼接
- 2. 大规模的字符串转换或清理
- 3. 跨方法组装复杂对象
- 建议使用 string 的场景
- 1. 静态或少量的拼接(少于 4-5 次)
- 2. 字符串插值
- 4.线程安全性
- 多线程操作下的典型问题
- 模拟故障
- 如何正确处理?
- 5.性能基准测试
- 1. 核心工具:BenchmarkDotNet
- 2. 性能测量的关键指标
- 3. 其他辅助方法与工具
- 6.拼接操作
- 1. StringBuilder:最通用的方案
- 2. Span<char> 与 ValueStringBuilder:追求极致性能
- 3. String.Create:预先分配内存
- 4. 模式建议:串联与汇聚
- 5.字符串拼接决策流程
- 7.预分配容量
- 1. 为什么要设置初始容量?
- 2. 如何选择适当的容量?
- 经验
- 3. 代码示例
- 8.文化和语言
- 1. 理念差异:文化敏感性 vs 纯字节搬运
- 2. 实现差异:代理对(Surrogate Pairs)的处理
- 3. 内存布局:复合格式化的代价
- 9.异常处理
- 1. OutOfMemoryException (OOM):最常见的杀手
- 2. ArgumentOutOfRangeException:边界检查失败
- 3. ArgumentNullException
- 4. 安全处理与防御策略
- 策略 A:检查剩余空间
- 策略 B:预估容量而非盲目扩容
- 策略 C:处理国际化/非法字符的安全截断
- 专业词汇解释
string和StringBuilder的区别,两者性能的比较
在C#中,string和StringBuilder是处理字符串的两种主要方式。它们之间有几个重要的区别:
1. 不可变性 vs 可变性
string: string是不可变的,即一旦一个字符串对象被创建,其值不能被更改。任何对字符串的操作(例如连接、替换等)都会生成一个新的字符串对象,这会导致额外的内存分配和垃圾回收。
StringBuilder: StringBuilder是可变的。你可以在同一个对象上进行修改(如添加、插入等),而不需要创建新的对象。这使得StringBuilder在频繁修改字符串的场景中更高效。
2. 性能
string: 在频繁修改字符串的情况下,例如在循环中进行多个字符串连接,使用string会导致性能下降,因为每次连接都会产生新的字符串实例,增加垃圾回收的负担。
StringBuilder: 优化了字符串的修改性能,特别是在需要累积大量字符串时。它使用一个动态数组来存储字符,随着内容的增加容量可以自动扩展。这使得它在需要频繁连接和修改字符串的情况下性能更好。
3. 使用场景
使用string:
适合字符串内容较小、变化不频繁的场景,如读取配置文件、输出日志等。
使用StringBuilder:
适合需要在循环中或者频繁修改字符串的情况,如构建大型文本、生成动态HTML内容等。
// 使用 stringstringresult="";for(inti=0;i<1000;i++){result+=i.ToString();// 每次都创建新的 string 对象}// 使用 StringBuilderStringBuildersb=newStringBuilder();for(inti=0;i<1000;i++){sb.Append(i.ToString());// 在同一对象上修改}stringresultWithBuilder=sb.ToString();总结
对于较少修改的字符串,使用string可能更便利且易于理解。
对于频繁修改的字符串,StringBuilder提供更好的性能和更少的内存压力。
在性能敏感的应用中,选择合适的类型可以显著提高效率。对于大多数简单应用,string的可读性和便利性往往更加有吸引力。
注意点和建议
在回答“string和StringBuilder的区别”这个问题时,建议考虑以下几个方面,以展现出对C#字符串处理的深入理解:
基本概念:确保清楚地解释什么是string和StringBuilder,它们的用途以及各自的特点。string是不可变的,而StringBuilder是可变的,这一点是理解它们区别的基础。
内存分配:应提到内存分配的差异。string在每次修改后都会创建一个新的实例,而StringBuilder则在内部维护一个可变的字符数组,这将影响性能。
性能表现:在多次修改字符串时,比如在循环中拼接字符串,使用StringBuilder的性能优势更加明显。可以提供实际场景的例子或代码片段来展示性能比较,从而使论点更具说服力。
使用场景:强调何时该使用string,何时该使用StringBuilder,在处理小数量或少量操作时,使用string可能更加简单和可读,而在大量拼接操作时,应优先考虑StringBuilder。
常见误区
应避免将两个类混淆,尤其是在性能方面。即使是较小的字符串操作,如果频繁调用,也应优先考虑使用StringBuilder而不是string,这是一种常见的误区。
简洁性与可读性:在某些情况下,string的使用可能更能提高代码的可读性。应提到在权衡性能与代码简洁性时,需要找到合适的平衡。
举例支持:使用实际代码示例加以说明会让回答更加生动,尤其是在解释性能差异时,可以提供具体的测量结果。
总之,从基础知识到具体的应用场景,再到实际性能测量的结合,全面的理解及交流才能表现出的专业性。在回答时,结构清晰、逻辑严谨也是很重要的。
深入提问
1.Immutable 性质
提示:C# 中的字符串是不可变的,这对性能有什么影响?在什么场景下会导致性能问题?
在 C# 内存管理中,string 的不可变性(Immutability)是一个双刃剑。作为深耕 .NET 领域的开发者,你必须理解其底层开销。
性能影响的本质
当你修改一个字符串时,.NET 运行时(CLR)并不会在原有的内存地址上操作。
内存分配:系统会在托管堆上申请一块全新的内存空间。
数据拷贝:将原字符串的内容和新增加的内容完整地拷贝到新空间。
引用切换:将变量指向新的内存地址。
垃圾产生:原有的字符串对象变成孤立对象,等待 GC(垃圾回收)处理。
性能问题的触发场景
最典型的灾难场景是 循环拼接。
// 性能极差的代码示例stringreport="";for(inti=0;i<10000;i++){// 每次循环都会分配新内存,拷贝长度递增的数据// 时间复杂度接近 O(n^2)report+=$"Line{i}\n";}在这种情况下,性能问题体现为:
CPU 占用高:大量时间浪费在内存拷贝(Buffer.BlockCopy)上。
内存碎片:产生数以万计的临时字符串对象,导致 LOH(大对象堆) 碎片化或频繁触发 Gen 0 GC,造成系统卡顿(Stop-the-world)。
2.内存管理
提示:在使用 string 和 StringBuilder 时,如何影响垃圾回收?哪种方式会导致更多的内存分配?
在 C# 内存管理层面,string和StringBuilder的性能差异主要体现在**托管堆(Managed Heap)**的分配策略以及对GC(垃圾回收器)的压力上。
内存分配的差异
1. string 的“快闪”式分配
由于string的不可变性,每次拼接操作(如str1 + str2)都会经历以下物理过程:
- 计算长度:计算两个字符串的总长度。
- 新对象申请:在托管堆上申请对应大小的新内存。
- 物理拷贝:将
str1和str2的内容拷贝到新内存。 - 旧对象报废:旧的
str1对象如果不再被引用,就变成了“内存垃圾”。
如果你在循环中这样做,会产生大量短命的字符串对象,迅速填满Gen 0(第 0 代)区域。
2. StringBuilder 的“缓冲区”策略
StringBuilder内部维护着一个char[]数组。
- 原地修改:
Append操作是直接在数组里填充字符,不产生新对象。 - 倍增扩容:只有当现有的数组装不下时,它才会申请一个更大的数组并进行一次性拷贝。
对垃圾回收(GC)的影响
| 特性 | string (多次拼接) | StringBuilder |
|---|---|---|
| 内存分配频率 | 极高(每次拼接一次分配) | 极低(仅在扩容时分配) |
| GC 触发频率 | 容易触发 Gen 0 回收 | 几乎不触发 |
| LOH 风险 | 若拼接出超大字符串,易进入大对象堆 | 初始预设好 Capacity 可规避扩容开销 |
| 内存利用率 | 产生大量碎片 | 连续内存块,利用率高 |
核心结论:string会导致显著更多的内存分配。在高频操作下,GC 为了清理这些成千上万的临时字符串,会占用大量的 CPU 周期,甚至导致应用程序出现明显的“Stop-the-world”(全局暂停)现象。
3.使用场景
提示:在实际开发中,你会在什么情况下选择使用 StringBuilder 而不是字符串?请举例说明。
在实际的 C# 开发中,选择string还是StringBuilder并不是拍脑门决定的,而是基于可预测性和操作频率的权衡。
必须使用 StringBuilder 的场景
1. 迭代次数不确定的循环拼接
这是最经典的使用场景。当你从数据库、文件或 API 获取数据,并需要在循环中组装成一个大的字符串时,必须使用StringBuilder。
- 例子:生成一个报表、组装动态 SQL 的
IN子句、拼接大量的日志信息。 - 反例:在
foreach里使用str += item。
2. 大规模的字符串转换或清理
如果你需要对一段长文本进行多次复杂的替换、删除或插入操作,StringBuilder的效率远高于string.Replace。
- 例子:解析并重新格式化一个 10MB 的文本文件,移除所有的特殊字符并每行添加前缀。
3. 跨方法组装复杂对象
当一个字符串的构建逻辑非常复杂,需要拆分到多个方法中去实现时,传递StringBuilder实例比传递并返回多个string要高效得多。
建议使用 string 的场景
1. 静态或少量的拼接(少于 4-5 次)
如果拼接在单行代码内完成,编译器通常会将其优化为String.Concat,这种情况下string反而更快,因为不需要创建StringBuilder对象的开销。
- 例子:
string path = folder + "\\" + fileName + "." + ext;(这种直接写就行)。
2. 字符串插值
在代码可读性优先且拼接次数极少时,首选$"{var1} {var2}"。
// 场景:构建一个复杂的 SQL 查询publicstringBuildQuery(List<int>ids){// 情况 A:使用 StringBuilder (推荐)// 假设 ids 有 1000 个,这里只会有 1 次内存分配(如果指定了 Capacity)varsb=newStringBuilder(ids.Count*10);sb.Append("SELECT * FROM Users WHERE Id IN (");for(inti=0;i<ids.Count;i++){sb.Append(ids[i]);if(i<ids.Count-1)sb.Append(",");}sb.Append(")");returnsb.ToString();// 情况 B:使用 string (灾难)// 每次 += 都会导致内存拷贝,1000 个 ID 会产生 1000 个临时字符串对象// string sql = "SELECT ..."; sql += ids[i]...}4.线程安全性
提示:StringBuilder 是线程安全的吗?如果多线程对同一个 StringBuilder 实例进行操作,会出现什么问题?
直接给结论:StringBuilder不是线程安全的。
在软件工程中,StringBuilder的设计目标是追求单线程下的极致性能,因此它内部没有加任何锁(Lock)机制。如果你在多个线程中共享同一个StringBuilder实例并尝试修改它,结果是不可预测的。
多线程操作下的典型问题
当两个线程同时调用Append方法时,它们会竞争修改StringBuilder内部的缓冲区和索引:
- 数据覆盖/丢失: 线程 A 和线程 B 同时读取了当前的
Length(比如都是 10)。线程 A 写入了数据并将Length设为 11,紧接着线程 B 也会在索引 10 的位置写入数据,覆盖掉线程 A 的内容。 - 索引越界异常 (
IndexOutOfRangeException): 由于没有同步,内部的字符计数器可能会被错误地更新。一个线程可能认为缓冲区还有空间,而另一个线程刚刚填满了它,导致前一个线程尝试往非法内存地址写入数据。 - 字符串乱码/结构破坏:
StringBuilder内部采用分块(Chunks)存储。并发修改可能导致内部链表结构损坏,最终生成的字符串可能包含乱码,或者在调用ToString()时直接崩溃。
模拟故障
你可以运行这段代码来观察结果(通常最后输出的长度不会是 2000,且可能会报错):
varsb=newStringBuilder();vartasks=newList<Task>();for(inti=0;i<2;i++){tasks.Add(Task.Run(()=>{for(intj=0;j<1000;j++){sb.Append("A");// 两个线程同时抢夺这块内存}}));}awaitTask.WhenAll(tasks);Console.WriteLine($"预期长度: 2000, 实际长度:{sb.Length}");如何正确处理?
- 线程隔离(推荐):每个线程创建并使用自己的
StringBuilder实例。这是最安全、性能最高的方法。 - 显式加锁:如果必须共享,请使用
lock关键字。但这会抵消StringBuilder的性能优势。 - 使用 ThreadLocal:利用
ThreadLocal<StringBuilder>为每个线程维护一个重用的实例。
5.性能基准测试
提示:你如何测量 string 和 StringBuilder 的性能差异?可以使用哪些工具或方法?
在 C# 领域,测量性能差异绝不是靠手写Stopwatch或简单的DateTime.Now相减,因为这无法排除 JIT 编译优化、垃圾回收频率和热点代码预热的影响。
作为一名资深工程师,我会推荐使用BenchmarkDotNet,它是 .NET 社区测量性能的工业级标准工具。
1. 核心工具:BenchmarkDotNet
这个库能为你处理所有复杂的测试细节:自动进行代码预热、多次迭代取平均值、统计误差。更重要的是,它能通过[MemoryDiagnoser]直接告诉你代码分配了多少内存。
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem.Text;// 1. 加上内存诊断特性[MemoryDiagnoser]publicclassStringPerformanceBench{// 2. 设置参数,测试不同规模下的表现[Params(100,1000)]publicintIterations;[Benchmark]publicstringUseStringConcat(){stringresult=string.Empty;for(inti=0;i<Iterations;i++){result+="test";}returnresult;}[Benchmark]publicstringUseStringBuilder(){varsb=newStringBuilder();for(inti=0;i<Iterations;i++){sb.Append("test");}returnsb.ToString();}}// 启动入口// BenchmarkRunner.Run<StringPerformanceBench>();2. 性能测量的关键指标
运行测试后,你会得到一张表格,重点关注以下几列:
- Mean (均值):执行该操作所需的平均时间。你会发现,随着
Iterations增加,string的耗时会呈指数级增长。 - Gen 0 (第 0 代回收):每 1000 次操作触发的 GC 次数。
string拼接会频繁触发此处回收。 - Allocated (分配内存):最直观的数据。
string方案会显示分配了数 MB 甚至更多,而StringBuilder只会分配最终结果大小所需的内存。
3. 其他辅助方法与工具
- LINQPad:如果你不想建立完整的工程,LINQPad 是快速验证性能的利器。它内置了
Util.Benchmark或直接配合Stopwatch做简单对比。 - dotMemory / Visual Studio Profiler:当你需要查看内存中具体产生了哪些“垃圾”字符串对象时,使用这些内存分析器(Profiler)可以看到托管堆的快照。
6.拼接操作
提示:在进行大量字符串拼接时,有哪些替代的方法或设计模式可以考虑?
在 C# 领域,处理大量字符串拼接时,直接使用+或string.Concat是性能杀手。因为string对象在内存中是**不可变(Immutable)**的,每次拼接都会在堆上分配一个新对象,并把旧内容复制过去。
以下是专家级开发者常用的替代方案:
1. StringBuilder:最通用的方案
当你需要在一个循环中拼接几十次、甚至上千次字符串时,StringBuilder是首选。它内部维护一个缓冲区(Buffer),在拼接时直接修改内存,而不会产生大量的临时对象。
varsb=newStringBuilder();for(inti=0;i<1000;i++){sb.Append("Value: ").Append(i).Append("; ");}stringresult=sb.ToString();2. Span 与 ValueStringBuilder:追求极致性能
在对性能极其敏感的场景(如高频交易系统、底层解析器),即便是StringBuilder带来的堆分配也可能成为负担。此时可以利用 C# 7.2 引入的Span<T>,在栈上操作内存。
通过ValueStringBuilder(这是 .NET 源码内部常用的一种模式),可以实现零 GC(垃圾回收)分配。
// 伪代码:在栈上分配 256 字节Span<char>initialBuffer=stackallocchar[256];varvsb=newValueStringBuilder(initialBuffer);vsb.Append("High performance");// 如果超出 256 字节,它会自动申请堆内存,非常灵活3. String.Create:预先分配内存
如果你能提前算出结果字符串的总长度,string.Create是目前最快的方法。它直接分配目标长度的内存,并给你一个Span权限让你直接填入字符。
intlength=10+5;stringresult=string.Create(length,state,(span,state)=>{// 直接在内存上“填色”,无需中间商赚差价"Hello".AsSpan().CopyTo(span);"World".AsSpan().CopyTo(span.Slice(5));});4. 模式建议:串联与汇聚
- 小量拼接(< 5个):直接用
$插值(Interpolation)。编译器底层会优化为string.Format或string.Concat,代码可读性最高。 - 数组/集合拼接:使用
string.Join。它内部已经处理好了缓冲区逻辑。 - 流式输出:如果要拼接的内容最终是写往文件或网络,永远不要先拼成大字符串。直接操作
StreamWriter或PipeWriter往缓冲区写。专业词汇解释
5.字符串拼接决策流程
7.预分配容量
提示:使用 StringBuilder 时,可以设置初始容量。这一做法有什么好处?如何选择适当的初始容量?
设置StringBuilder的初始容量(Capacity)是进阶开发者的常规操作。如果你不设置,它默认只有16个字符。
1. 为什么要设置初始容量?
如果不设置初始容量,当拼接的字符串长度超过当前缓冲区大小时,StringBuilder会执行以下昂贵的操作:
- 申请新内存:通常会申请一个翻倍大小的新数组。
- 数据迁移:将旧数组中的所有字符拷贝到新数组中。
- 产生垃圾:旧的数组变成垃圾,等待 GC 回收。
如果你能预判长度并设置初始容量,就可以实现一次分配,终身使用,从而彻底消除由于“扩容”带来的性能损耗。
2. 如何选择适当的容量?
选择容量不是靠死记硬背,而是基于业务场景的预估与权衡。
| 场景 | 策略 | 建议做法 |
|---|---|---|
| 已知固定格式 | 精确计算 | 例如拼接 DateTime 或固定长度的 ID,直接算出总和。 |
| 循环处理数据 | 平均值原则 | 统计单条数据的平均长度,乘以循环次数。 |
| 不确定长度 | 保守估算 | 给出一个能覆盖 90% 情况的“大样”值,防止频繁扩容。 |
| 超长文本 | 分段处理 | 如果超过 80KB,会进入大对象堆(LOH),需谨慎分配。 |
经验
- 不要过度分配:如果你只需要拼接 50 个字符,却分配了 10,000 的容量,会浪费内存,特别是在高并发环境下。
- 2 的幂次方:虽然不是强制要求,但计算机内存管理通常更亲和 2 的幂(如 64, 128, 256, 512)。
3. 代码示例
// 假设我们要生成一个 CSV 的一行,已知大约有 10 列,每列平均 15 字符intestimatedCapacity=10*15;varsb=newStringBuilder(estimatedCapacity);foreach(varcolindataColumns){sb.Append(col).Append(",");}8.文化和语言
提示:在处理国际化字符串时,StringBuilder 和 string 在理念和实现上有什么不同?
在国际化(i18n)背景下处理字符串,开发者最容易掉进的坑就是把字符串仅仅当成“字符数组”。在 C# 中,string和StringBuilder处理国际化时的核心差异体现在语义一致性与内存布局的权衡上。
1. 理念差异:文化敏感性 vs 纯字节搬运
string(文化感知型):
string 的设计初衷是作为数据的“表现层”。在进行 Compare、IndexOf 或 Replace 操作时,它默认是受 CultureInfo 影响的。
例子:在土耳其语中,大写的 I 并不是 i。如果你用 string.ToUpper(),结果会根据当前线程的文化背景而变化。
StringBuilder(纯粹的缓冲区):
- StringBuilder 的理念是“高效构建”。它几乎不关心文化属性。当你调用 sb.Append 时,它只是机械地将 UTF-16 编码的 char 搬运到缓冲区。它不负责处理复杂的语言规则,只负责内存的高效伸缩。
2. 实现差异:代理对(Surrogate Pairs)的处理
这是国际化中最专业的问题。很多复杂的汉字、表情符号(Emoji)在 UTF-16 编码下占用2个 char(即 4 字节)。
string 的实现:
- string 作为一个完整的对象,很多内置方法会尝试维护字符的完整性。虽然它本质也是 UTF-16,但 C# 的 StringInfo 类可以配合 string 来枚举“文本元素(Text Elements)”,确保你不会把一个表情符号斩成两半。
StringBuilder 的实现:
- StringBuilder 操作的是底层的 char 数组。如果你在做截断操作(例如设置 sb.Length = n),而位置 n 恰好落在一个代理对中间,StringBuilder 不会阻止你。这会导致产生非法的 Unicode 字符串,输出时显示为乱码或问号。
3. 内存布局:复合格式化的代价
在国际化中,我们经常使用string.Format或插值来根据模板生成多语言文本。
| 特性 | string.Format / 插值 | StringBuilder.AppendFormat |
|---|---|---|
| 内存分配 | 每次调用都会分配新字符串。 | 在原有缓冲区修改,零或极低分配。 |
| 文化参数 | 隐式使用 CurrentCulture。 | 强制建议传入 IFormatProvider。 |
| 性能 | 适合短小的翻译词条。 | 适合生成大型的多语言报表、HTML 邮件。 |
9.异常处理
提示:StringBuilder 在执行操作时有哪些情况可能抛出异常?如何安全地处理这些异常?
主要异常风险集中在以下三个方面:
1. OutOfMemoryException (OOM):最常见的杀手
这是StringBuilder最容易触发的异常。即便你的物理内存很大,但在以下情况仍会发生:
- 连续内存不足:
StringBuilder底层是连续的char[]。如果你需要申请 1GB 的空间,但内存中没有这么大的连续空闲块,即便总可用内存够,也会抛出 OOM。 - 达到最大限制:
StringBuilder有个属性叫MaxCapacity。如果你拼接的内容超过了这个预设上限(默认是int.MaxValue),它会罢工。
2. ArgumentOutOfRangeException:边界检查失败
这类异常通常发生在试图“操纵”缓冲区时:
- 错误索引:调用
Insert、Remove或Replace时传入了负数索引,或索引超出了当前Length。 - 截断错误:设置
sb.Length为负数。 - 初始容量越界:在构造函数里设置
Capacity大于MaxCapacity。
3. ArgumentNullException
虽然简单,但容易忽视。向Append方法传入null不会报错(它会当成空字符串处理),但如果调用AppendFormat时传入了null的格式化字符串,程序会直接崩溃。
4. 安全处理与防御策略
针对这些异常,我不建议到处写try-catch,而应该采用防御性编程:
策略 A:检查剩余空间
在执行大规模插入前,先对比MaxCapacity。
if (sb.Length + newData.Length > sb.MaxCapacity) { // 提前采取措施,比如持久化到磁盘或报错,而不是等 OOM }策略 B:预估容量而非盲目扩容
如果你知道要处理巨量数据,在构造时就给一个合理的Capacity。这能减少频繁申请内存导致的碎片化,降低 OOM 概率。
策略 C:处理国际化/非法字符的安全截断
当你手动修改Length来截断字符串时,要防止把 Unicode 代理对(Surrogate Pair)切断。
publicstaticvoidSafeTruncate(StringBuildersb,inttargetLength){if(targetLength<=0){sb.Length=0;return;}if(targetLength>=sb.Length)return;// 如果截断点正好在一个高代理项上,说明我们把一个完整字符切断了if(char.IsHighSurrogate(sb[targetLength-1])){sb.Length=targetLength-1;// 往前再退一格,保持字符完整性}else{sb.Length=targetLength;}}专业词汇解释
- MaxCapacity:
StringBuilder实例允许达到的最大字符容量。 - 代理对 (Surrogate Pair):用两个
char表示一个 Unicode 字符(如 Emoji)。切断它会导致产生非法的乱码字符。 - 连续内存 (Contiguous Memory):指地址空间上挨在一起的内存块。数组要求内存必须是连续的。
- 容量 (Capacity):
StringBuilder当前内部缓冲区能容纳的最大字符数。 - 长度 (Length):当前已经实际写入的字符数。
- 扩容 (Reallocation):当
Length > Capacity时,内部自动寻找更大内存块并搬家的过程。 - LOH (Large Object Heap):大对象堆。在 .NET 中,大于 85,000 字节的对象会直接进入此区域,GC 回收它们的代价非常高。
- CultureInfo:.NET 中代表特定文化(语言+地区)的对象,决定了数字、日期和大小写的格式。
- 代理对 (Surrogate Pairs):在 UTF-16 中,用两个 16 位代码单元表示一个单一字符的机制(常见于生僻字和 Emoji)。
- 文本元素 (Text Element):用户感知的一个完整字符。一个文本元素可能由多个
char组成。 - IFormatProvider:一个接口,用于控制如何将对象转换为字符串表示形式(如
CultureInfo就实现了它)。 - 不可变性 (Immutability):指对象一旦创建,其内容就不能修改。在 C# 中,修改字符串本质上是销毁旧的,创建一个新的。
- 堆分配 (Heap Allocation):在托管堆上分配内存,由 GC 负责清理。频繁分配会触发 GC,导致程序卡顿(Stop-the-world)。
- Span:一种类型安全的、表示内存连续区域的结构,可以指向栈内存或堆内存。
- GC (Garbage Collection):垃圾回收机制,负责自动清理不再使用的内存对象。
- JIT (Just-In-Time) Warmup:代码第一次运行时需要编译成机器码,这会很慢。Benchmark 工具会先跑几次“热身”,确保测量的是编译后的真实速度。
- MemoryDiagnoser:BenchmarkDotNet 的一个插件,能够精确计算出托管方法在堆上申请的内存字节数。
- Stop-the-world:当
string拼接产生过多垃圾触发 GC 时,程序可能会短暂挂起,这对高性能应用是致命的。 - Thread-Safe (线程安全):指代码在多线程环境下并发执行时,能够保证逻辑正确,不会出现竞态条件或内存损坏。
- Race Condition (竞态条件):指多个线程同时访问同一资源,且最终结果取决于线程执行的精确时序。
- IndexOutOfRangeException (索引超出范围异常):当程序尝试访问数组或集合中不存在的下标时抛出的错误。
- O(n^2):描述算法复杂度的符号。在循环中使用
string +=的时间复杂度接近 O(n^2),而StringBuilder是 O(n)。 - String.Concat:C# 编译器在处理连续的
+号拼接时调用的底层方法,它会先计算总长度再分配一次内存,比多次+=效率高。 - Allocation Overhead (分配开销):创建一个新对象(如
new StringBuilder())本身也是有成本的。如果只拼接两个短字符串,这个开销可能超过它带来的收益。 - Gen 0 / Gen 1 / Gen 2:.NET GC 的分代管理机制。Gen 0 存放新对象,回收速度最快;Gen 2 存放长寿对象,回收成本最高。
- Stop-the-world:GC 在进行某些回收操作时,必须暂停所有应用线程,这会导致程序短暂“卡死”。
- Managed Heap (托管堆):由 CLR 自动管理的内存区域,所有引用类型都分配在这里。
- Buffer (缓冲区):
StringBuilder内部用于暂存字符的数组,避免了频繁申请内存。 - **Immutable (不可变性):**对象创建后,其状态(内存中的数据)不可更改。
- Managed Heap (托管堆):.NET 运行时用来存放引用类型实例的内存区域。
- **LOH (Large Object Heap, 大对象堆):**专门存储大于 85,000 字节对象的堆区域,GC 对此区域的回收成本极高。
- **String Interning (字符串驻留):**CLR 的一种优化机制,相同字面量的字符串在内存中只存一份,但这只针对常量或显式调用的字符串。