C# 字符串基础

一、C# 字符串基础

其实,C# 中的 string 类型远比你想象的复杂。它不仅是日常开发中使用最频繁的类型之一,其背后的内存管理、不可变性等特性,也直接影响着代码的性能和正确性。

  1. 字符串的本质 :什么是 string,它和 char 数组有何不同?
  2. 不可变性:为什么说字符串是不可变的?这带来了哪些影响?
  3. 常用操作:拼接、比较、格式化、查找与替换。
  4. 性能与最佳实践:如何高效地处理大量字符串,避免常见的性能陷阱。

1.1 什么是 string?

在 C# 中,string​ 是一个引用类型,代表一个不可变的 Unicode 字符序列。它被定义在 System.String 命名空间中,是 .NET 框架中最基础的类型之一。

csharp 复制代码
// 声明字符串的几种方式
string str1 = "Hello, World!";
string str2 = string.Empty; // 空字符串,等同于 ""
string str3 = null; // 空引用,表示不指向任何对象
char[] charArray = { 'H', 'e', 'l', 'l', 'o' };
string str4 = new string(charArray); // 从字符数组创建

代码解析:

  1. ​string str1 = "Hello, World!";​ :这是最常用的字面量声明方式。
  2. ​string str2 = string.Empty;​ :推荐使用 string.Empty 而不是 "" 来表示空字符串,语义更清晰。
  3. ​string str3 = null;​ :null 和空字符串不同,表示变量不引用任何对象。调用 null 字符串的方法会抛出 NullReferenceException。
  4. ​new string(charArray)​ :展示了字符串可以从字符数组构建。

1.2 字符串的不可变性

划重点: 字符串对象一旦被创建,其内容就不能被更改。任何看似修改字符串的操作(如 ToUpper()​、Replace()​),实际上都会在内存中创建一个新的字符串对象,而原字符串保持不变。

csharp 复制代码
string original = "Hello";
string upper = original.ToUpper();

Console.WriteLine(original); // 输出: Hello (原字符串不变)
Console.WriteLine(upper);    // 输出: HELLO (新创建的字符串)

常见坑: 在循环中进行字符串拼接(+ 操作符)是性能杀手。因为每次拼接都会创建一个新的字符串对象,导致大量的内存分配和回收。

csharp 复制代码
// ❌ 低效做法:循环内拼接
string result = "";
for (int i = 0; i < 10000; i++)
{
    result += i.ToString(); // 每次循环都创建一个新字符串
}

// ✅ 高效做法:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
    sb.Append(i.ToString());
}
string efficientResult = sb.ToString();

二、字符串的常用操作

2.1 字符串拼接

除了 + 操作符,C# 提供了多种更优雅、高效的拼接方式。

方法 示例 适用场景
​+ 操作符 ​"Hello, " + name 少量、简单的拼接
​string.Concat() ​string.Concat("a", "b", "c") 拼接多个字符串
​string.Join() ​string.Join(", ", array) 用分隔符连接集合
​$ 字符串插值 ​$"Hello, {name}" 最推荐,可读性最佳
​StringBuilder ​sb.Append("...") 大量字符串的循环拼接
csharp 复制代码
string firstName = "John";
string lastName = "Doe";
int age = 30;

// 字符串插值 (推荐)
string message = $"Hello, my name is {firstName} {lastName} and I am {age} years old.";
Console.WriteLine(message);
// 输出: Hello, my name is John Doe and I am 30 years old.

// 使用 Join 连接数组
string[] words = { "The", "quick", "brown", "fox" };
string sentence = string.Join(" ", words);
Console.WriteLine(sentence);
// 输出: The quick brown fox

2.2 字符串比较

比较字符串时,要特别注意区域性 和大小写。

csharp 复制代码
string strA = "apple";
string strB = "Apple";

// 默认区分大小写
bool isEqual = strA == strB; // false

// 推荐:使用 StringComparison 枚举明确比较规则
bool isEqualIgnoreCase = string.Equals(strA, strB, StringComparison.OrdinalIgnoreCase); // true

// 用于排序的比较
int result = string.Compare(strA, strB, StringComparison.CurrentCultureIgnoreCase);

划重点: 在大多数内部编程场景(如比较配置项、URL、标识符)中,推荐使用 StringComparison.Ordinal​ 或 StringComparison.OrdinalIgnoreCase​,性能最好且行为可预测。仅在需要处理用户界面文本(如按字母排序)时,才使用 CurrentCulture 相关的比较。

2.3 查找与截取

csharp 复制代码
string text = "The quick brown fox jumps over the lazy dog.";

