优先用$"..."处理日常字符串拼接,因其安全、易读、支持表达式和空值安全;String.Format适用于需文化敏感格式化、日志性能控制或旧API兼容等场景。

直接用 $ 前缀加内插表达式,比 String.Format 更安全、更易读,且编译器会做基本类型检查;但若需延迟格式化、文化敏感处理或日志性能控制,String.Format 或 FormattableString 仍有不可替代的场景。
什么时候该用 $"..." 而不是 String.Format
绝大多数日常拼接场景,比如日志、提示、API 请求体生成,都该优先用内插字符串。它把变量名和位置绑定在文本里,不会因参数顺序错位导致值塞错占位符(String.Format("ID:{0}, Name:{1}", name, id) 这种写法极易翻车)。
-
$"User {userId} logged in at {DateTime.Now:HH:mm:ss}"—— 格式字符串直接跟在表达式后,不用记索引 - 编译器能在编辑时检查大括号里的表达式是否存在、是否可转为
string,而String.Format的参数是运行时才解析的 object 数组 - 支持表达式:如
$"Total: {items.Sum(x => x.Price * x.Count):C2}",无需先算出中间变量 - 空值安全:
string name = null; $"Hello {name}"输出"Hello ",不会抛NullReferenceException
{expression,width:format} 里的对齐和格式怎么写
内插表达式支持完整的复合格式语法,但必须写在同一个大括号内,顺序不能颠倒:先表达式,再逗号分隔的宽度(可选),再冒号分隔的格式字符串(可选)。
- 右对齐 10 字符宽:
$"ID: {id,10}";左对齐加负号:$"Name: {name,-15}" - 数字保留两位小数:
$"Price: {price:F2}";日期 ISO 格式:$"Date: {dt:yyyy-MM-dd}" - 宽度和格式可共存:
$"Pi: {Math.PI,8:F4}"→"Pi: 3.1416"(8 字符宽,右对齐) - 宽度只影响最终字符串表示,不改变表达式求值逻辑;若表达式结果本身超宽,宽度设置会被忽略
为什么有时 String.Format 还不能被取代
内插字符串本质是编译器糖,最终仍可能转成 String.Format 调用;但有些高级需求它搞不定,得绕回原生方法或用 FormattableString。
- 需要根据当前线程文化(
CultureInfo)动态格式化时,$"..."默认用当前文化,而String.Format(provider, ...)可显式传入 provider - 日志库想实现“日志级别不够就跳过整个字符串构造”,必须用
FormattableString类型接收内插表达式,再手动调用ToString(provider)或直接丢弃 ——$"..."一写出来就是个string,没法懒执行 - 某些旧 API(如部分 .NET Framework 方法)只接受
string+object[]签名,无法直接喂进内插结果 - 调试时想看原始模板和参数分离结构(比如用于国际化提取工具),
String.Format的固定模式比内插更易静态分析
容易踩的坑:嵌套大括号、转义、性能误解
内插字符串看着简单,但几个细节不注意就会报错或行为异常。
- 想输出字面量
{或},必须双写:$"Brace: {{value}}"→"Brace: {value}";单写会触发解析错误 - 表达式里不能含未闭合的大括号,比如
$"Json: {JsonConvert.SerializeObject(obj)}"没问题,但若obj是个含{的字符串,不会影响解析 —— 因为大括号只在最外层字符串中起语法作用 - 别以为
$"..."一定比String.Format快:两者编译后差异极小;真正影响性能的是表达式本身是否被重复计算(比如$"{ExpensiveFunc()}{ExpensiveFunc()}"会调两次) - 常量内插有限制:只有所有插值表达式都是编译期常量时,才能声明
const string,例如const string s = $"Version {1}.{2}";合法,但含变量就不行
最常被忽略的一点是:内插字符串的“延迟性”是假的 —— 它在拼接那一刻就执行了所有表达式。真要控制执行时机,得主动用 FormattableString 接收,而不是依赖语法糖。











