CSharp(二十一)String 类详解
一、概述与定义
String 是 C# 中最常用的类型之一,位于 System 命名空间下,表示不可变的 Unicode 字符序列。在 C# 中,string 关键字是 System.String 的别名,两者完全等价。
string s1 = "Hello";
// 完全等价于
System.String s2 = "Hello";
核心特性:不可变性(Immutability)
String 对象一旦创建,其值就永远无法改变。 所有看似"修改"字符串的操作(如 Replace、Substring、ToUpper 等),实际上都会创建一个全新的字符串对象,原始字符串保持不变。
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# 的 string 和 char 使用 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的使用场景。