// 查找子串位置
int index = text.IndexOf("fox"); // 返回 16
bool contains = text.Contains("fox"); // true

// 截取子串
string sub = text.Substring(16, 3); // 从索引 16 开始,截取 3 个字符,结果为 "fox"

// 检查开头或结尾
bool startsWith = text.StartsWith("The"); // true
bool endsWith = text.EndsWith("dog."); // true

2.4 格式化

​string.Format 和字符串插值是格式化字符串的主要方式。

csharp 复制代码
double price = 1234.5678;

// 字符串插值 + 格式说明符
string formatted = $"Price: {price:C2}"; // 输出: Price: ¥1,234.57 (货币格式,两位小数)

// 等同于 string.Format
string formatted2 = string.Format("Price: {0:C2}", price);

// 其他常用格式
Console.WriteLine($"{123456789:N0}"); // 输出: 123,456,789 (数字,千位分隔符)
Console.WriteLine($"{0.25:P1}");      // 输出: 25.0% (百分比)
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd}"); // 输出: 2024-05-24 (日期)

三、性能与最佳实践

3.1 为什么 StringBuilder 更快?

本案例的执行流程(拆解版):

  1. ​string result = ""; result += "a";​ :内存中创建了 "" 和 "a" 两个字符串对象,"" 被丢弃。
  2. ​result += "b";​ :内存中创建了 "ab" 新对象,"a" 被丢弃。
  3. ​result += "c";​ :内存中创建了 "abc" 新对象,"ab" 被丢弃。

可以看到,N 次拼接会产生 N 个临时字符串对象,导致频繁的 GC(垃圾回收)。

​StringBuilder​ 内部维护一个可变 的字符缓冲区。当调用 Append 时,它直接在缓冲区末尾添加字符。只有当缓冲区容量不足时,才会重新分配更大的内存。这大大减少了内存分配和回收的次数。

3.2 字符串驻留

CLR(公共语言运行时)维护着一个名为驻留池的哈希表,用于存储编译时声明的字符串字面量。

csharp 复制代码
string a = "hello";
string b = "hello";

// 因为 "hello" 是字面量,a 和 b 指向驻留池中的同一个对象
bool isSameObject = object.ReferenceEquals(a, b); // true

常见坑: 动态创建的字符串(如 new string(...)​ 或 Console.ReadLine()​ 读取的)默认不会被驻留。如果需要,可以手动调用 string.Intern() 方法,但通常不推荐,因为驻留池中的对象不会被 GC 回收。

3.3 最佳实践总结

场景 推荐做法 避免做法
少量、固定字符串拼接 ​$ 字符串插值 无
循环内大量拼接 ​StringBuilder ​+ 操作符
比较字符串(忽略大小写) ​string.Equals(a, b, StringComparison.OrdinalIgnoreCase) ​a.ToLower() == b.ToLower() (会创建临时小写字符串)
判断字符串是否为空或空白 ​string.IsNullOrEmpty()​ 或 string.IsNullOrWhiteSpace() ​str == ""​ 或 str == null
频繁修改字符串内容 ​StringBuilder​ 或 char[] 多次调用 Replace​、Remove 等方法

最后: 字符串是 C# 中最基础也最容易被忽视的类型。理解其不可变性、掌握正确的比较和拼接方式,是写出高性能、无 Bug 代码的关键一步。希望这篇能帮你避开那些常见的坑,写出更优雅的 C# 代码。

‍

相关推荐
ZWZhangYu39 分钟前
从 Dify 实践理解大模型上下文缓存
java·后端·spring
考虑考虑2 小时前
瀚高数据库
数据库·后端·postgresql
锋行天下2 小时前
DeepSeek写Langgraph就是个大傻逼,一本正经胡说八道
前端·人工智能·后端
小小张说故事4 小时前
Python 的 str 为什么设计成不可变?拆开字符串驻留、字符串池,以及 += 拼接到底慢在哪
后端·python
AI架构师之家4 小时前
我把 Hermes 这匹"马"养了两周,它干了一件让我有点发毛的事
后端
dpharness4 小时前
块协议用了两周,我把九个坑按顺序记下来了
后端
加宝的起点雷达5 小时前
跑了两周的信号日报,我把降级链改了四次
人工智能·后端
IT枫斗者枫哥5 小时前
CompletableFuture失败后返回空列表:异常没了,业务含义也变了
java·后端
Gopher_HBo5 小时前
opentelemetrySDK源码解析
后端
UrbanJazzerati5 小时前
AutoHotkey v2 零基础快速入门 Part 10:给路线回放添加高精度计时,理解累计延迟与路线一致性
后端