C# 字符串基础

11 阅读3分钟

一、C# 字符串基础

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

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

1.1 什么是 string

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

// 声明字符串的几种方式
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()​),实际上都会在内存中创建一个新的字符串对象,而原字符串保持不变。

string original = "Hello";
string upper = original.ToUpper();

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

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

// ❌ 低效做法:循环内拼接
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}"最推荐,可读性最佳
StringBuildersb.Append("...")大量字符串的循环拼接
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 字符串比较

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

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 查找与截取

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 和字符串插值是格式化字符串的主要方式。

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

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# 代码。