MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的字符串处理与正则表达式

2021-10-051.7k 阅读

C# 字符串基础

在C#中,字符串是一种重要的数据类型,用于存储和操作文本。string 关键字是 System.String 类的别名,它表示一个不可变的 Unicode 字符序列。

字符串的声明与初始化

// 声明并初始化一个字符串
string str1 = "Hello, World!";
// 通过构造函数初始化
string str2 = new string(new char[] { 'H', 'e', 'l', 'l', 'o' });

这里,str1 直接使用字符串字面量进行初始化,而 str2 则通过 string 类的构造函数,使用字符数组来初始化。

字符串的特性 - 不可变性

C# 中的字符串是不可变的。这意味着一旦字符串被创建,其内容就不能被修改。任何看似修改字符串的操作,实际上都是创建了一个新的字符串对象。

string original = "Hello";
string modified = original + ", World";

在上述代码中,original 字符串对象并没有被修改,而是 original + ", World" 操作创建了一个新的字符串对象,并将其引用赋给 modified

字符串操作方法

C# 的 string 类提供了丰富的方法来处理字符串。

字符串拼接

  1. 使用 + 运算符
string part1 = "Hello";
string part2 = " World";
string combined = part1 + part2;

+ 运算符简单直观,但在循环中频繁使用会导致性能问题,因为每次拼接都会创建新的字符串对象。 2. 使用 string.Concat 方法

string s1 = "One";
string s2 = "Two";
string s3 = "Three";
string result = string.Concat(s1, s2, s3);

string.Concat 方法可以接受多个字符串参数,内部会对参数进行优化,减少创建中间字符串对象的次数,性能优于 + 运算符在多个字符串拼接时的情况。 3. 使用 StringBuilder 当需要在循环中大量拼接字符串时,StringBuilder 类是更好的选择。

using System.Text;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
    sb.Append(i.ToString());
}
string finalString = sb.ToString();

StringBuilder 类在内部维护一个可变的字符数组,避免了每次拼接都创建新的字符串对象,从而大大提高了性能。

字符串查找

  1. Contains 方法
string sentence = "The quick brown fox jumps over the lazy dog";
bool containsFox = sentence.Contains("fox");

Contains 方法用于检查字符串中是否包含指定的子字符串,返回 truefalse。 2. IndexOf 方法

string text = "apple, banana, cherry";
int index = text.IndexOf("banana");

IndexOf 方法返回指定子字符串第一次出现的位置,如果未找到则返回 -1。 3. LastIndexOf 方法

string str = "ababab";
int lastIndex = str.LastIndexOf("ab");

LastIndexOf 方法返回指定子字符串最后一次出现的位置,如果未找到则返回 -1。

字符串替换

  1. Replace 方法
string input = "Hello, World!";
string replaced = input.Replace("World", "C#");

Replace 方法将字符串中指定的旧子字符串替换为新的子字符串,并返回新的字符串。

字符串截取

  1. Substring 方法
string fullString = "Hello, C# Developer";
string sub1 = fullString.Substring(7);
string sub2 = fullString.Substring(7, 6);

第一种形式的 Substring 方法从指定位置开始截取到字符串末尾,第二种形式则从指定位置开始截取指定长度的子字符串。

字符串大小写转换

  1. ToUpper 方法
string lowerCase = "hello";
string upperCase = lowerCase.ToUpper();

ToUpper 方法将字符串中的所有字符转换为大写形式。 2. ToLower 方法

string upper = "HELLO";
string lower = upper.ToLower();

ToLower 方法将字符串中的所有字符转换为小写形式。

字符串修剪

  1. Trim 方法
string withSpaces = "   Hello   ";
string trimmed = withSpaces.Trim();

Trim 方法移除字符串开头和结尾的空白字符。 2. TrimStart 方法

string startSpaces = "   Hello";
string trimmedStart = startSpaces.TrimStart();

TrimStart 方法仅移除字符串开头的空白字符。 3. TrimEnd 方法

string endSpaces = "Hello   ";
string trimmedEnd = endSpaces.TrimEnd();

TrimEnd 方法仅移除字符串结尾的空白字符。

正则表达式基础

正则表达式是一种用于匹配、搜索和替换文本的强大工具。在C# 中,正则表达式相关的功能主要由 System.Text.RegularExpressions 命名空间提供。

