1. C# 字符串基础概念
1.1 字符串不可变性
在 C# 中,字符串具有不可变性,这意味着一旦创建了一个字符串对象,其内容就不能被修改。例如,当我们执行以下代码时:
string str = "Hello";
str = str + " World";
实际上,str + " World"
并是修改了原来的 "Hello"
字符串,而是创建了一个全新的字符串对象 "Hello World"
,并将 str
的引用指向了这个新对象,原来的 "Hello"
字符串依然存在于内存中,直到被垃圾回收机制回收。这种不可变性虽然保证了字符串的安全性和一致性,但也可能会带来一些性能问题,尤其是在频繁修改字符串的场景下,因为每次修改都会产生新的字符串对象,从而增加内存的使用和垃圾回收的负担。
1.2 字符串与字符数组
字符串和字符数组在 C# 中可以相互转换,这为字符串的处理提供了更多的灵活性。
-
字符串转字符数组:可以通过
ToCharArray()
方法将字符串转换为字符数组。例如:
string str = "Hello World";
char[] charArray = str.ToCharArray();
这样,charArray
就包含了字符串 str
中的每个字符,可以方便地对单个字符进行操作,如排序、反转等。
-
字符数组转字符串:有多种方法可以将字符数组转换回字符串:
-
使用
string
类的构造函数:
-
char[] charArray = { 'H', 'e', 'l', 'l', 'o' };
string str = new string(charArray);
-
使用
string.Concat()
方法:
char[] charArray = { 'H', 'e', 'l', 'l', 'o' };
string str = string.Concat(charArray);
-
使用
string.Join()
方法,可以指定分隔符:
char[] charArray = { 'H', 'e', 'l', 'l', 'o' };
string str = string.Join("", charArray);
这些转换方法使得字符串和字符数组可以根据不同的需求进行灵活的处理,既可以利用字符串的不可变性和整体性,也可以利用字符数组的可变性和单个字符操作的便利性。
2. 常用字符串处理方法
2.1 字符串长度获取
获取字符串长度是字符串处理中最基本的操作之一。在 C# 中,可以通过 Length
属性来获取字符串的长度,它返回字符串中字符的数量。例如:
string str = "Hello World";
int length = str.Length;
Console.WriteLine(length); // 输出:11
Length
属性是一个只读属性,它在很多场景下都非常有用。例如,在处理用户输入时,可以根据字符串长度来判断输入是否符合要求;在进行字符串截取或拼接时,也可以根据长度来确定操作的范围。
需要注意的是,Length
属性返回的是字符串中字符的个数,而不是字节的个数。对于一些包含多字节字符(如中文、日文等)的字符串,其长度可能与实际占用的字节数不同。如果需要获取字符串的字节长度,可以使用 Encoding
类的相关方法,例如:
string str = "你好,世界";
int byteLength = System.Text.Encoding.UTF8.GetByteCount(str);
Console.WriteLine(byteLength); // 输出:21
2.2 字符串大小写转换
在实际开发中,经常需要对字符串进行大小写转换,以便进行比较、格式化或其他操作。C# 提供了 ToLower()
和 ToUpper()
方法来实现字符串的大小写转换。
-
ToLower():将字符串中的所有字符转换为小写形式。例如:
string str = "Hello World";
string lowerStr = str.ToLower();
Console.WriteLine(lowerStr); // 输出:hello world
-
ToUpper():将字符串中的所有字符转换为大写形式。例如:
string str = "Hello World";
string upperStr = str.ToUpper();
Console.WriteLine(upperStr); // 输出:HELLO WORLD
这些方法在处理用户输入、数据清洗等场景中非常有用。例如,在比较两个字符串是否相等时,通常会先将它们转换为统一的大小写形式,以避免因大小写不同而导致比较失败。此外,在生成日志信息、显示提示信息等场景中,也可以根据需要将字符串转换为大写或小写形式,以满足特定的格式要求。
需要注意的是,ToLower()
和 ToUpper()
方法不会修改原始字符串,而是返回一个新的字符串对象。这是因为字符串在 C# 中是不可变的,每次对字符串进行操作时都会创建一个新的字符串对象。如果需要对字符串进行多次大小写转换或其他操作,建议使用 StringBuilder
类来提高性能,避免频繁创建新的字符串对象。
3. 字符串截取与拼接
3.1 子字符串截取
在 C# 中,截取子字符串是一项常见的操作,通常用于提取字符串中的特定部分。Substring
方法是实现这一功能的主要方式,它可以根据指定的起始索引和长度来截取子字符串。
-
基本用法:
Substring
方法有两种重载形式。第一种只需要指定起始索引,从该索引开始截取到字符串的末尾。例如:string str = "Hello World"; string subStr = str.Substring(6); // 从索引 6 开始截取 Console.WriteLine(subStr); // 输出:World
第二种需要指定起始索引和截取的长度。例如:
string str = "Hello World"; string subStr = str.Substring(3, 5); // 从索引 3 开始截取长度为 5 的子字符串 Console.WriteLine(subStr); // 输出:lo Wo
-
性能考虑:虽然
Substring
方法使用起来非常方便,但在处理非常大的字符串或频繁进行截取操作时,可能会对性能产生一定影响。这是因为每次调用Substring
方法都会创建一个新的字符串对象。如果需要对同一个字符串进行多次截取操作,建议先将其转换为字符数组,然后通过索引操作来提取所需的子字符串,这样可以避免多次创建新的字符串对象。 -
边界检查:在使用
Substring
方法时,需要注意索引和长度的边界条件。如果索引或长度超出字符串的范围,会抛出ArgumentOutOfRangeException
异常。因此,在调用Substring
方法之前,最好先检查索引和长度是否有效。例如:string str = "Hello World"; int startIndex = 3; int length = 5; if (startIndex >= 0 && startIndex < str.Length && length >= 0 && startIndex + length <= str.Length) { string subStr = str.Substring(startIndex, length); Console.WriteLine(subStr); } else { Console.WriteLine("Invalid start index or length"); }
3.2 字符串拼接方式
在 C# 中,字符串拼接是将多个字符串连接成一个字符串的过程。有多种方法可以实现字符串拼接,每种方法在性能和可读性上都有其特点。
-
使用
+
运算符:这是最简单直观的拼接方式,适用于拼接少量字符串。例如:string str1 = "Hello"; string str2 = "World"; string result = str1 + " " + str2; Console.WriteLine(result); // 输出:Hello World
优点是代码简单直观,但缺点是在拼接大量字符串时效率较低,因为每次拼接都会创建一个新的字符串对象,导致频繁的内存分配和垃圾回收。
-
使用
string.Concat
方法:string.Concat
方法专门用于字符串拼接,可以将多个字符串参数连接成一个字符串。例如:string str1 = "Hello"; string str2 = "World"; string result = string.Concat(str1, " ", str2); Console.WriteLine(result); // 输出:Hello World
与使用
+
运算符类似,string.Concat
方法在拼接少量字符串时性能相当,但在拼接大量字符串时仍然会创建多个临时字符串对象,因此性能不如StringBuilder
。 -
使用
string.Join
方法:string.Join
方法可以将字符串数组或集合中的字符串元素连接成一个字符串,并在元素之间插入指定的分隔符。例如:string[] parts = { "Hello", "World" }; string result = string.Join(" ", parts); Console.WriteLine(result); // 输出:Hello World
这种方法在需要在字符串之间插入相同的分隔符时非常方便,代码可读性高。但如果不使用分隔符,使用
string.Join
方法会稍显繁琐。 -
使用
StringBuilder
类:StringBuilder
类是专为高效字符串拼接设计的。它通过维护一个可变的字符数组来避免频繁的内存分配。例如:StringBuilder sb = new StringBuilder(); sb.Append("Hello"); sb.Append(" "); sb.Append("World"); string result = sb.ToString(); Console.WriteLine(result); // 输出:Hello World
在拼接大量字符串时,
StringBuilder
的性能显著优于使用+
运算符或string.Concat
方法。它可以在内部动态调整字符数组的大小,减少内存分配次数。此外,StringBuilder
提供了多种方法来操作字符串,如Append
、AppendLine
、Insert
、Remove
等,使得字符串操作更加灵活。 -
使用字符串插值:字符串插值是 C# 6.0 引入的特性,允许在字符串文字中直接插入表达式的值。例如:
string name = "World"; string result = $"Hello, {name}!"; Console.WriteLine(result); // 输出:Hello, World!
字符串插值的代码可读性极佳,特别是在需要插入变量或表达式时。它在编译时会进行类型检查,减少了运行时错误。虽然在拼接大量字符串时性能不如
StringBuilder
,但通常足够满足日常需求。
在实际开发中,选择合适的字符串拼接方式非常重要。对于少量字符串的拼接,可以使用 +
运算符或字符串插值;对于大量字符串的拼接,推荐使用 StringBuilder
类;如果需要在字符串之间插入相同的分隔符,string.Join
方法是一个不错的选择。
4. 字符串查找与替换
4.1 查找字符或子字符串
在 C# 中,查找字符或子字符串是字符串处理中的常见任务,提供了多种方法来实现这一功能。
-
IndexOf 方法:这是最常用的方法之一,用于查找指定字符或子字符串在字符串中第一次出现的位置。例如:
string str = "Hello World"; int index = str.IndexOf("World"); Console.WriteLine(index); // 输出:6
如果查找的字符或子字符串不存在,则返回 -1。
IndexOf
方法还可以指定从哪个索引开始查找,以及查找的范围等参数,提供了灵活的查找功能。 -
LastIndexOf 方法:与
IndexOf
方法类似,但它是从字符串的末尾开始向前查找指定字符或子字符串的位置。例如:string str = "Hello World"; int index = str.LastIndexOf("o"); Console.WriteLine(index); // 输出:7
这在需要查找字符串中最后一次出现的字符或子字符串时非常有用。
-
Contains 方法:用于判断字符串中是否包含指定的子字符串。它返回一个布尔值,表示是否找到。例如:
string str = "Hello World"; bool contains = str.Contains("World"); Console.WriteLine(contains); // 输出:True
这种方法在需要快速判断子字符串是否存在时非常方便,但不提供具体的位置信息。
-
StartsWith 和 EndsWith 方法:分别用于判断字符串是否以指定的子字符串开头或结尾。例如:
string str = "Hello World"; bool startsWith = str.StartsWith("Hello"); bool endsWith = str.EndsWith("World"); Console.WriteLine(startsWith); // 输出:True Console.WriteLine(endsWith); // 输出:True
这些方法在处理文件名、协议头等场景中非常有用,可以快速判断字符串的格式是否符合要求。
4.2 替换字符或子字符串
在 C# 中,替换字符串中的字符或子字符串是常见的操作,提供了多种方法来实现这一功能。
-
Replace 方法:这是最常用的方法,用于替换字符串中的指定字符或子字符串。例如:
string str = "Hello World"; string result = str.Replace("World", "C#"); Console.WriteLine(result); // 输出:Hello C#
Replace
方法会替换字符串中所有匹配的子字符串。如果需要替换单个字符,也可以直接使用字符作为参数。例如:string str = "Hello World"; string result = str.Replace('o', '0'); Console.WriteLine(result); // 输出:Hell0 W0rld
这种方法简单高效,适用于大多数替换场景。
-
使用正则表达式替换:如果需要更复杂的替换规则,可以使用正则表达式。例如,替换所有以特定模式匹配的子字符串:
using System.Text.RegularExpressions; string str = "The quick brown fox jumps over the lazy dog."; string result = Regex.Replace(str, @"\bquick\b", "slow"); Console.WriteLine(result); // 输出:The slow brown fox jumps over the lazy dog.
正则表达式提供了强大的模式匹配能力,可以实现更复杂的替换逻辑,如替换特定格式的数字、日期等。
-
使用 StringBuilder 替换:如果需要对字符串进行多次替换操作,使用
StringBuilder
类可以提高性能。例如:StringBuilder sb = new StringBuilder("Hello World"); sb.Replace("World", "C#"); string result = sb.ToString(); Console.WriteLine(result); // 输出:Hello C#
StringBuilder
类在内部维护一个可变的字符数组,避免了每次替换都创建新的字符串对象,从而提高了性能。
5. 字符串拆分与组合
5.1 字符串拆分方法
在 C# 中,字符串拆分是将一个字符串按照指定的分隔符分解为多个子字符串的过程,这在处理文本数据时非常有用,例如解析 CSV 文件、分割用户输入等。
-
使用
Split
方法:这是最常用的字符串拆分方法,可以根据一个或多个分隔符将字符串拆分成子字符串数组。例如:string str = "Hello,World,C#"; string[] parts = str.Split(','); foreach (string part in parts) { Console.WriteLine(part); }
输出结果为:
Hello World C#
Split
方法还可以指定多个分隔符,以及是否移除空元素。例如:string str = "Hello, World; C#"; string[] parts = str.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); foreach (string part in parts) { Console.WriteLine(part.Trim()); // 使用 Trim 去除多余空格 }
输出结果为:
Hello World C#
-
使用正则表达式拆分:如果需要更复杂的拆分规则,可以使用正则表达式。例如,按照任意非字母字符拆分字符串:
using System.Text.RegularExpressions; string str = "Hello, World! C# is great."; string[] parts = Regex.Split(str, @"\W+"); // \W 匹配非字母字符 foreach (string part in parts) { if (!string.IsNullOrEmpty(part)) // 过滤空字符串 { Console.WriteLine(part); } }
输出结果为:
Hello World C is great
-
性能考虑:在处理非常大的字符串或频繁进行拆分操作时,
Split
方法的性能通常足够好。但如果需要复杂的拆分逻辑,正则表达式可能会带来一定的性能开销。在这种情况下,可以考虑预先编译正则表达式以提高性能:using System.Text.RegularExpressions; Regex regex = new Regex(@"\W+", RegexOptions.Compiled); string str = "Hello, World! C# is great."; string[] parts = regex.Split(str); foreach (string part in parts) { if (!string.IsNullOrEmpty(part)) { Console.WriteLine(part); } }
5.2 字符串组合方法
字符串组合是将多个字符串或字符串数组合并成一个字符串的过程,这在生成日志信息、构建文件路径等场景中非常常见。
-
使用
string.Join
方法:这是最常用的字符串组合方法,可以将一个字符串数组或集合中的字符串元素连接成一个字符串,并在元素之间插入指定的分隔符。例如:string[] parts = { "Hello", "World", "C#" }; string result = string.Join(", ", parts); Console.WriteLine(result); // 输出:Hello, World, C#
如果不需要分隔符,可以直接使用空字符串作为分隔符:
string[] parts = { "Hello", "World", "C#" }; string result = string.Join("", parts); Console.WriteLine(result); // 输出:HelloWorldC#
-
使用
StringBuilder
组合:如果需要动态构建字符串,StringBuilder
类是一个更好的选择。它通过维护一个可变的字符数组来避免频繁的内存分配。例如:StringBuilder sb = new StringBuilder(); string[] parts = { "Hello", "World", "C#" }; foreach (string part in parts) { sb.Append(part); sb.Append(", "); } sb.Length -= 2; // 移除最后一个多余的逗号和空格 string result = sb.ToString(); Console.WriteLine(result); // 输出:Hello, World, C#
-
使用 LINQ 组合:如果需要对字符串数组进行更复杂的操作后再组合,可以使用 LINQ。例如,将字符串数组中的每个元素转换为大写后再组合:
using System.Linq; string[] parts = { "Hello", "World", "C#" }; string result = string.Join(", ", parts.Select(p => p.ToUpper())); Console.WriteLine(result); // 输出:HELLO, WORLD, C#
-
性能比较:在组合少量字符串时,
string.Join
方法和StringBuilder
类的性能差异不大。但在组合大量字符串时,StringBuilder
的性能优势更加明显,因为它减少了内存分配次数。如果需要对字符串数组进行复杂的操作后再组合,使用 LINQ 可以使代码更加简洁,但可能会带来一定的性能开销。
6. 字符串格式化技巧
6.1 使用 string.Format
string.Format
是 C# 中用于格式化字符串的强大工具,它允许将多个变量插入到一个字符串模板中,并且可以指定各种格式化规则,适用于需要精确控制字符串格式的场景。
-
基本用法:
string.Format
方法通过占位符{}
来指定插入变量的位置。例如:string name = "Alice"; int age = 30; string formattedString = string.Format("Name: {0}, Age: {1}", name, age); Console.WriteLine(formattedString); // 输出:Name: Alice, Age: 30
在占位符中,
{0}
表示第一个参数,{1}
表示第二个参数,以此类推。 -
格式化数字:
string.Format
可以对数字进行格式化,例如指定小数点后的位数、使用千位分隔符等。例如:double price = 1234.5678; string formattedPrice = string.Format("Price: {0:C}", price); // 货币格式 Console.WriteLine(formattedPrice); // 输出:Price: $1,234.57 string formattedNumber = string.Format("Number: {0:N2}", price); // 保留两位小数 Console.WriteLine(formattedNumber); // 输出:Number: 1,234.57
-
格式化日期:对于日期和时间,
string.Format
提供了丰富的格式化选项。例如:DateTime now = DateTime.Now; string formattedDate = string.Format("Date: {0:yyyy-MM-dd}", now); // 年-月-日 Console.WriteLine(formattedDate); // 输出:Date: 2025-03-19 string formattedTime = string.Format("Time: {0:HH:mm:ss}", now); // 24小时制 Console.WriteLine(formattedTime); // 输出:Time: 14:30:45
-
格式化百分比:可以将数字格式化为百分比形式。例如:
double percentage = 0.75; string formattedPercentage = string.Format("Percentage: {0:P}", percentage); // 百分比格式 Console.WriteLine(formattedPercentage); // 输出:Percentage: 75.00 %
-
对齐和填充:
string.Format
还可以控制字符串的对齐和填充。例如:string leftAligned = string.Format("Left aligned: {0,-10}", "Hello"); Console.WriteLine(leftAligned); // 输出:Left aligned: Hello string rightAligned = string.Format("Right aligned: {0,10}", "Hello"); Console.WriteLine(rightAligned); // 输出:Right aligned: Hello
-
性能考虑:虽然
string.Format
功能强大,但在频繁调用时可能会对性能产生一定影响,因为它每次调用都会创建一个新的字符串对象。如果需要在循环中进行大量的格式化操作,建议使用StringBuilder
类来提高性能。
6.2 字符串插值
字符串插值是 C# 6.0 引入的一种更简洁的字符串格式化方式,它允许直接在字符串中嵌入变量或表达式,代码可读性更高。
-
基本用法:字符串插值使用
$
符号和大括号{}
来嵌入变量或表达式。例如:string name = "Alice"; int age = 30; string interpolatedString = $"Name: {name}, Age: {age}"; Console.WriteLine(interpolatedString); // 输出:Name: Alice, Age: 30
-
格式化数字和日期:与
string.Format
类似,字符串插值也支持格式化数字和日期。例如:double price = 1234.5678; DateTime now = DateTime.Now; string formattedString = $"Price: {price:C}, Date: {now:yyyy-MM-dd}"; Console.WriteLine(formattedString); // 输出:Price: $1,234.57, Date: 2025-03-19
-
嵌入表达式:字符串插值不仅可以嵌入变量,还可以嵌入任意表达式。例如:
int a = 10; int b = 20; string result = $"Sum: {a + b}, Product: {a * b}"; Console.WriteLine(result); // 输出:Sum: 30, Product: 200
-
性能比较:字符串插值在编译时会被转换为
string.Format
或其他等效代码,因此其性能与string.Format
相当。在处理少量格式化操作时,字符串插值的性能完全足够。但如果需要在循环中进行大量的格式化操作,建议使用StringBuilder
类来提高性能。 -
编译时检查:字符串插值的一个重要优点是它在编译时会进行类型检查,减少了运行时错误的可能性。例如,如果嵌入的变量类型与格式化字符串不匹配,编译器会报错。
-
结合 LINQ 使用:字符串插值可以与 LINQ 结合使用,生成复杂的格式化字符串。例如:
using System.Linq; int[] numbers = { 1, 2, 3, 4, 5 }; string result = $"Sum: {numbers.Sum()}, Average: {numbers.Average():F2}"; Console.WriteLine(result); // 输出:Sum: 15, Average: 3.00
在实际开发中,选择合适的字符串格式化方法非常重要。如果需要简洁易读的代码,推荐使用字符串插值;如果需要更复杂的格式化规则或在性能敏感的场景中,string.Format
和 StringBuilder
是更好的选择。
7. 字符串性能优化
7.1 使用 StringBuilder
在 C# 中,StringBuilder
是一个专门用于高效字符串操作的类,尤其在需要频繁修改字符串的场景下,其性能优势非常明显。
-
原理:
StringBuilder
内部维护一个可变的字符数组,当对字符串进行修改(如拼接、插入、删除等操作)时,不会像普通字符串那样创建新的对象,而是直接在内部的字符数组上操作,从而减少了内存分配和垃圾回收的开销。 -
性能对比:在处理大量字符串拼接时,使用
StringBuilder
的性能远优于使用+
运算符或string.Concat
方法。例如,在一个循环中拼接 10,000 个字符串,使用+
运算符可能会导致程序运行缓慢,而使用StringBuilder
可以显著提高性能,减少内存占用。 -
使用示例:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.Append("Hello"); } string result = sb.ToString();
-
动态调整容量:
StringBuilder
的容量可以根据需要动态调整。如果在拼接过程中发现当前容量不足,它会自动分配更大的内存空间。不过,为了避免频繁调整容量,可以在创建StringBuilder
时指定一个合理的初始容量,例如:StringBuilder sb = new StringBuilder(10000); // 初始容量为 10000
7.2 避免频繁操作字符串
频繁地对字符串进行操作(如拼接、截取、替换等)会导致大量的内存分配和垃圾回收,从而降低程序的性能。为了避免这种情况,可以采取以下策略:
-
减少不必要的字符串操作:在编写代码时,尽量减少对字符串的频繁修改。例如,如果需要拼接多个字符串,可以先将它们存储在一个集合中,然后一次性进行拼接,而不是在循环中逐个拼接。
-
使用临时变量:在处理复杂的字符串操作时,可以使用临时变量来存储中间结果,避免重复计算。例如,在多次调用
Substring
方法时,可以先将字符串存储在一个变量中,然后对这个变量进行操作。 -
优化循环中的字符串操作:如果在循环中需要对字符串进行操作,可以考虑使用
StringBuilder
或其他高效的方法。例如,在循环中拼接字符串时,使用StringBuilder
可以显著提高性能。 -
使用字符串池:对于一些常用的字符串,可以使用字符串池来避免重复创建。字符串池会缓存字符串对象,当需要使用相同的字符串时,可以直接从池中获取,而不需要重新创建。例如:
string str1 = "Hello"; string str2 = "Hello"; Console.WriteLine(object.ReferenceEquals(str1, str2)); // 输出:True
在这个例子中,
str1
和str2
指向同一个字符串对象,这是因为字符串池的作用。