目 录CONTENT

文章目录

CSharp(二十一)String 类详解

CSharp(二十一)String 类详解

一、概述与定义

String 是 C# 中最常用的类型之一,位于 System 命名空间下,表示不可变的 Unicode 字符序列。在 C# 中,string 关键字是 System.String 的别名,两者完全等价。

string s1 = "Hello";
// 完全等价于
System.String s2 = "Hello";

核心特性:不可变性(Immutability)

String 对象一旦创建,其值就永远无法改变。 所有看似"修改"字符串的操作(如 ReplaceSubstringToUpper 等),实际上都会创建一个全新的字符串对象,原始字符串保持不变。

string str = "Hello";
string newStr = str.Replace('H', 'J');  // str 仍然是 "Hello"
// newStr 是 "Jello",这是一个全新的对象

// 下面的代码创建了多个字符串对象,非常低效:
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString();  // 每次循环都创建新对象!
}

内存位置

  • String 是引用类型(存储在堆上),但表现得像值类型
  • 重写了 ==!= 操作符,比较的是内容而非引用
  • 支持**字符串驻留(String Interning)**机制

二、字符串的创建方式

2.1 字面量创建

string s1 = "Hello World";           // 普通字符串
string s2 = "";                       // 空字符串
string s3 = string.Empty;             // 推荐的空字符串写法(与 "" 等价)
string s4 = null;                     // null,与空字符串不同

2.2 使用构造函数

// 从字符重复创建
string s1 = new string('*', 10);           // "**********"

// 从字符数组创建
char[] chars = { 'H', 'e', 'l', 'l', 'o' };
string s2 = new string(chars);             // "Hello"

// 从字符数组的一部分创建
string s3 = new string(chars, 0, 3);       // "Hel"

// 从 sbyte 指针创建(unsafe 上下文)
unsafe
{
    sbyte* ptr = ...;
    string s4 = new string(ptr);
}

2.3 转义字符

// 常见的转义字符
string tab      = "Hello\tWorld";   // Hello    World(制表符)
string newline  = "Hello\nWorld";   // Hello(换行)World
string quote    = "He said \"Hi\""; // He said "Hi"
string backslash = "C:\\Program Files\\";  // C:\Program Files\
string nullChar = "Hello\0World";   // 包含空字符

// 完整转义字符表:
// \'   单引号(仅在字符字面量中使用)
// \"   双引号
// \\   反斜杠
// \0   空字符 (null)
// \a   警报(蜂鸣)
// \b   退格
// \f   换页
// \n   换行
// \r   回车
// \t   水平制表符
// \v   垂直制表符
// \u   后跟 4 位十六进制 Unicode 码位
// \U   后跟 8 位十六进制 Unicode 码位
// \x   后跟可变位数的十六进制值

2.4 逐字字符串字面量(Verbatim String)

使用 @ 前缀,转义序列不会被处理,适合写文件路径和正则表达式。

string path = @"C:\Program Files\MyApp\data.txt";  // 不需要转义反斜杠
string regex = @"\d{3}-\d{4}";                      // 正则表达式清爽

// 逐字字符串中的双引号需要写成两个
string quote = @"He said ""Hello""";  // He said "Hello"

// 逐字字符串可以跨行
string multiLine = @"第一行
第二行
第三行";
// 结果:"第一行\r\n第二行\r\n第三行"

