C# 基础数据类型之字符串类型详解

 更新时间:2026年05月25日 08:44:38   作者:少年。  
C#字符串作为引用类型却常被当作值类型使用,探讨其不可变性、字符串驻留及常用操作,本文给大家介绍C# 基础数据类型之字符串类型,感兴趣的朋友一起看看吧

字符串不就是一串字符吗,能有多少花样?实际上,字符串作为引用类型,却常常表现出值类型的“错觉”,在拼接、比较、内存开销等方面藏着不少坑。这篇就来聊聊 C# 中的 string,从本质到常用骚操作,帮你把字符串用得明明白白。

  • 字符串的本质:它到底是值类型还是引用类型?
  • 不可变性与字符串驻留:为什么修改字符串会创建新对象?
  • 常用操作与最佳实践:拼接、比较、格式化怎么选?
  • 性能陷阱与优化:什么时候该用 StringBuilder
  • 字符串与 Span :新一代高性能字符串处理方式

一、字符串的本质

1.1 什么是字符串

在 C# 中,string​ 是 System.String​ 的别名,表示一个 不可变的字符序列。它虽然是引用类型,但设计上模仿了值类型的语义(如比较相等性时比较内容而非引用)。

string greeting = "Hello, World!";
Console.WriteLine(greeting.Length); // 13

代码解析:

  • string:定义一个字符串变量,本质是 System.String 类的实例。
  • "Hello, World!" :字符串字面量,编译器会将其放入字符串驻留池。
  • Length:字符串的长度属性,表示字符数(注意 string​ 的索引是基于 Char​ 的,对于包含代理项的 Unicode 字符,一个“字符”可能占两个 Char)。

1.2 字符串是引用类型,但行为像值类型

字符串属于引用类型,分配在堆上,变量存储的是引用。但它重写了 Equals 方法,让比较时比较内容。此外,字符串的不可变性使得它经常被当作值类型来使用。

string a = "hello";
string b = "hello";
Console.WriteLine(object.ReferenceEquals(a, b)); // True(因为字符串驻留)
Console.WriteLine(a == b); // True

核心知识点:字符串比较时,==​ 运算符被重载为比较内容,而 ReferenceEquals​ 比较引用。由于 字符串驻留 机制,相同字面量的字符串常常指向同一块内存。

二、字符串的不可变性

2.1 为什么字符串不可变?

字符串一旦创建,其内部的字符数组就不能被修改。任何修改操作(如 ToUpper​、Replace、拼接)都会生成一个新的字符串对象,而原字符串保持不变。

string original = "hello";
string modified = original.ToUpper();
Console.WriteLine(original); // "hello"
Console.WriteLine(modified); // "HELLO"
Console.WriteLine(object.ReferenceEquals(original, modified)); // False

划重点: 不可变性保证了字符串是线程安全的,多个线程可以共享同一个字符串实例,无需担心数据被意外修改。

2.2 字符串驻留(String Interning)

公共语言运行时(CLR)维护一个字符串驻留池,对于编译时确定的字面量字符串,会自动驻留。运行时可手动调用 string.Intern 将字符串放入池中。

string s1 = "Hello";
string s2 = "Hello";
string s3 = new string("Hello".ToCharArray());
Console.WriteLine(ReferenceEquals(s1, s2)); // True(编译时驻留)
Console.WriteLine(ReferenceEquals(s1, s3)); // False(s3是动态创建的字符串)
s3 = string.Intern(s3);
Console.WriteLine(ReferenceEquals(s1, s3)); // True(手动驻留后相同)

常见坑: 大量动态拼接的字符串如果不手动驻留,会占用过多内存。适时使用 string.Intern 可以减少重复字符串的内存开销,但过度使用反而会增加驻留池的压力。

三、字符串常用操作

3.1 拼接

方式特点适用场景
+ 运算符简单直接,每次拼接创建新对象少量拼接(< 5次)
string.Concat内部使用 StringBuilder 优化?不,直接拼接明确知道拼接个数时
string.Format格式字符串,可读性好带格式化的拼接
$ 字符串插值语法糖,编译后等同 string.FormatC# 6.0 起推荐

