在C#里处理文本时,字符串属于引用类型且具备不可变性,这意味着一旦创建,其内容就无法更改。当我们需要交换字符串中两个位置的字符,例如把第1个和第3个字符对调,不能像操作数组那样直接通过索引赋值。理解这一限制之后,才能选择合适的实现方式,在保证代码正确性的同时兼顾运行效率。

为什么不能直接用索引器修改字符串
很多初学者会尝试用类似 str[0] = str[1] 的写法,但C#的字符串类型并没有提供可写的索引器。字符串的索引器仅用于读取字符,其本质是对只读内存的访问。如果强行赋值,编译器会提示无法对属性或索引器赋值,因为它是只读的。这一设计来源于字符串在CLR层面的不可变特性,任何变更都会产生新的字符串实例,旧实例若无引用就会被回收。
从内存角度看,字符串实际保存在托管堆上的连续字符数组之外的一层封装中。正因为不可变,多个变量可以安全地共享同一个字符串,且字符串驻留机制也能减少重复内容的内存占用。但也正因如此,所有变更操作都必须以创建新字符串为代价。在交换字符这种场景下,我们需要先脱离字符串的不可变约束,借助可变容器完成修改。
基于字符数组的常规交换写法
最直观且易读的方式是将字符串转换为 char[] 数组,交换数组元素后再调用构造函数生成新字符串。下面示例演示如何交换索引 i 与 j 处的字符:
using System;
class Program
{
static string SwapChars(string input, int i, int j)
{
if (input == null) throw new ArgumentNullException(nameof(input));
if (i < 0 || j < 0 || i >= input.Length || j >= input.Length)
throw new ArgumentOutOfRangeException("索引超出字符串长度");
char[] chars = input.ToCharArray();
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
return new string(chars);
}
static void Main()
{
string original = "abcdef";
string swapped = SwapChars(original, 1, 4);
Console.WriteLine(swapped); // 输出 aecdbf
}
}
这段代码先做了参数校验,避免空引用和越界。通过 ToCharArray 得到的数组是可变的,交换逻辑与普通数组并无区别。最后用 new string(char[]) 构造结果,原字符串保持不变,符合不可变原则。
这种写法的优点是逻辑清晰、易于维护,适合绝大多数业务场景。缺点是会产生一次字符数组分配以及一次新字符串分配。如果交换操作只执行少数几次,这点开销可以忽略;但在循环里高频调用时,就需要考虑更轻量的方案。
使用 Span 减少内存分配
从 C# 7.2 开始,我们可以使用 Span<char> 在栈上或已有内存上操作,避免额外的堆分配。对于字符串,可借助 stackalloc 创建临时缓冲区,但要注意栈空间有限,不适合超长文本。
using System;
class Program
{
static string SwapWithSpan(string input, int i, int j)
{
if (input == null) throw new ArgumentNullException(nameof(input));
if (i < 0 || j < 0 || i >= input.Length || j >= input.Length)
throw new ArgumentOutOfRangeException("索引超出字符串长度");
Span<char> buffer = stackalloc char[input.Length];
input.AsSpan().CopyTo(buffer);
(buffer[i], buffer[j]) = (buffer[j], buffer[i]);
return new string(buffer);
}
static void Main()
{
string text = "hello";
Console.WriteLine(SwapWithSpan(text, 0, 4)); // 输出 oellh
}
}
上述代码利用 stackalloc 在栈上分配字符缓冲区,并通过 AsSpan 将字符串内容复制进去。元组交换语法让代码更简洁,最后同样用构造函数生成字符串。由于缓冲区在栈上,方法结束后自动回收,不会给 GC 带来压力。
不过需要注意,栈空间通常只有几 MB,若字符串过长会导致栈溢出。因此该方案更适合短字符串或性能敏感且调用频繁的路径。若字符串长度不确定且可能很大,仍建议回退到字符数组方案,或分批处理。
不同方案的对比与选择
我们可以用一张表来概括前面两种实现的核心差异:
| 方案 | 内存分配 | 适用场景 | 代码复杂度 |
|---|---|---|---|
| 字符数组 ToCharArray | 堆上数组 + 新字符串 | 通用、长文本也可 | 低 |
| Span 栈分配 | 仅新字符串(栈缓冲) | 短文本、高频调用 | 中 |
从表中可以看出,没有绝对最优的解法,只有更匹配当前上下文的写法。如果只是在配置解析或偶尔的数据清理中交换字符,字符数组写法已经足够。若在解析热路径中需要反复调整字符顺序,且字符串较短,Span 方案能明显降低分配次数。
另外要强调的是,无论哪种方式,都不要试图通过不安全代码去强改字符串内部字段。那样会破坏不可变契约,导致驻留字符串被篡改,引发难以排查的全局错误。坚持通过副本修改再生成新实例,是稳妥且符合语言设计哲学的做法。
总结实践要点
交换字符串中的字符,核心是先转换到可变形态,再交换,最后生成新字符串。常规开发用 ToCharArray 最为直观;追求性能且字符串短小时可用 Span<char> 配合栈分配。始终做好边界检查和空值防护,避免索引越界异常。
只要牢记字符串不可变这一前提,就能自然推导出上述写法。在代码评审中,若看到有人直接对字符串索引赋值,应及时指出并改用本文所列方式,既保证安全,也方便后续维护。