string.create 仅适用于长度已知且需逐字符计算的高性能场景,如十六进制格式化、base64编码等;不适用于字符串拼接或简单插值,否则反而更慢且易出错。

String.Create 是唯一能真正避免堆分配的字符串创建方式,但必须配合 Span
什么时候该用 String.Create 而不是 $"" 或 string.Concat
当你需要构造一个「长度已知、内容需逐字符计算」的字符串,且对 GC 压力敏感(如高频日志、序列化、网络协议编码),才值得引入 String.Create。它不适用于拼接已有字符串或简单插值场景——那只会增加认知负担和潜在错误。
- 适合:将
int格式化为固定长度十六进制字符串、Base64 编码中间结果、JSON 数字序列化缓冲区填充 - 不适合:
$"User: {name}, ID: {id}"、string.Join(", ", list)、path + "/" + filename - 关键判断点:能否在调用前确定最终长度?能否把每个字符的生成逻辑写进一个
Span<char> → void</char>委托里?
必须传入 Span 回调,不能直接操作 string
String.Create 的核心设计是把字符串内存的「写入权」交给用户回调,自己只负责分配并保证不可变性。你无法拿到可变的 string 引用,也不能在回调里做字符串拼接或调用 ToString() —— 那会触发额外分配,彻底破坏零分配目标。
- 正确写法:
String.Create(8, value, (span, i) => { span[0] = '0'; span[1] = 'x'; /* ... */ }) - 错误写法:
String.Create(8, value, (span, i) => span.Fill(i.ToString()));(i.ToString()分配新字符串) - 错误写法:
String.Create(8, value, (span, i) => span.CopyTo(someOtherString.AsSpan()));(假设someOtherString已存在,但长度不匹配就崩溃)
性能陷阱:泛型参数 T 不能是 class,且必须是只读结构体
String.Create<t></t> 的 T 必须是 ref struct 或无状态 struct,否则 JIT 无法内联回调,会退化为委托调用+装箱。常见翻车点是把 class 实例(比如 JsonSerializerOptions)当状态传进去,或者用 readonly struct 但内部含引用字段。
- 安全状态类型:
readonly struct HexState { public readonly int Value; public HexState(int v) => Value = v; } - 危险状态类型:
class FormatContext { public string Prefix; }或struct BadState { public StringBuilder Builder; } - 验证方法:编译后用
dotnet tool install -g ilspycmd反编译,确认回调未生成newobj或callvirt
实际可用的最小完整示例
下面这个函数把 uint 转成 8 位小写十六进制字符串,全程无托管堆分配(.NET 6+):
static string ToHex8(uint value) =>
String.Create(8, value, (span, state) =>
{
const string hex = "0123456789abcdef";
for (int i = 7; i >= 0; i--)
{
span[i] = hex[(int)(state & 0xf)];
state >>= 4;
}
});
注意:这个函数在 Release 模式下会被 JIT 内联,span 直接映射到新字符串的底层 char[];若你在 Debug 模式测试,会看到明显性能下降——那是调试器强制禁用了 span 内联优化,不代表真实表现。
真正难的是把业务逻辑拆解成「长度预判 + 单字符确定」两步,而不是记 API 签名。多数人卡在这一步,就退回 StringBuilder——这其实更合理,除非你正在写高性能序列化库或实时音频元数据处理器。