StringBuilder | 可变缓冲区,频繁修改性能高 | 循环中大量拼接 |

// 少量拼接,用插值
var msg = $"Hello, {name}! You have {count} messages.";
// 循环拼接,用 StringBuilder
var sb = new StringBuilder();
foreach (var item in items)
{
    sb.Append(item).Append(", ");
}
string result = sb.ToString().TrimEnd(',', ' ');

3.2 比较

推荐使用 string.Equals​ 或静态方法,并指定 StringComparison 选项。

string a = "hello";
string b = "HELLO";
bool ignoreCase = a.Equals(b, StringComparison.OrdinalIgnoreCase);
bool exact = string.Compare(a, b, StringComparison.Ordinal) == 0;

划重点: 在.NET Core/.NET 5+ 中,默认的 ==​ 比较采用 序数比较(Ordinal),行为与 StringComparison.Ordinal​ 一致。但在 .NET Framework 中默认使用当前区域性进行语言比较,可能导致意外的排序结果。始终显式指定比较模式,以避免跨平台差异。

3.3 格式化

string.Format 和字符串插值支持复合格式,包括对齐和格式化字符。

double price = 123.456;
Console.WriteLine($"Price: {price,10:C2}"); // 右对齐,宽度10,货币格式
// 输出:Price:   ¥123.46

四、性能陷阱与优化

4.1 循环中的字符串拼接

每次用 + 拼接都会创建新字符串,在循环中会引发严重的性能和内存问题。

// 错误做法
string s = "";
for (int i = 0; i < 100000; i++)
{
    s += i.ToString(); // 每次循环都分配新字符串
}
// 正确做法
var sb = new StringBuilder();
for (int i = 0; i < 100000; i++)
{
    sb.Append(i);
}
string result = sb.ToString();

【提示】 少量拼接(比如 3-5 次)直接用插值或 +​,编译器会优化成 string.Concat​,性能不差。但一旦进了循环,务必用 StringBuilder,否则垃圾回收(GC)会教你做人。

4.2 字符串的截取与内存

Substring​ 方法在 .NET Framework 中返回的字符串与原字符串共享内部的字符数组,可能导致大对象被长期引用。但在 **.NET Core / .NET 5+ ** 中,Substring 每次都会创建新的字符数组,不再共享内存。

string filePath = @"C:\Users\Alice\Documents\report.docx";
// .NET Core 中,Substring 会分配新内存
string fileName = filePath.Substring(filePath.LastIndexOf('\\') + 1);

4.3 使用 Span<char>​ 和 ReadOnlySpan<char> 处理字符串

当需要在不分配堆内存的情况下对字符串进行切片、解析时,Span<T> 是神器。

ReadOnlySpan<char> span = "Hello, World".AsSpan();
ReadOnlySpan<char> slice = span.Slice(0, 5); // 指向 "Hello",无堆内存分配
Console.WriteLine(slice.ToString()); // 但调用 ToString 还是会在堆上分配字符串

划重点: Span<T> 可以避免大量临时字符串的分配,尤其在解析配置文件、日志行等场景下性能飞跃。但它只能在栈上使用,不能作为类的字段。

五、常用字符串方法一览

方法功能是否修改原字符串
Length获取字符数只读属性
ToUpper()​/ToLower()大小写转换返回新字符串
Trim()移除首尾空白返回新字符串
Split(char[])分割字符串返回字符串数组
Substring(int, int)提取子字符串返回新字符串
Replace(string, string)替换子串返回新字符串
Contains(string)判断是否包含子串只读取
IndexOf(string)查找子串位置只读取

常见坑: Replace​ 和 Trim 等返回新字符串,如果忘了接收返回值,原字符串不会改变。

string text = "  hello  ";
text.Trim(); // 错误!原字符串不变
string trimmed = text.Trim(); // 正确,用 trimmed 接收新值

六、字符串与编码

C# 的 string​ 内部使用 UTF-16 编码存储,即每个字符至少占 2 个字节。当与外部二进制数据交互时,需要使用 Encoding 类进行转换。

