在C#里给字符串赋值看起来极其简单,一个等号就能搞定,比如 string name = "Alice";。但这个操作背后的机制并不像表面那么单纯。字符串类型在.NET中属于引用类型,却表现出很多值类型的特征,这源于它的不可变设计。理解字符串赋值的底层行为,对于避免内存浪费和性能陷阱非常重要。

先明确一个核心事实:每次给字符串变量赋一个新值,实际上并不是修改原对象,而是让变量指向一个新的字符串对象。比如执行 name = "Bob"; 之后,之前的 "Alice" 对象如果没有任何变量引用它,就会等待垃圾回收。这一点和数组、列表等可变引用类型完全不同。后面我们会结合字符串驻留和StringBuilder进一步展开。
基础赋值方式与不可变特性
最直观的赋值方式是直接使用字符串字面量,例如 string city = "Shanghai";。编译器在编译阶段就会把双引号内的内容处理成一个字符串对象,并把变量指向它。也可以使用 new string(char[]) 构造函数从字符数组创建字符串,比如 string letters = new string(new char[] {'A','B','C'});。不过这种方式会在堆上创建新对象,并且不会参与字符串驻留优化,通常只在特殊场景下使用。
还可以通过其他类型转换得到字符串,例如 string numText = 123.ToString(); 或者 string joined = string.Join(",", new[] {"a","b"});。这些方法内部都会生成新的字符串实例。理解这一点很关键:所有看似修改字符串的操作,如 text.ToUpper()、text.Replace("a","b")、text.Trim(),返回的都是新字符串,原始字符串保持不变。下面这段代码可以验证这一点:
string original = "Hello";
string changed = original.Replace("H", "J");
Console.WriteLine(original); // 输出 Hello
Console.WriteLine(changed); // 输出 Jello
Console.WriteLine(object.ReferenceEquals(original, changed)); // False
为什么不允许直接修改字符串内容?因为字符串作为不可变对象具有线程安全、哈希码稳定、适合做字典键等优势。如果字符串可变,多个引用指向同一对象时,任何一处的修改都会影响其他使用者,会带来大量难以排查的问题。代价就是频繁赋值或修改会产生较多临时对象,增加GC压力。
还有一个容易混淆的点:字符串变量的赋值可以改变变量指向的对象,但字符串中的字符无法被单独修改。比如 str[0] = 'X'; 会编译报错,因为字符串的索引器是只读的。即便通过 unsafe 代码或反射强行修改了字符数组,也会破坏.NET对字符串的固定假设,属于危险操作。
字符串驻留机制与赋值细节
当使用字面量给字符串赋值时,.NET运行时默认会启用字符串驻留。这意味着相同的字符串字面量在程序集加载时只会存储一份。例如下面两个变量实际上指向同一个内存地址:
string a = "HelloWorld"; string b = "HelloWorld"; Console.WriteLine(object.ReferenceEquals(a, b)); // True
驻留的好处是节省内存,并能让字符串比较的某些场景更快,因为当两个字符串变量都来自驻留池时,可以直接比较引用。不过要注意,运行时动态创建的字符串默认不会自动驻留。比如 string c = new string("HelloWorld".ToCharArray()); 和 string d = new string("HelloWorld".ToCharArray()); 是两个不同对象,ReferenceEquals 返回 False。
如果希望手动把动态字符串放入驻留池,可以调用 string.Intern 方法。它会先检查池中是否已有相同值的字符串,有则返回已有引用,没有则把当前字符串加入池中并返回引用。与之相对的是 string.IsInterned,仅查询不添加。下面示例演示了手动驻留的效果:
string s1 = new string(new char[] {'H','i'});
string s2 = new string(new char[] {'H','i'});
Console.WriteLine(object.ReferenceEquals(s1, s2)); // False
string interned1 = string.Intern(s1);
string interned2 = string.Intern(s2);
Console.WriteLine(object.ReferenceEquals(interned1, interned2)); // True
但手动驻留并不是无条件有益的。驻留池中的字符串在应用程序结束前不会被垃圾回收,如果大量动态生成的唯一字符串被驻留,反而会造成内存泄漏。因此只有对于生命周期长、重复概率高的字符串,才考虑手动驻留。大多数业务场景下使用默认字面量驻留即可。
高效修改字符串:StringBuilder的赋值方式
当需要反复拼接或修改字符串时,直接使用加号或 += 会让每次操作都产生新的字符串对象,尤其是循环中更为明显。例如下面这段代码在每轮循环都创建一个中间字符串,时间复杂度接近O(n²):
string result = "";
for (int i = 0; i < 10000; i++)
{
result += i.ToString() + ",";
}
对于这种场景,推荐使用 System.Text.StringBuilder。StringBuilder内部维护一个可变的字符缓冲区,通过 Append、Insert、Remove 等方法直接修改缓冲区内容,不会产生大量临时字符串。最后使用 ToString() 一次性生成最终字符串,或者通过 StringBuilder.ToString() 赋值给目标变量。
下面是用StringBuilder改写后的版本:
using System.Text;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
sb.Append(',');
}
string result = sb.ToString();
StringBuilder默认容量为16个字符,当追加内容超过容量时会自动扩容,通常容量翻倍。频繁扩容也会带来性能损耗,但远小于字符串不可变带来的复制开销。如果预先知道大概长度,可以在构造时指定容量,例如 new StringBuilder(100000),避免多次扩容。
需要特别指出的是,StringBuilder并不是在所有情况下都优于字符串直接拼接。如果拼接次数很少,比如只有三五次,直接使用加号或 string.Format 编译后的代码足够高效,而且可读性更好。编译器对于少量字符串拼接会调用 string.Concat 进行优化,不会造成明显的性能问题。StringBuilder的优势在循环或超过几十次拼接时才会明显体现。
常见误区与赋值建议
第一个常见误区是认为 string a = "abc"; a = a + "def"; 会修改原来的 "abc" 对象。实际上原来的对象没有被修改,反而额外创建了 "abcdef" 对象。如果这种操作出现在高频率调用的方法里,会产生大量垃圾对象,进而频繁触发GC,影响应用性能。建议在需要累加字符串的循环中使用StringBuilder,在简单拼接时直接使用插值字符串 $"{a}{b}"。
第二个误区是过度依赖 string.Format 或 + 来拼接包含数字和非字符串类型的值。例如 string result = "count=" + count; 在编译时会被转换为 string.Concat(object, object),涉及装箱操作。当拼接次数较多时,建议使用 StringBuilder.Append(int) 重载,避免不必要的装箱。
第三个误区是认为所有字符串都是驻留的。前面已经提到动态创建的字符串不会自动驻留,即使是相同内容也可能存在多个副本。在做频繁的字符串比较时,如果字符串来源是动态生成的,可以考虑使用 string.Intern 或改用 string.Equals 并指定 StringComparison.Ordinal 提高效率,但要权衡驻留带来的内存占用。
最后给出一个综合示例,展示不同赋值方式的选择原则:
// 少量拼接,优先使用插值或Concat
string userName = "Alice";
string greeting = $"Hello, {userName}!";
// 循环拼接,优先使用StringBuilder
StringBuilder logBuilder = new StringBuilder(256);
foreach (var item in items)
{
logBuilder.AppendLine($"Processing {item.Id}");
}
string log = logBuilder.ToString();
// 需要共享相同字符串引用时考虑驻留
string dynamicValue = GetDynamicString(); // 可能返回相同内容的不同对象
string interned = string.Intern(dynamicValue);
总结一下,C#给字符串赋值看似只是变量指向对象的简单操作,但因为字符串不可变、驻留机制和内存分配策略的存在,不同赋值方式在性能和内存上差异很大。编写代码时应根据拼接次数、字符串来源和应用场景选择合适的方案,避免在循环中直接使用 +=,同时理解驻留的限制,不要盲目手动驻留所有字符串。
C#字符串赋值字符串不可变StringBuilder修改时间:2026-10-04 08:48:52