2.5 原始字符串字面量(C# 11+)

使用 """ 包裹,是逐字字符串的增强版,非常适合包含大段 JSON、XML、SQL 等内容。

// 原始字符串字面量,内部无需任何转义
string json = """
{
    "name": "张三",
    "age": 30,
    "city": "北京"
}
""";

// 多个 $ 和 { 用于插值
string name = "张三";
string json2 = $$"""
{
    "name": "{{name}}",
    "path": "C:\Program Files\"
}
""";

// 允许左缩进对齐
string sql = """
    SELECT Id, Name, Age
    FROM Users
    WHERE Age > 18
    ORDER BY Name
    """;

2.6 字符串插值(String Interpolation, C# 6+)

string name = "张三";
int age = 25;
string city = "北京";

// 基本用法
string s1 = $"姓名:{name},年龄:{age}";

// 格式化
string s2 = $"价格:{123.456:F2}";           // "价格:123.46"
string s3 = $"日期:{DateTime.Now:yyyy-MM-dd}";
string s4 = $"百分比:{0.856:P2}";           // "百分比:85.60%"

// 对齐
string s5 = $"|{name,-10}|{age,5}|";        // 左对齐10位,右对齐5位

// 表达式
string s6 = $"明年年龄:{age + 1}";
string s7 = $"是否为成年人:{(age >= 18 ? "是" : "否")}";

// 结合逐字字符串
string s8 = $@"文件路径:C:\Users\{name}\Documents";

2.7 使用 Format 方法

string name = "李四";
int score = 95;

// 基于索引的占位符
string s1 = string.Format("姓名:{0},成绩:{1}分", name, score);

// 数字格式化
string s2 = string.Format("{0:C}", 1234.56);     // ¥1,234.56(货币)
string s3 = string.Format("{0:D5}", 42);         // "00042"(十进制补零)
string s4 = string.Format("{0:N2}", 12345.6789); // "12,345.68"(数字千分位)
string s5 = string.Format("{0:X}", 255);         // "FF"(十六进制)
string s6 = string.Format("{0:P1}", 0.856);      // "85.6%"(百分比)

2.8 常用格式化说明符

说明符 名称 示例输入 格式字符串 输出
C 货币 1234.56 {0:C} ¥1,234.56
D 十进制 42 {0:D5} 00042
E 科学计数法 12345.0 {0:E2} 1.23E+004
F 定点数 123.456 {0:F2} 123.46
G 常规 123.456 {0:G} 123.456
N 数字千分位 12345.67 {0:N1} 12,345.7
P 百分比 0.856 {0:P1} 85.6%
X 十六进制 255 {0:X} FF
0 零占位符 123 {0:00000} 00123
# 数字占位符 123 {0:#####} 123

三、字符串属性

属性 类型 说明
Length int 字符串中字符的数量(不是字节数)
Chars[int index] char this[int] 索引器,获取指定位置的字符(只读)
string str = "Hello World";

// Length 属性
Console.WriteLine(str.Length);      // 11(空格也算一个字符)

// 中文也按字符数计算,不是字节数
string cn = "你好世界";
Console.WriteLine(cn.Length);       // 4

// 索引器(只读,不能修改)
char ch = str[0];                   // 'H'
char last = str[^1];                // 'd'(C# 8+ 索引语法)
char second = str[1];               // 'e'

// 错误:不能通过索引修改字符串
// str[0] = 'h';                    // 编译错误!

四、字符串静态方法

4.1 比较方法

// Compare —— 返回整数(<0 表示前者小于后者,0 表示相等,>0 表示前者大于后者)
int result1 = string.Compare("abc", "ABC");                      // 1(区分大小写)
int result2 = string.Compare("abc", "ABC", ignoreCase: true);    // 0(忽略大小写)
int result3 = string.Compare("abc", "abd");                      // -1

// CompareOrdinal —— 基于码位比较(不依赖区域设置,性能更好)
int result4 = string.CompareOrdinal("abc", "ABC");               // 32(小写 > 大写)

// Equals —— 判断是否相等
bool eq1 = string.Equals("abc", "ABC");                          // false
bool eq2 = string.Equals("abc", "ABC", StringComparison.OrdinalIgnoreCase); // true

4.2 判断方法

// IsNullOrEmpty —— 判断是否为 null 或空字符串
bool b1 = string.IsNullOrEmpty(null);    // true
bool b2 = string.IsNullOrEmpty("");      // true
bool b3 = string.IsNullOrEmpty(" ");     // false(空格不是空字符串)

// IsNullOrWhiteSpace —— 判断是否为 null、空或仅包含空白字符(C# 4+)
bool b4 = string.IsNullOrWhiteSpace(null);   // true
bool b5 = string.IsNullOrWhiteSpace("");     // true
bool b6 = string.IsNullOrWhiteSpace("   ");  // true
bool b7 = string.IsNullOrWhiteSpace(" a ");  // false

4.3 Join —— 连接字符串数组

string[] words = { "苹果", "香蕉", "橘子" };

// 用指定分隔符连接
string s1 = string.Join(", ", words);        // "苹果, 香蕉, 橘子"
string s2 = string.Join("", words);          // "苹果香蕉橘子"

// 连接泛型集合(C# 4+)
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
string s3 = string.Join("-", numbers);       // "1-2-3-4-5"

// 从指定索引和数量拼接
string s4 = string.Join("|", words, 0, 2);   // "苹果|香蕉"

4.4 Concat —— 连接字符串

string s1 = string.Concat("Hello", " ", "World");              // "Hello World"
string s2 = string.Concat(new string[] { "A", "B", "C" });     // "ABC"
string s3 = string.Concat(new object[] { 1, " + ", 2, " = ", 3 }); // "1 + 2 = 3"

4.5 Interning(字符串驻留)

// Intern —— 将字符串放入驻留池(如果有相同值的字符串,则返回已有引用)
string s1 = string.Intern("Hello World");

// IsInterned —— 检查字符串是否在驻留池中(不在则返回 null)
string s2 = string.IsInterned("Hello World");

4.6 Copy(已过时,不推荐使用)

// 已标记为 Obsolete,不推荐使用
string s = "Hello";
string copy = string.Copy(s);   // 创建独立副本(不同引用)

五、字符串实例方法

5.1 查找与定位

string str = "Hello World, Hello C#";

// Contains —— 是否包含子字符串
bool b1 = str.Contains("World");           // true
bool b2 = str.Contains("world");           // false(区分大小写)
bool b3 = str.Contains("WORLD", StringComparison.OrdinalIgnoreCase); // true

// StartsWith / EndsWith —— 判断开头/结尾
bool b4 = str.StartsWith("Hello");         // true
bool b5 = str.EndsWith("C#");              // true

// IndexOf —— 查找子字符串/字符首次出现的位置(未找到返回 -1)
int idx1 = str.IndexOf('o');               // 4
int idx2 = str.IndexOf("World");           // 6
int idx3 = str.IndexOf("Hello", 5);        // 13(从索引5开始查找)
int idx4 = str.IndexOf('o', 5);            // 7

// LastIndexOf —— 查找子字符串/字符最后出现的位置
int idx5 = str.LastIndexOf('o');           // 17
int idx6 = str.LastIndexOf("Hello");       // 13

// IndexOfAny —— 查找任意字符首次出现的位置
int idx7 = str.IndexOfAny(new char[] { 'W', 'C' });  // 6

// LastIndexOfAny —— 查找任意字符最后出现的位置
int idx8 = str.LastIndexOfAny(new char[] { 'o', 'l' }); // 18

5.2 提取子字符串

string str = "Hello World";

// Substring —— 提取子字符串
string s1 = str.Substring(6);          // "World"(从索引6到末尾)
string s2 = str.Substring(0, 5);       // "Hello"(从索引0开始,取5个字符)

// 使用范围语法(C# 8+)
string s3 = str[..5];       // "Hello"(从开头到索引5前)
string s4 = str[6..];       // "World"(从索引6到末尾)
string s5 = str[0..5];      // "Hello"
string s6 = str[^5..];      // "World"(末尾5个字符)

5.3 大小写转换

string str = "Hello World";

string upper = str.ToUpper();           // "HELLO WORLD"
string lower = str.ToLower();           // "hello world"

// 使用特定区域设置
string upperTR = str.ToUpper(new System.Globalization.CultureInfo("tr-TR")); // 土耳其语
// 土耳其语中 'i' 的大写是 'İ'(带点的 I),不是 'I'

// Invariant(不依赖特定区域设置,推荐用于数据处理)
string upperInv = str.ToUpperInvariant(); // "HELLO WORLD"

5.4 修剪空白

string str = "   Hello World   ";

// Trim —— 移除首尾空白字符(空格、制表符、换行等)
string s1 = str.Trim();                    // "Hello World"

// TrimStart —— 移除开头空白
string s2 = str.TrimStart();               // "Hello World   "

// TrimEnd —— 移除末尾空白
string s3 = str.TrimEnd();                 // "   Hello World"

// 移除指定字符
string code = "*** ERROR ***";
string s4 = code.Trim('*');                // " ERROR "
string s5 = code.TrimStart('*');           // " ERROR ***"
string s6 = code.TrimEnd('*');             // "*** ERROR "

// 移除指定字符集合
string messy = "###Hello World!!!";
string s7 = messy.Trim('#', '!');          // "Hello World"
string s8 = messy.Trim("#!".ToCharArray());  // "Hello World"(等价写法)

// 空值处理
string nullStr = null;
// string s9 = nullStr.Trim();             // NullReferenceException!
string s9 = nullStr?.Trim();               // null(安全导航操作符 C# 6+)

5.5 替换

string str = "Hello World, Hello C#";

// Replace —— 替换所有出现的字符或字符串
string s1 = str.Replace('o', '0');                  // "Hell0 W0rld, Hell0 C#"
string s2 = str.Replace("Hello", "你好");            // "你好 World, 你好 C#"
string s3 = str.Replace("Hello", "");               // " World,  C#"(相当于删除)

// 注意:Replace 区分大小写
string s4 = str.Replace("hello", "Hi");              // 无变化("Hello World, Hello C#")

// 如果不需要区分大小写,需要用 Regex
string s5 = System.Text.RegularExpressions.Regex.Replace(
    str, "hello", "Hi",
    System.Text.RegularExpressions.RegexOptions.IgnoreCase);

5.6 分割字符串(Split)

// 按字符分割
string str = "苹果,香蕉,橘子,西瓜";
string[] fruits1 = str.Split(',');
// ["苹果", "香蕉", "橘子", "西瓜"]

// 按字符串分割
string str2 = "苹果::香蕉::橘子::西瓜";
string[] fruits2 = str2.Split(new string[] { "::" }, StringSplitOptions.None);

// StringSplitOptions.RemoveEmptyEntries —— 移除空条目
string str3 = "苹果,,香蕉,,,橘子";
string[] fruits3 = str3.Split(',', StringSplitOptions.RemoveEmptyEntries);
// ["苹果", "香蕉", "橘子"](空字符串被移除)

// StringSplitOptions.TrimEntries(C# 8+)—— 去除每个结果的空白
string str4 = " 苹果 , 香蕉 ,  橘子 ";
string[] fruits4 = str4.Split(',', StringSplitOptions.TrimEntries);
// ["苹果", "香蕉", "橘子"]

// 按多个字符分割
string str5 = "苹果,香蕉;橘子|西瓜";
string[] fruits5 = str5.Split(',', ';', '|');
// ["苹果", "香蕉", "橘子", "西瓜"]

// 限制返回数量
string str6 = "A,B,C,D,E,F";
string[] parts1 = str6.Split(',', 3);
// ["A", "B", "C,D,E,F"](最多返回3个子串)

5.7 填充

// PadLeft —— 左填充(向右对齐)
string s1 = "42".PadLeft(5);              // "   42"
string s2 = "42".PadLeft(5, '0');         // "00042"

// PadRight —— 右填充(向左对齐)
string s3 = "42".PadRight(5);             // "42   "
string s4 = "42".PadRight(5, '-');        // "42---"

5.8 插入与移除

string str = "Hello World";

// Insert —— 在指定位置插入字符串
string s1 = str.Insert(5, " C#");          // "Hello C# World"

// Remove —— 从指定位置开始移除若干字符
string s2 = str.Remove(5);                 // "Hello"(从索引5开始移除到最后)
string s3 = str.Remove(5, 6);              // "Hello"(从索引5开始,移除6个字符)

5.9 判断是否为空或空白

// 实例方法(C# 9+ 才有 IsNullOrEmpty / IsNullOrWhiteSpace)
// 在此之前只有静态方法
string str = "";
// str.IsNullOrEmpty(); // 不存在!需要使用静态方法 string.IsNullOrEmpty(str)

5.10 字符数组转换

string str = "Hello";

// ToCharArray —— 转换为字符数组
char[] chars = str.ToCharArray();           // ['H','e','l','l','o']

// 取部分字符
char[] subChars = str.ToCharArray(1, 3);    // ['e','l','l']

六、字符串比较详解

6.1 StringComparison 枚举

// 核心枚举值
public enum StringComparison
{
    CurrentCulture,           // 使用当前区域设置(默认)
    CurrentCultureIgnoreCase, // 使用当前区域设置,忽略大小写
    InvariantCulture,         // 使用固定区域设置
    InvariantCultureIgnoreCase, // 使用固定区域设置,忽略大小写
    Ordinal,                  // 基于码位的比较(最快、最准确)
    OrdinalIgnoreCase         // 基于码位的比较,忽略大小写
}

6.2 比较方式选择指南

// ✅ 推荐:大多数情况下使用 Ordinal(性能最好,结果最一致)
bool eq1 = string.Equals(s1, s2, StringComparison.Ordinal);

// ✅ 面向用户的自然排序(如中文拼音顺序)时使用 CurrentCulture
int cmp1 = string.Compare(s1, s2, StringComparison.CurrentCulture);

// ✅ 序列化、文件路径、标识符比较使用 Ordinal
bool eq2 = path1.Equals(path2, StringComparison.Ordinal);

// ✅ 忽略大小写
bool eq3 = string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);

6.3 比较示例

string a = "Straße";    // 德语 "街道"
string b = "STRASSE";

// 不同比较方式的差异
bool r1 = a.Equals(b, StringComparison.Ordinal);              // false
bool r2 = a.Equals(b, StringComparison.OrdinalIgnoreCase);    // false(码位不同)
bool r3 = a.Equals(b, StringComparison.CurrentCultureIgnoreCase); // true(语言上等价)
bool r4 = a.Equals(b, StringComparison.InvariantCultureIgnoreCase);// true

七、字符串拼接方式与性能对比

7.1 五种拼接方式

string a = "Hello";
string b = "World";

// 方式1:+ 运算符(少量拼接可用)
string s1 = a + " " + b;              // "Hello World"

// 方式2:string.Concat
string s2 = string.Concat(a, " ", b); // "Hello World"

// 方式3:string.Format
string s3 = string.Format("{0} {1}", a, b); // "Hello World"

// 方式4:字符串插值(推荐日常使用)
string s4 = $"{a} {b}";               // "Hello World"

// 方式5:string.Join(连接数组/集合)
string s5 = string.Join(" ", a, b);   // "Hello World"

7.2 += 循环拼接问题

// ❌ 糟糕的做法 —— 每次 += 都会创建新的字符串对象
string result = "";
for (int i = 0; i < 10000; i++)
{
    result += i.ToString();  // 创建 10000 个临时字符串对象!
}

// ✅ 正确做法 —— 使用 StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
    sb.Append(i);
}
string result2 = sb.ToString();  // 只创建一个最终字符串

7.3 编译器优化

// 编译时常量拼接 —— 编译器在编译时完成,无运行时开销
string s = "Hello" + " " + "World";
// 编译后等价于:string s = "Hello World";

// 同一表达式中用 + 拼接多个值 —— 编译器会优化为 string.Concat
string name = "张三";
int age = 25;
string info = "姓名:" + name + ",年龄:" + age;
// 编译后等价于:string info = string.Concat("姓名:", name, ",年龄:", age);

八、StringBuilder 详解

当需要大量字符串拼接操作时,必须使用 StringBuilder。它内部维护一个可变的字符缓冲区,避免创建大量临时对象。

8.1 创建与基本操作

using System.Text;

// 创建(可指定初始容量和最大容量)
var sb1 = new StringBuilder();                    // 默认容量 16
var sb2 = new StringBuilder("Hello");              // 初始内容 "Hello"
var sb3 = new StringBuilder(256);                  // 指定初始容量 256
var sb4 = new StringBuilder("Hello", 256);         // 初始内容 + 容量

// 追加
sb1.Append("Hello");
sb1.Append(' ');                        // 追加字符
sb1.Append("World");
sb1.AppendFormat(" [{0}]", 42);        // 追加格式化内容

// 追加一行(自动添加换行)
sb1.AppendLine();                       // 追加 Environment.NewLine
sb1.AppendLine("第二行内容");

// 插入
sb1.Insert(0, "开头:");

// 替换
sb1.Replace("World", "C#");

// 移除
sb1.Remove(0, 3);                      // 从索引0开始移除3个字符

// 清空
sb1.Clear();                            // 清空所有内容(C# 4+)

// 获取结果
string result = sb1.ToString();

// 属性
int length = sb1.Length;               // 当前字符串长度
int capacity = sb1.Capacity;           // 当前分配的缓冲区大小
int maxCapacity = sb1.MaxCapacity;     // 最大容量
char ch = sb1[0];                      // 索引器(支持读写)
sb1[0] = 'H';                          // 可以直接修改指定位置字符

8.2 性能最佳实践

// ✅ 预估算容量 —— 避免频繁扩容
var sb = new StringBuilder(estimatedLength: 10000);

// ✅ 链式调用
sb.Append("SELECT ")
  .Append("Id, Name, Age ")
  .Append("FROM Users ")
  .Append("WHERE Age > 18");

// ✅ 使用 AppendFormat(比多次 Append 更简洁)
sb.AppendFormat("用户 {0},年龄 {1},城市 {2}", name, age, city);

// ✅ 获取迭代器中的 StringBuilder
// 通过 string.Join 可以直接拼装(内部实现已经优化)
string result = string.Join(", ", collection);

8.3 StringBuilder 容量管理

var sb = new StringBuilder();
Console.WriteLine(sb.Capacity);      // 初始容量:16

sb.Append("12345678901234567");      // 超过 16 字符
Console.WriteLine(sb.Capacity);      // 会自动扩容(通常是翻倍)

// 扩容规则:新容量 = max(旧容量 * 2, 需要的最小容量)

// 手动设置容量
sb.Capacity = 100;                   // 手动扩容(如果小于当前长度则抛出异常)

// 确保容量
sb.EnsureCapacity(5000);             // 确保至少有 5000 的容量

九、字符串驻留(String Interning)

9.1 原理

CLR 维护一个字符串驻留池(Intern Pool),用于存储程序中唯一的字符串字面量。相同内容的字符串字面量在内存中只存一份。

// 字符串字面量自动驻留
string s1 = "Hello";
string s2 = "Hello";

// 验证:s1 和 s2 引用的是同一个对象
bool same = object.ReferenceEquals(s1, s2);  // true!

// 运行时创建的字符串不会自动驻留
string s3 = new string(new char[] { 'H', 'e', 'l', 'l', 'o' });
bool same2 = object.ReferenceEquals(s1, s3); // false

// 手动驻留
string s4 = string.Intern(s3);
bool same3 = object.ReferenceEquals(s1, s4); // true

9.2 驻留的注意事项

// ⚠️ 驻留池中的字符串不会被 GC 回收(直到 AppDomain 卸载)
// 不要滥用 string.Intern()!

// ✅ 适合驻留的场景:大量重复的标识符、配置键、枚举名等
// ❌ 不适合驻留的场景:用户输入、动态生成的唯一字符串

// IsInterned —— 安全地检查并获取驻留引用(不会自动驻留)
string s = string.IsInterned("Hello");  // 如果存在则返回引用,否则返回 null

十、字符串编码

10.1 C# 内部编码

C# 的 stringchar 使用 UTF-16 编码。每个 char 占 2 个字节。

string str = "Hello你好";

// 字节数 ≠ 字符数
int charCount = str.Length;  // 7 个字符
int byteCount = System.Text.Encoding.Unicode.GetByteCount(str); // 14 字节

// 不同编码的字节占用
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str);   // 11 字节(英文1字节,中文3字节)
byte[] utf16 = System.Text.Encoding.Unicode.GetByteCount(str); // 14 字节
byte[] utf32 = System.Text.Encoding.UTF32.GetBytes(str); // 28 字节

10.2 字符串与字节数组互转

string original = "Hello 世界";

// 字符串 → 字节数组
byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(original);
byte[] gbkBytes = System.Text.Encoding.GetEncoding("GB2312").GetBytes(original);

// 字节数组 → 字符串
string utf8Str = System.Text.Encoding.UTF8.GetString(utf8Bytes);
string gbkStr = System.Text.Encoding.GetEncoding("GB2312").GetString(gbkBytes);

// Base64 编码(常用于网络传输)
string base64 = Convert.ToBase64String(utf8Bytes);
byte[] decodedBytes = Convert.FromBase64String(base64);
string decodedStr = System.Text.Encoding.UTF8.GetString(decodedBytes);

10.3 代理项对(Surrogate Pairs)

// 某些 Unicode 字符(如 Emoji)超出基本多语言平面(BMP),
// 需要使用两个 char 表示(代理项对)

string emoji = "😀";
Console.WriteLine(emoji.Length);  // 2!(不是 1)

// ✅ 正确处理代理项对的方式
int realCharCount = System.Globalization.StringInfo.ParseCombiningCharacters(emoji).Length; // 1

// 或使用 Rune 类型(C# 8+ / .NET Core 3+)
// foreach (System.Text.Rune rune in emoji.EnumerateRunes()) { ... }

十一、字符串与 null / 空字符串

11.1 区别对比

string nullStr = null;      // 没有引用任何对象
string emptyStr = "";       // 引用了空字符串对象(长度为0)
string emptyStr2 = string.Empty;  // 与 "" 完全等价

// 行为差异
nullStr.Length;             // NullReferenceException!
emptyStr.Length;            // 0(正常)
nullStr.ToString();         // NullReferenceException!
emptyStr.ToString();        // ""(正常)
string.IsNullOrEmpty(nullStr);   // true
string.IsNullOrEmpty(emptyStr);  // true

// 拼接时的差异
string s1 = nullStr + "World";   // "World"(null 被当作空字符串处理)
string s2 = emptyStr + "World";  // "World"

11.2 安全处理最佳实践

// ✅ 接收外部输入时始终检查
public string Process(string input)
{
    // 防御性编程
    if (string.IsNullOrEmpty(input))
    {
        return string.Empty;  // 或抛出异常,取决于业务需求
    }

    // 对于可空的参数
    return input?.Trim()?.ToUpper() ?? string.Empty;
}

// ✅ 集合中的字符串处理
List<string> names = GetNames();
var validNames = names
    .Where(n => !string.IsNullOrWhiteSpace(n))
    .Select(n => n.Trim())
    .ToList();

十二、字符串与 LINQ

string str = "Hello World";

// 统计字符
int count = str.Count(c => c == 'l');              // 3

// 筛选字符
string lettersOnly = new string(str.Where(char.IsLetter).ToArray());  // "HelloWorld"

// 转换
string reversed = new string(str.Reverse().ToArray());  // "dlroW olleH"

// 去重
string unique = new string(str.Distinct().ToArray());   // "Helo Wrd"

// 判断是否全是数字
bool isAllDigit = str.All(char.IsDigit);

十三、正则表达式与字符串

using System.Text.RegularExpressions;

string input = "电话:13812345678,邮箱:test@example.com,日期:2024-01-15";

// 匹配
bool isMatch = Regex.IsMatch(input, @"\d{3}-\d{8}");  // false(格式不匹配)

// 搜索
Match match = Regex.Match(input, @"\d{11}");           // 匹配手机号
if (match.Success)
    Console.WriteLine(match.Value);                     // "13812345678"

// 查找所有匹配
MatchCollection matches = Regex.Matches(input, @"\w+@\w+\.\w+");
foreach (Match m in matches)
    Console.WriteLine(m.Value);                         // "test@example.com"

// 替换
string replaced = Regex.Replace(input, @"\d", "*");    // "电话:***********,邮箱:test@example.com,日期:****-**-**"

// 分割
string[] parts = Regex.Split("one,two;three four", @"[;, ]");

// 验证常见格式
bool isEmail = Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
bool isMobile = Regex.IsMatch(mobile, @"^1[3-9]\d{9}$");
bool isIdCard = Regex.IsMatch(idCard, @"^\d{17}[\dXx]$");

十四、完整的代码示例

示例一:字符串处理辅助类

public static class StringHelper
{
    /// <summary>
    /// 安全截取字符串(超出长度不抛异常,加省略号)
    /// </summary>
    public static string Truncate(string input, int maxLength, string suffix = "...")
    {
        if (string.IsNullOrEmpty(input)) return input;
        if (input.Length <= maxLength) return input;
        return input.Substring(0, maxLength) + suffix;
    }

    /// <summary>
    /// 反转字符串
    /// </summary>
    public static string Reverse(string input)
    {
        if (string.IsNullOrEmpty(input)) return input;
        char[] chars = input.ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }

    /// <summary>
    /// 判断是否为回文字符串
    /// </summary>
    public static bool IsPalindrome(string input)
    {
        if (string.IsNullOrEmpty(input)) return false;
        string cleaned = new string(input.Where(char.IsLetterOrDigit).ToArray()).ToLower();
        return cleaned == Reverse(cleaned);
    }

    /// <summary>
    /// 驼峰命名转下划线命名
    /// </summary>
    public static string ToSnakeCase(string input)
    {
        if (string.IsNullOrEmpty(input)) return input;
        return string.Concat(input.Select((c, i) =>
            i > 0 && char.IsUpper(c) ? "_" + c : c.ToString())).ToLower();
    }

    /// <summary>
    /// 统计单词数量
    /// </summary>
    public static int WordCount(string input)
    {
        if (string.IsNullOrWhiteSpace(input)) return 0;
        return input.Split(new char[] { ' ', '\t', '\n', '\r' },
            StringSplitOptions.RemoveEmptyEntries).Length;
    }

    /// <summary>
    /// 隐藏敏感信息(如手机号中间 4 位)
    /// </summary>
    public static string Mask(string input, int startIndex, int length, char maskChar = '*')
    {
        if (string.IsNullOrEmpty(input) || startIndex >= input.Length) return input;

        int actualLength = Math.Min(length, input.Length - startIndex);
        string mask = new string(maskChar, actualLength);
        return input.Remove(startIndex, actualLength).Insert(startIndex, mask);
    }
}

// 使用示例
class Program
{
    static void Main()
    {
        string text = "Hello World, C# Programming";

        Console.WriteLine(StringHelper.Truncate(text, 15));        // "Hello World, C..."
        Console.WriteLine(StringHelper.Reverse(text));             // "gnimmargorP #C ,dlroW olleH"
        Console.WriteLine(StringHelper.IsPalindrome("racecar"));   // True
        Console.WriteLine(StringHelper.ToSnakeCase("HelloWorld")); // "hello_world"
        Console.WriteLine(StringHelper.WordCount(text));           // 4
        Console.WriteLine(StringHelper.Mask("13812345678", 3, 4)); // "138****5678"
    }
}

示例二:CSV 解析器

public class SimpleCsvParser
{
    public static List<string[]> Parse(string csvContent)
    {
        var result = new List<string[]>();
        if (string.IsNullOrWhiteSpace(csvContent)) return result;

        string[] lines = csvContent.Split(
            new[] { "\r\n", "\r", "\n" },
            StringSplitOptions.RemoveEmptyEntries);

        foreach (string line in lines)
        {
            string[] fields = ParseLine(line);
            result.Add(fields);
        }
        return result;
    }

    private static string[] ParseLine(string line)
    {
        var fields = new List<string>();
        bool inQuotes = false;
        var currentField = new System.Text.StringBuilder();

        for (int i = 0; i < line.Length; i++)
        {
            char c = line[i];

            if (c == '"')
            {
                if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
                {
                    // 转义引号
                    currentField.Append('"');
                    i++;
                }
                else
                {
                    inQuotes = !inQuotes;
                }
            }
            else if (c == ',' && !inQuotes)
            {
                fields.Add(currentField.ToString().Trim());
                currentField.Clear();
            }
            else
            {
                currentField.Append(c);
            }
        }
        fields.Add(currentField.ToString().Trim());
        return fields.ToArray();
    }
}

十五、常见陷阱与最佳实践

15.1 陷阱

陷阱 错误示例 正确做法
循环中使用 += for(...) { s += i; } 使用 StringBuilder
使用 == 比较 null 不会编译,但逻辑上注意 使用 string.IsNullOrEmpty()
混淆空字符串和 null str.Length 未判空 先判断再使用
频繁使用 ToUpper/ToLower 比较 if(str.ToLower()=="abc") 使用 StringComparison.OrdinalIgnoreCase
未考虑编码问题 直接 System.Text.Encoding.Default 明确指定编码,推荐 UTF-8
字符串格式化时区域依赖 ToString("C") 在不同机器结果不同 指定 CultureInfo 或使用 Invariant

15.2 最佳实践清单

// ✅ 1. 使用 string.Empty 而不是 ""
string s1 = string.Empty;  // 推荐
string s2 = "";             // 也可以,但不如前者语义清晰

// ✅ 2. 判空使用 IsNullOrEmpty / IsNullOrWhiteSpace
if (!string.IsNullOrWhiteSpace(input))
{
    // 安全使用 input
}

// ✅ 3. 比较时明确指定 StringComparison
if (s1.Equals(s2, StringComparison.OrdinalIgnoreCase)) { }
if (s1.Contains(s2, StringComparison.Ordinal)) { }

// ✅ 4. 大量拼接使用 StringBuilder
var sb = new StringBuilder(capacity);
for (...) { sb.Append(item); }
string result = sb.ToString();

// ✅ 5. 数组拼接优先使用 string.Join
string result = string.Join(", ", items);

// ✅ 6. 少量拼接使用字符串插值
string result = $"{firstName} {lastName}, Age: {age}";

// ✅ 7. 使用 nameof 避免硬编码字符串
Console.WriteLine(nameof(MyMethod));  // "MyMethod" —— 重构安全

// ✅ 8. 对日志中的敏感信息脱敏
string safeInfo = Mask(phoneNumber, 3, 4);

// ✅ 9. 在合适的时候使用 Span<char> 减少分配(高级)
ReadOnlySpan<char> span = str.AsSpan();

十六、字符串内存分析

16.1 垃圾回收与字符串

// 临时字符串会快速变为垃圾,增加 GC 压力
string ProcessData(List<string> items)
{
    // ❌ 每个 items 元素拼接都产生大量中间字符串
    string result = "";
    foreach (var item in items)
    {
        if (!string.IsNullOrEmpty(item))
            result += item.Trim().ToUpper() + ",";  // 多次分配!
    }
    return result;
}

// ✅ 优化版本
string ProcessDataOptimized(List<string> items)
{
    var sb = new StringBuilder(items.Count * 10);  // 预估容量
    foreach (var item in items)
    {
        if (!string.IsNullOrWhiteSpace(item))
        {
            sb.Append(item.AsSpan().Trim());  // 使用 Span 减少分配
            sb.Append(item.ToUpperInvariant());
            sb.Append(',');
        }
    }
    if (sb.Length > 0) sb.Length--;  // 移除最后的逗号
    return sb.ToString();
}

16.2 对象头开销

// 每个 String 对象在 64 位 .NET 中有约 26 字节的对象开销:
// - 对象头(SyncBlock 索引):8 字节
// - 方法表指针:8 字节
// - 长度字段:4 字节
// - 字符数组开始前的 padding:约 4 字节
// - 空终止符(用于互操作):2 字节
// 加上实际字符数据(每个字符 2 字节)

// 所以空字符串 "" 实际占用约 28 字节内存
// "Hello"(5 字符)占用约 28 + 10 = 38 字节

十七、总结

特性 说明
类型 引用类型,System.String,别名 string
不可变 创建后不可修改,所有"修改"操作都返回新对象
字符编码 内部 UTF-16,每个 char 占 2 字节
比较 == 比较内容,推荐使用 StringComparison.Ordinal
驻留 字符串字面量自动驻留,运行时创建的字符串不会
空值处理 使用 IsNullOrEmpty / IsNullOrWhiteSpace 判空
拼接 少量用 + / 插值,大量用 StringBuilder
格式化 string.Format$ 插值、StringBuilder.AppendFormat
性能关键点 预估容量、避免循环拼接、选择合适的比较方式

学习建议:学习 String 类的最佳方式是实际动手编码。建议将上述每个方法都写一个小程序验证运行,加深理解。特别要理解不可变性带来的性能影响,以及 StringBuilder 的使用场景。

0
博主关闭了当前页面的评论