string text = "Hello, 世界";
byte[] utf8Bytes = Encoding.UTF8.GetBytes(text);
string decoded = Encoding.UTF8.GetString(utf8Bytes);

性能注意: Encoding.GetBytes​ 和 GetString​ 涉及内存分配,对于高吞吐场景,可以使用 Encoding.GetBytes​ 的重载,将数据写入 Span<byte> 缓冲区。

七、总结

字符串虽然简单,但用不好就会成为性能瓶颈。记住几个核心原则:

  • 不可变性是双刃剑:保证了线程安全,但拼接会生成大量临时对象。
  • 少量拼接用插值,循环拼接用 ​StringBuilder​。
  • 比较字符串时务必指定 ​StringComparison​,避免跨平台行为差异。
  • 在 .NET Core / .NET 5+ 中,​​Substring​​ 不再共享内存,可以放心使用。
  • 善用 ​Span<T>​ 实现零分配的字符串切片和解析。

最后:字符串是门大学问,搞清楚了不可变性和驻留机制,能帮你避开大部分坑。下次遇到字符串拼接变慢,别犹豫,换上 StringBuilder 再说。

到此这篇关于C# 基础数据类型:字符串类型的文章就介绍到这了,更多相关C# 字符串类型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#中数组、ArrayList和List三者的区别详解

    C#中数组、ArrayList和List三者的区别详解

    这篇文章主要介绍了C#中数组、ArrayList和List三者的区别详解,对于三者之间的区别想要了解的可以进来了解一下。
    2016-12-12
  • C#使用反射和LINQ查询程序集的元数据

    C#使用反射和LINQ查询程序集的元数据

    在 C# 中,反射是一个强大的工具,它允许我们在运行时检查程序集、类型、方法等的元数据,结合 LINQ,我们可以用更简洁和表达力强的方式处理这些信息,本文将详细讲解如何使用反射与 LINQ 查询程序集的元数据,需要的朋友可以参考下
    2024-08-08
  • C#高效反射调用方法类实例详解

    C#高效反射调用方法类实例详解

    在本篇文章中小编给大家分享的是关于C#高效反射调用方法类的相关实例内容,有兴趣的朋友们学习下。
    2019-07-07
  • C#中lock关键字的使用小结

    C#中lock关键字的使用小结

    在C#中,lock关键字用于确保当一个线程位于给定实例的代码块中时,其他线程无法访问同一实例的该代码块,下面就来介绍一下lock关键字的使用
    2025-07-07
  • C#如何动态创建lambda表达式

    C#如何动态创建lambda表达式

    这篇文章主要介绍了C#如何动态创建lambda表达式问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • C#实现为一张大尺寸图片创建缩略图的方法

    C#实现为一张大尺寸图片创建缩略图的方法

    这篇文章主要介绍了C#实现为一张大尺寸图片创建缩略图的方法,涉及C#创建缩略图的相关图片操作技巧,需要的朋友可以参考下
    2015-06-06
  • 使用WPF在Windows实现任务栏缩略图效果

    使用WPF在Windows实现任务栏缩略图效果

    这篇文章主要为大家详细介绍了如何使用WPF在Windows实现任务栏缩略图效果,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-06-06
  • C#多线程系列之多线程锁lock和Monitor

    C#多线程系列之多线程锁lock和Monitor

    这篇文章介绍了C#多线程锁lock和Monitor的用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02
  • C#通过rabbitmq实现定时任务(延时队列)

    C#通过rabbitmq实现定时任务(延时队列)

    工作中经常会有定时任务的需求,常见的做法可以使用Timer、Quartz、Hangfire等组件,本文使用C#通过rabbitmq实现定时任务(延时队列),感兴趣的可以了解一下
    2021-05-05
  • Unity的AssetPostprocessor之Model函数使用实战

    Unity的AssetPostprocessor之Model函数使用实战

    这篇文章主要为大家介绍了Unity的AssetPostprocessor之Model函数使用实战,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08

最新评论