字符串内部实现
大约 11 分钟约 3379 字
字符串内部实现
简介
字符串是 C# 中最常用的类型之一。理解字符串的不可变性、驻留池机制、Span<char> 切片优化和 StringBuilder 的内部原理,有助于编写高效的字符串处理代码,避免不必要的内存分配。
特点
字符串内存模型
不可变性与驻留池
// 字符串是不可变的引用类型
string s1 = "hello";
string s2 = s1;
s2 += " world"; // 创建新字符串,s1 不受影响
Console.WriteLine(s1); // "hello"
Console.WriteLine(s2); // "hello world"
// 字符串驻留池(String Intern Pool)
// 编译时字面量自动驻留
string a = "hello";
string b = "hello";
Console.WriteLine(ReferenceEquals(a, b)); // True!同一个对象
// 运行时创建的字符串不自动驻留
string c = "hel" + "lo"; // 编译时常量,驻留
string d = "hel" + GetString(); // 运行时,不驻留
Console.WriteLine(ReferenceEquals(a, d)); // False
// 手动驻留(慎用,驻留池不会被 GC 回收)
string e = string.Intern(d);
Console.WriteLine(ReferenceEquals(a, e)); // True
// 检查是否在驻留池中
string? found = string.IsInterned(d);
Console.WriteLine(found != null); // False(除非手动驻留)
// 驻留池的内存影响
// 驻留的字符串永远不会被 GC 回收!
// 只适合少量高频重复的字符串字符串内存布局
// .NET 中字符串的内存布局(UTF-16 编码)
// [TypeHandle (8 bytes)] [Length (4 bytes)] [Chars (n * 2 bytes)] [\0 (2 bytes)]
// 空字符串 "" 也至少占 26 字节(8+4+2+2+padding)
// 查看字符串大小
string s = "Hello";
Console.WriteLine($"Length: {s.Length}"); // 5 字符
// 内存大小 ≈ 8(TypeHandle) + 4(Length) + 5*2(Chars) + 2(\0) = 24 字节
// 空字符串优化
string empty1 = "";
string empty2 = string.Empty;
Console.WriteLine(ReferenceEquals(empty1, empty2)); // True
// Substring 的分配问题
string text = "Hello, World!";
string sub = text.Substring(7, 5); // "World" — 分配新字符串!
// 每次调用都分配一个新的 string 对象
// 使用 AsSpan 避免分配
ReadOnlySpan<char> span = text.AsSpan(7, 5); // 零分配切片
Console.WriteLine(span.ToString()); // "World"(ToString 时才分配)
// 字符串拼接的分配分析
string result = "";
for (int i = 0; i < 100; i++)
{
result += i.ToString(); // 每次循环创建一个新字符串!
// 100 次分配,越来越大的字符串被反复复制
}StringBuilder 深入
内部机制
// StringBuilder 内部使用链式 char[] 缓冲区
// 默认容量 16,按 2 倍扩展
var sb = new StringBuilder(); // 容量 16
var sb2 = new StringBuilder(1000); // 容量 1000
var sb3 = new StringBuilder("init", 200); // 初始值 + 容量 200
// 扩容过程
// 16 → 32 → 64 → 128 → 256 → 512 → 1024 → ...
// 每次扩容分配新数组并复制旧数据
// 性能建议:预估容量避免多次扩容
var sbOptimal = new StringBuilder(estimatedCapacity: 10000);
for (int i = 0; i < 1000; i++)
{
sbOptimal.AppendLine($"Item {i}: {SomeData()}");
}
// StringBuilder vs string.Join
// 拼接已知数量的字符串
string[] parts = { "Hello", " ", "World", "!" };
string joined = string.Join("", parts); // 一次分配
string concat = string.Concat(parts); // 一次分配(更快)
// 对于复杂拼接,StringBuilder 更优
var sb4 = new StringBuilder();
foreach (var part in parts)
{
if (part.Length > 0)
sb4.Append(part);
}
string result2 = sb4.ToString();
// Append vs AppendFormat vs 插值
int x = 42;
sb.Append("Value: ").Append(x); // 最快
sb.AppendFormat("Value: {0}", x); // 较慢(解析格式)
sb.Append($"Value: {x}"); // .NET 6+ 高性能插值
// .NET 6+ StringBuilder.AppendJoin
sb.AppendJoin(", ", Enumerable.Range(1, 10));
// "1, 2, 3, 4, 5, 6, 7, 8, 9, 10"StringBuilder 与 ValueStringBuilder
// .NET 内部使用 ValueStringBuilder(ref struct)
// 栈上分配,零堆开销
// 模拟 ValueStringBuilder 模式
public ref struct ValueStringBuilder
{
private Span<char> _buffer;
private int _length;
public ValueStringBuilder(Span<char> buffer)
{
_buffer = buffer;
_length = 0;
}
public void Append(ReadOnlySpan<char> value)
{
value.CopyTo(_buffer[_length..]);
_length += value.Length;
}
public void Append(int value)
{
value.TryFormat(_buffer[_length..], out int written);
_length += written;
}
public override string ToString() => new(_buffer[.._length]);
}
// 使用
Span<char> buffer = stackalloc char[256];
var vsb = new ValueStringBuilder(buffer);
vsb.Append("Hello, ");
vsb.Append(42);
string result = vsb.ToString(); // "Hello, 42"高性能字符串处理
使用 Span 处理字符串
// 零分配字符串解析
bool TryParseVersion(ReadOnlySpan<char> input, out Version? version)
{
int dotIndex = input.IndexOf('.');
if (dotIndex < 0) { version = null; return false; }
if (!int.TryParse(input[..dotIndex], out int major) ||
!int.TryParse(input[(dotIndex + 1)..], out int minor))
{
version = null;
return false;
}
version = new Version(major, minor);
return true;
}
// 使用 Span 逐字符处理
static ReadOnlySpan<char> TrimCustom(ReadOnlySpan<char> input)
{
int start = 0, end = input.Length - 1;
while (start <= end && char.IsWhiteSpace(input[start])) start++;
while (end >= start && char.IsWhiteSpace(input[end])) end--;
return input[start..(end + 1)];
}
// 字符串分割(避免 string.Split 的数组分配)
static IEnumerable<ReadOnlyMemory<char>> SplitLazy(string input, char separator)
{
int start = 0;
for (int i = 0; i < input.Length; i++)
{
if (input[i] == separator)
{
yield return input.AsMemory(start, i - start);
start = i + 1;
}
}
if (start < input.Length)
yield return input.AsMemory(start);
}
// 高性能 CSV 解析
void ParseCsvLine(ReadOnlySpan<char> line, Span<Range> ranges)
{
int rangeIndex = 0, start = 0;
for (int i = 0; i < line.Length; i++)
{
if (line[i] == ',')
{
ranges[rangeIndex++] = start..i;
start = i + 1;
}
}
ranges[rangeIndex] = start..line.Length;
}DefaultInterpolatedStringHandler
// .NET 6+ 字符串插值优化
// 编译器使用 DefaultInterpolatedStringHandler 减少分配
// C# 代码
string name = "World";
int count = 42;
string message = $"Hello, {name}! Count: {count}";
// 编译器生成(简化):
var handler = new DefaultInterpolatedStringHandler(22, 2);
handler.AppendLiteral("Hello, ");
handler.AppendFormatted(name);
handler.AppendLiteral("! Count: ");
handler.AppendFormatted(count);
string message2 = handler.ToStringAndClear();
// 自定义格式化器
// String.Create<TState> 零分配格式化
string formatted = string.Create(10, (name: "World", count: 42), (span, state) =>
{
"Hello, ".AsSpan().CopyTo(span);
state.name.AsSpan().CopyTo(span[7..]);
});字符编码
UTF-16 与 UTF-8 转换
// .NET 内部使用 UTF-16 编码
// 每个 char 是 2 字节(UTF-16 code unit)
// UTF-16 长度 vs UTF-8 长度
string emoji = "😀"; // U+1F600
Console.WriteLine(emoji.Length); // 2(surrogate pair)
Console.WriteLine(Encoding.UTF8.GetByteCount(emoji)); // 4
// 高性能 UTF-8 转换
ReadOnlySpan<byte> utf8Json = "\"Hello\""u8; // UTF-8 字面量
// 避免不必要的编码转换
void WriteUtf8ToStream(Stream stream, string text)
{
// 直接编码写入,不通过中间 byte[]
var encoder = Encoding.UTF8;
int byteCount = encoder.GetByteCount(text);
Span<byte> buffer = stackalloc byte[byteCount];
encoder.GetBytes(text, buffer);
stream.Write(buffer);
}
// Rune(.NET Core 3.0+)— 正确处理 Unicode 码点
string text2 = "Hello 😀 World";
foreach (var rune in text2.EnumerateRunes())
{
Console.WriteLine($"U+{rune.Value:X4} '{rune}'");
}
// 0048 'H', 0065 'e', 1F600 '😀', ...高性能字符串比较与搜索
/// <summary>
/// 字符串比较的性能优化
/// </summary>
// 1. 字符串比较方式
string a = "Hello World";
string b = "hello world";
// ❌ 慢 — 考虑文化信息
bool eq1 = string.Equals(a, b, StringComparison.CurrentCulture);
// ✅ 快 — 序数比较(字节级)
bool eq2 = string.Equals(a, b, StringComparison.Ordinal);
// ✅ 快速 — 忽略大小写的序数比较
bool eq3 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
// 性能排序:Ordinal > OrdinalIgnoreCase > CurrentCultureIgnoreCase > CurrentCulture
// 在热路径中始终使用 Ordinal
// 2. StartsWith/EndsWith 优化
string url = "https://example.com/api/users";
// ❌ 使用默认比较(文化敏感)
bool starts = url.StartsWith("https://");
// ✅ 使用 Ordinal 比较(更快)
bool starts2 = url.StartsWith("https://", StringComparison.Ordinal);
// 3. Contains 优化
string text = "The quick brown fox jumps over the lazy dog";
// ❌ 不区分大小写的 Contains(.NET 5+ 才有高性能版本)
bool contains = text.Contains("FOX", StringComparison.OrdinalIgnoreCase);
// 4. string.Create — 高性能字符串构造
// 零中间分配,直接写入目标字符串
string result = string.Create(10, 42, (span, state) =>
{
for (int i = 0; i < span.Length; i++)
span[i] = (char)('A' + (state + i) % 26);
});
// 5. AsSpan 与 stringComparison
ReadOnlySpan<char> span = "Hello World".AsSpan();
bool match = span.StartsWith("hello", StringComparison.OrdinalIgnoreCase);字符串池化与缓存
/// <summary>
/// 字符串缓存策略减少分配
/// </summary>
// 1. string.Intern 的正确使用
// 驻留池中的字符串不会被 GC 回收!只适合高频重复的短字符串
public static class StringCache
{
// ❌ 不要驻留用户输入或大字符串
// string.Intern(userInput); // 可能导致内存泄漏
// ✅ 适合驻留的:已知的高频常量字符串
private static readonly string[] CachedHeaders = new[]
{
string.Intern("Content-Type"),
string.Intern("Authorization"),
string.Intern("Accept"),
string.Intern("User-Agent"),
};
public static string GetCachedHeader(string header)
{
var interned = string.Intern(header);
return interned;
}
}
// 2. 使用 Dictionary 作为字符串缓存
public class StringKeyCache<TKey>
{
private readonly ConcurrentDictionary<string, TKey> _cache = new(StringComparer.Ordinal);
public bool TryGet(string key, out TKey value)
{
return _cache.TryGetValue(key, out value);
}
public void Set(string key, TKey value) => _cache[key] = value;
}
// 3. 避免在热路径中创建字符串
// ❌ 每次调用创建新字符串
public string GetStatusMessage_Old(int code)
{
return $"HTTP_{code}"; // 每次分配新字符串
}
// ✅ 使用 switch 表达式返回缓存的字符串
public string GetStatusMessage(int code) => code switch
{
200 => "OK",
404 => "Not Found",
500 => "Internal Server Error",
_ => $"HTTP_{code}" // 只有未知状态码才分配
};优点
缺点
常见字符串操作性能分析
string 操作的分配对比
/// <summary>
/// 各种字符串操作的内存分配对比
/// </summary>
// 1. 字符串拼接
string result = "";
for (int i = 0; i < 100; i++)
result += i.ToString();
// 分配: ~100 个字符串(每次拼接创建新字符串)
// GC压力: 高(大量短命对象)
// ✅ StringBuilder
var sb = new StringBuilder(1000);
for (int i = 0; i < 100; i++)
sb.Append(i);
result = sb.ToString();
// 分配: 1-3 个字符串(StringBuilder 内部缓冲区 + 最终 ToString)
// GC压力: 低
// 2. 格式化
string formatted = string.Format("User: {0}, Age: {1}, Score: {2}", name, age, score);
// 分配: 1 个字符串(内部使用 StringBuilder)
// ✅ 字符串插值(.NET 6+)
string interpolated = $"User: {name}, Age: {age}, Score: {score}";
// 分配: 1 个字符串(编译器优化为 DefaultInterpolatedStringHandler)
// 3. string.Join vs +
string[] parts = { "a", "b", "c", "d", "e" };
// ❌ 循环拼接
string joined = "";
foreach (var p in parts) joined += p + ","; // 5 次分配
// ✅ string.Join
joined = string.Join(",", parts); // 1 次分配
// ✅ string.Concat
joined = string.Concat(parts); // 1 次分配
// 4. 判断字符串是否为空
// 慢 → 快
if (str == "") { } // 比较
if (str == string.Empty) { } // 比较
if (str.Length == 0) { } // 长度检查(最快)
if (string.IsNullOrEmpty(str)) { } // null + 长度
if (string.IsNullOrWhiteSpace(str)) { } // 含空白字符高性能字符串去重
/// <summary>
/// 字符串去重的性能优化
/// </summary>
// 场景:处理大量日志文本,去除重复行
public static string[] DeduplicateLines(string[] lines)
{
// ❌ 使用 Distinct — 可能保留多个相等的字符串对象
var unique = lines.Distinct().ToArray();
// 内存: 所有原始字符串 + 去重后的数组
// ✅ 使用 HashSet + StringComparer.Ordinal
var set = new HashSet<string>(lines, StringComparer.Ordinal);
return set.ToArray();
// 内存: 只有去重后的字符串
// ✅ 使用 StringComparer + 拜访优化
var dict = new Dictionary<string, byte>(StringComparer.Ordinal);
foreach (var line in lines)
{
if (!dict.ContainsKey(line))
dict[line] = 0;
}
return dict.Keys.ToArray();
}
// 场景:HTTP 头部名称去重(利用驻留池)
public static string NormalizeHeader(string header)
{
// HTTP 头部名称是有限的已知集合
// 驻留后所有相同名称共享内存
return string.Intern(header);
}总结
字符串是不可变的 UTF-16 编码引用类型,每次修改都创建新对象。驻留池使相同字面量共享内存,但驻留的字符串永不释放。Substring 每次分配新字符串,用 AsSpan 替代实现零分配切片。StringBuilder 通过可变缓冲区减少拼接分配,预估容量可避免扩容。DefaultInterpolatedStringHandler(.NET 6+)优化字符串插值性能。处理 Unicode 时使用 Rune 替代 char 以正确处理代理对。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《字符串内部实现》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《字符串内部实现》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《字符串内部实现》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《字符串内部实现》最大的收益和代价分别是什么?