正则表达式语法基础

  1. 字符匹配
    • 普通字符:普通字符直接匹配自身,例如 a 匹配字符 a
    • 元字符:具有特殊含义的字符,如 . 匹配除换行符外的任意字符。例如,正则表达式 .at 可以匹配 catbat 等。
  2. 字符类
    • 方括号 []:定义一个字符类,匹配方括号内的任意一个字符。例如,[abc] 匹配 abc
    • 范围表示:在方括号内可以使用 - 表示范围,如 [a - z] 匹配任意小写字母。
  3. 量词
    • *:表示前面的字符或字符类可以出现 0 次或多次。例如,ab* 可以匹配 aababb 等。
    • +:表示前面的字符或字符类可以出现 1 次或多次。例如,ab+ 可以匹配 ababb 等,但不匹配 a
    • ?:表示前面的字符或字符类可以出现 0 次或 1 次。例如,ab? 可以匹配 aab
    • {n}:表示前面的字符或字符类必须出现 n 次。例如,a{3} 只匹配 aaa
    • {n,}:表示前面的字符或字符类必须出现至少 n 次。例如,a{3,} 匹配 aaaaaaa 等。
    • {n,m}:表示前面的字符或字符类必须出现至少 n 次,但不超过 m 次。例如,a{3,5} 匹配 aaaaaaaaaaaa
  4. 分组与捕获
    • 圆括号 ():用于分组和捕获。例如,(ab)+ 表示 ab 整体可以出现 1 次或多次。捕获组会记住匹配的内容,以便后续使用。
  5. 边界匹配
    • ^:匹配字符串的开头。例如,^Hello 只匹配以 Hello 开头的字符串。
    • $:匹配字符串的结尾。例如,World$ 只匹配以 World 结尾的字符串。

在 C# 中使用正则表达式

C# 提供了 Regex 类来处理正则表达式。

匹配操作

  1. Regex.IsMatch 方法
using System.Text.RegularExpressions;
string input = "This is a test string";
bool isMatch = Regex.IsMatch(input, "test");

Regex.IsMatch 方法用于判断输入字符串是否与指定的正则表达式匹配,返回 truefalse。 2. Regex.Match 方法

string text = "The price is $10.99";
Match match = Regex.Match(text, @"\$\d+\.\d{2}");
if (match.Success)
{
    Console.WriteLine(match.Value);
}

Regex.Match 方法尝试在输入字符串中查找与正则表达式匹配的第一个子字符串。如果找到匹配项,Match.Success 属性为 true,可以通过 Match.Value 获取匹配的内容。这里正则表达式 @"\$\d+\.\d{2}" 用于匹配以 $ 开头,后跟一个或多个数字,再跟一个小数点和两个数字的价格格式。注意这里使用了逐字字符串(以 @ 开头),以避免对反斜杠进行转义。 3. Regex.Matches 方法

string content = "Apples cost $1.99, Oranges cost $2.49";
MatchCollection matches = Regex.Matches(content, @"\$\d+\.\d{2}");
foreach (Match match in matches)
{
    Console.WriteLine(match.Value);
}

Regex.Matches 方法查找输入字符串中所有与正则表达式匹配的子字符串,并返回一个 MatchCollection,可以通过遍历 MatchCollection 来获取所有匹配项。

替换操作

  1. Regex.Replace 方法
string sentence = "The dog runs fast";
string replaced = Regex.Replace(sentence, "dog", "cat");

Regex.Replace 方法将输入字符串中所有与正则表达式匹配的子字符串替换为指定的替换字符串。这里将所有出现的 dog 替换为 cat

分割操作

  1. Regex.Split 方法
string data = "apple,banana;cherry|date";
string[] parts = Regex.Split(data, "[,;|]");
foreach (string part in parts)
{
    Console.WriteLine(part);
}

Regex.Split 方法根据正则表达式指定的分隔符将输入字符串分割成字符串数组。这里正则表达式 [,;|] 表示以逗号、分号或竖线作为分隔符。

正则表达式的高级应用

命名捕获组

在复杂的正则表达式中,命名捕获组可以使代码更易读和维护。

string email = "user@example.com";
Match emailMatch = Regex.Match(email, @"(?<username>[a-zA-Z0 - 9_.+-]+)@(?<domain>[a-zA-Z0 - 9 -]+\.[a-zA-Z0 - 9 -]+)");
if (emailMatch.Success)
{
    string username = emailMatch.Groups["username"].Value;
    string domain = emailMatch.Groups["domain"].Value;
    Console.WriteLine($"Username: {username}, Domain: {domain}");
}

这里,(?<username>[a-zA-Z0 - 9_.+-]+) 是一个命名捕获组,名称为 username,用于捕获电子邮件地址中的用户名部分。(?<domain>[a-zA-Z0 - 9 -]+\.[a-zA-Z0 - 9 -]+) 是另一个命名捕获组,用于捕获域名部分。通过 Match.Groups["组名"] 可以获取相应捕获组匹配的内容。

反向引用

反向引用允许在正则表达式中引用之前捕获组匹配的内容。

string html = "<b>Bold Text</b>";
Match htmlMatch = Regex.Match(html, @"<([a-zA-Z]+)>(.*?)</\1>");
if (htmlMatch.Success)
{
    string tag = htmlMatch.Groups[1].Value;
    string content = htmlMatch.Groups[2].Value;
    Console.WriteLine($"Tag: {tag}, Content: {content}");
}

在这个例子中,([a-zA-Z]+) 是第一个捕获组,</\1> 中的 \1 是对第一个捕获组的反向引用,确保结束标签与开始标签匹配。

正则表达式选项

Regex 类的许多方法都接受一个 RegexOptions 枚举值,用于指定正则表达式的行为。

  1. RegexOptions.IgnoreCase:忽略大小写匹配。
string text1 = "Hello";
bool match1 = Regex.IsMatch(text1, "hello", RegexOptions.IgnoreCase);
  1. RegexOptions.Multiline:使 ^$ 分别匹配每行的开头和结尾,而不仅仅是整个字符串的开头和结尾。
string multilineText = "Line1\nLine2\nLine3";
Match multilineMatch = Regex.Match(multilineText, @"^Line2$", RegexOptions.Multiline);
  1. RegexOptions.Singleline:使 . 字符匹配包括换行符在内的所有字符。默认情况下,. 不匹配换行符。
string textWithNewline = "Hello\nWorld";
Match singlelineMatch = Regex.Match(textWithNewline, @"Hello.World", RegexOptions.Singleline);

字符串处理与正则表达式的性能考量

字符串操作性能

  1. 不可变字符串的性能影响 由于C# 字符串的不可变性,频繁的字符串拼接操作会导致大量的内存分配和垃圾回收。如前文所述,在需要大量拼接字符串时,应优先使用 StringBuilder 类。
  2. 查找与替换操作的性能 string 类的查找和替换方法通常是高效的,但对于复杂的查找和替换需求,正则表达式可能更强大,但性能相对较低。例如,简单的子字符串查找使用 string.Containsstring.IndexOf 方法通常比使用正则表达式更快。

正则表达式性能

  1. 正则表达式的编译 每次使用 Regex 类的静态方法(如 Regex.IsMatchRegex.Match 等)时,正则表达式都会被编译。如果需要多次使用相同的正则表达式,可以预编译正则表达式以提高性能。
Regex regex = new Regex(@"[a - z]+");
for (int i = 0; i < 1000; i++)
{
    Match match = regex.Match($"test{i}");
}

这里通过 new Regex 创建一个 Regex 对象,预编译了正则表达式,避免了每次匹配时的编译开销。 2. 复杂正则表达式的性能 复杂的正则表达式,特别是包含大量回溯的表达式,可能会导致性能问题。例如,过度使用贪婪量词(如 *+)而没有适当的限制可能会使匹配过程变得非常缓慢。在编写正则表达式时,应尽量使其简洁和高效,避免不必要的复杂性。

实际应用场景

数据验证

  1. 电子邮件验证
string emailToValidate = "user@example.com";
bool isValidEmail = Regex.IsMatch(emailToValidate, @"^[a-zA-Z0 - 9_.+-]+@[a-zA-Z0 - 9 -]+\.[a-zA-Z0 - 9 -]+$");

这个正则表达式用于验证电子邮件地址的基本格式,确保其符合常见的电子邮件地址规范。 2. 电话号码验证

string phoneNumber = "123 - 456 - 7890";
bool isValidPhone = Regex.IsMatch(phoneNumber, @"^\d{3}-\d{3}-\d{4}$");

此正则表达式用于验证美国格式的电话号码,确保其格式为三位数区号、三位数交换码和四位数号码,中间以 - 分隔。

文本提取

  1. 从 HTML 中提取链接
string htmlContent = "<a href='https://example.com'>Example Link</a>";
Match linkMatch = Regex.Match(htmlContent, @"<a href='([^']+)'>");
if (linkMatch.Success)
{
    string link = linkMatch.Groups[1].Value;
    Console.WriteLine(link);
}

这里通过正则表达式从 HTML 代码中提取 a 标签的 href 属性值,即链接地址。

数据清洗

  1. 去除字符串中的特殊字符
string dirtyText = "Hello!@# World?";
string cleanText = Regex.Replace(dirtyText, @"[^\w\s]", "");

此正则表达式 [^\w\s] 匹配任何非单词字符(字母、数字、下划线)和非空白字符,通过 Regex.Replace 将其替换为空字符串,从而实现去除特殊字符的目的。

在C# 开发中,熟练掌握字符串处理和正则表达式的使用,对于处理文本数据、验证用户输入、提取有用信息等任务至关重要。通过合理选择字符串操作方法和编写高效的正则表达式,可以提高程序的性能和健壮性。