目录
[1. 指定位置获取字符](#1. 指定位置获取字符)
[2. 正向/反向查找字符位置](#2. 正向/反向查找字符位置)
[3. 移除指定位置后的字符](#3. 移除指定位置后的字符)
[4. 替换特定内容](#4. 替换特定内容)
[5. 大小写转换](#5. 大小写转换)
[6. 截取部分字符串](#6. 截取部分字符串)
[7. 分割字符串](#7. 分割字符串)
[StringBuilder 的基本使用方法](#StringBuilder 的基本使用方法)
[1. 创建 StringBuilder 对象](#1. 创建 StringBuilder 对象)
[2. 追加内容:Append()](#2. 追加内容:Append())
[3. 插入内容:Insert()](#3. 插入内容:Insert())
[4. 删除内容:Remove()](#4. 删除内容:Remove())
[5. 替换内容:Replace()](#5. 替换内容:Replace())
[6. 清空内容:Clear()](#6. 清空内容:Clear())
[7. 追加特殊内容](#7. 追加特殊内容)
[8. 获取特定长度的子串](#8. 获取特定长度的子串)
[9. 获取容量信息](#9. 获取容量信息)
前言

字符串在C#中看似简单,实则涉及不可变性、驻留机制、编码格式等核心概念。在Unity开发中滥用字符串操作可能引发GC压力、内存碎片等问题。
本文将介绍字符串的基本使用方式以及梳理字符串常量池(string.Intern)与字符串拼接优化(StringBuilder),并探讨Span<T>等新特性对字符串处理的革新。
string的基本使用方法
1. 指定位置获取字符
cs
string sweet = "抹茶蛋糕";
// 字符串本质是char数组
char[] chars = sweet.ToCharArray();
char berry = sweet[2]; // 获取第3个字符 → '蛋'
// 记得下标从0开始
2. 正向/反向查找字符位置
cs
string target = "Unity大魔王在哪里?Unity在这里!";
int frontHunt = target.IndexOf("Unity"); // 正向:位置0
int backHunt = target.LastIndexOf("Unity"); // 反向:位置15
int notFound = target.IndexOf("Unreal"); // 找不到返回-1
3. 移除指定位置后的字符
cs
// 移除后面的内容
string phone = "电话:13800138000";
string text = phone.Remove(3); // 移除数字部分 → "电话:"
// 移除中间指定部分
string address = "北京市海淀区中关村";
string area = address.Remove(6, 3); // 6位置开始移除3字符 → "北京市海淀区"
4. 替换特定内容
cs
string message = "今天是周一,天气晴";
string newMsg = message.Replace("周一", "周三"); // → "今天是周三,天气晴"
// 安全处理文本
string input = "A<>B";
string safe = input.Replace("<", "<").Replace(">", ">");
// → "A<>B"
5. 大小写转换
cs
string welcome = "Hello World";
// 转大写
Console.WriteLine(welcome.ToUpper()); // → "HELLO WORLD"
// 转小写
Console.WriteLine(welcome.ToLower()); // → "hello world"
6. 截取部分字符串
cs
// 提取文件名(无后缀)
string file = "report_2023.pdf";
int dotIndex = file.LastIndexOf('.');
string name = file.Substring(0, dotIndex); // → "report_2023"
// 截取中间段文本
string code = "[1001]王同学";
string student = code.Substring(5, 3); // 从5位取3字 → "王同学"
7. 分割字符串
cs
// 分割简单列表
string fruits = "苹果,香蕉,橙子";
string[] fruitList = fruits.Split(',');
// 得到: ["苹果", "香蕉", "橙子"]
// 用户输入解析
string input = "李白 男 25";
string[] data = input.Split(' ');
Console.WriteLine($"姓名:{data[0]} 性别:{data[1]} 年龄:{data[2]}");
8.字符串替换
cs
string template = "尊敬的@姓名,您的订单@订单号已发货";
string message = template.Replace("@姓名", "张三")
.Replace("@订单号", "DH20230528001");
Console.WriteLine(message);
// 输出: 尊敬的张三,您的订单DH20230528001已发货
默认会替换字符串中的所有匹配项
cs
string text = "a a a";
Console.WriteLine(text.Replace("a", "b")); // b b b
Replace方法默认区分大小写
cs
string text = "Apple apple";
Console.WriteLine(text.Replace("apple", "fruit"));
// 输出: Apple fruit
与正则表达式搭配使用
cs
using System.Text.RegularExpressions;
string text = "订单号:12345,金额:¥100";
// 替换所有数字
string masked = Regex.Replace(text, @"\d+", "***");
Console.WriteLine(masked);
// 输出: 订单号:***,金额:¥***
StringBuilder 的基本使用方法
1. 创建 StringBuilder 对象
cs
// 最简单的创建方式
StringBuilder sb = new StringBuilder();
// 创建时指定初始容量(避免频繁扩容)
StringBuilder report = new StringBuilder(200);
// 创建时包含初始内容
StringBuilder greeting = new StringBuilder("您好,");
2. 追加内容:Append()
cs
// 追加字符串
sb.Append("小铃的主人");
// 追加数值(自动转换为字符串)
sb.Append(2023);
// 追加布尔值
sb.Append(true);
// 追加格式化的日期
sb.AppendFormat("今天是 {0:yyyy-MM-dd}", DateTime.Now);
Console.WriteLine(sb.ToString());
// 输出: 小铃的主人2023True今天是 2023-07-15
3. 插入内容:Insert()
cs
// 在索引位置插入内容
StringBuilder code = new StringBuilder("12345");
code.Insert(2, "ABC");
// 结果: 12ABC345
// 开头插入
code.Insert(0, "Prefix:");
// 结果: Prefix:12ABC345
4. 删除内容:Remove()
cs
// 移除索引位置开始的字符
StringBuilder address = new StringBuilder("北京市海淀区中关村");
address.Remove(3, 3); // 从索引3开始移除3个字符
// 结果: 北京市中关村
5. 替换内容:Replace()
cs
// 全局替换方法
StringBuilder msg = new StringBuilder("用户名:guest, 登录次数:3");
msg.Replace("guest", "master");
// 结果: 用户名:master, 登录次数:3
// 指定替换范围
msg.Replace("3", "1000", 17, 2);
// 结果: 用户名:master, 登录次数:1000
6. 清空内容:Clear()
cs
// 完全清空
sb.Clear();
7. 追加特殊内容
cs
// 追加换行
StringBuilder log = new StringBuilder();
log.AppendLine("[日志开始]");
// 追加带缩进的代码
log.Append("\t").AppendLine("Debug: 用户登录成功");
// 追加JSON内容
log.AppendLine("\t{ \"userId\": 1001 }");
Console.WriteLine(log);
/* 输出:
[日志开始]
Debug: 用户登录成功
{ "userId": 1001 }
*/
8. 获取特定长度的子串
cs
StringBuilder longText = new StringBuilder("0123456789ABCDEF");
// 截取索引3开始,长度为5的子串
string part = longText.ToString(3, 5);
// 结果: "34567"
9. 获取容量信息
cs
StringBuilder buffer = new StringBuilder(50);
Console.WriteLine($"初始容量: {buffer.Capacity}"); // 50
Console.WriteLine($"当前长度: {buffer.Length}"); // 0
buffer.Append("Hello");
Console.WriteLine($"当前容量: {buffer.Capacity}"); // 50 (保持原值)
string与StringBuilder核心区别
1.不可变性
- string:是不可变(immutable)的,一旦创建,其内容就不能被修改。每次修改(如连接、替换、插入)都会创建新的字符串对象。
- StringBuilder:是可变的,内部维护一个字符数组(字符缓冲区),可以在原对象上直接修改,不会频繁创建新对象。
2.性能表现
- string:在少量修改时性能尚可,但频繁修改(如循环追加)会导致大量内存分配和垃圾回收,严重影响性能。
- StringBuilder:在频繁修改字符串的场景下性能卓越,尤其在长字符串多次操作时,能显著减少内存分配和GC压力。
3.内存使用
- string:每次修改产生新字符串,旧字符串成为垃圾,增加GC负担。
- StringBuilder:内部动态扩充缓冲区,当长度超过当前容量时才分配新的更大数组(通常是倍增),内存利用率高。
4.方法功能
- string:提供各种静态方法(如Format, Join, IsNullOrEmpty)和实例方法(如Replace, Substring, Contains),但这些操作都返回新字符串。
- StringBuilder:提供直接修改缓冲区的方法(Append, Insert, Remove, Replace)并且这些方法都返回自身,便于链式调用,但没有一些辅助方法(如Substring, Compare等)。
5.使用场景
- string:适用于少量字符串修改(如格式化一个字符串、常量字符串操作)、作为值传递(线程安全)、键值(如字典键)使用。
- StringBuilder:适用于需要构建大量字符串(如循环中拼接)、动态生成长文本(如生成HTML、XML或JSON)、多次修改字符串内容(特别是在循环体内)。
6.线程安全
- String:由于字符串是不可变的,任何修改操作都会生成新的字符串实例,因此多个线程同时访问同一字符串对象是安全的,不存在竞态条件或数据不一致问题。
- StringBuilder:设计上不是线程安全的,其内部状态(如字符数组、长度等)可能在多线程环境下被并发修改,导致数据损坏或异常。
Span<T>
Span<T> 是 .NET Core 2.1 引入的一种高性能内存视图类型,提供对连续内存区域的类型安全访问。它支持栈分配或托管堆内存的引用,避免不必要的内存分配和复制。对于字符串处理,Span<char> 可直接操作字符串的底层内存,显著提升性能。
1.字符串切片的高效实现
传统字符串操作(如 Substring)会创建新字符串对象,而 Span<char> 通过切片(Slice)返回原字符串的视图,无需分配内存。例如:
cs
string text = "Hello, World!";
Span<char> span = text.AsSpan();
Span<char> slice = span.Slice(7, 5); // "World"(无新分配)
2.避免编码转换开销
处理字节流时,Span<byte> 可直接与 Span<char> 交互,减少 Encoding.UTF8.GetString 等方法的调用。例如解析网络数据时:
cs
Span<byte> buffer = stackalloc byte[128];
Span<char> chars = MemoryMarshal.Cast<byte, char>(buffer);
3.与正则表达式的结合优化
.NET 7 引入了 Regex 对 Span<char> 的支持,避免生成中间字符串。例如:
cs
Span<char> input = "2023-01-01".AsSpan();
if (Regex.IsMatch(input, @"\d{4}-\d{2}-\d{2}")) { ... }
4.字符串处理的零分配模式
通过 stackalloc 和 Span<T> 实现完全栈上操作:
cs
Span<char> buffer = stackalloc char[64];
int length = "123".AsSpan().TryCopyTo(buffer); // 无堆分配
5.与管道(Pipeline)模型的集成
在 System.IO.Pipelines 中,Span<byte> 直接处理 I/O 缓冲区,避免 byte[] 分配。例如处理文件流时:
cs
PipeReader reader = ...;
ReadResult result = await reader.ReadAsync();
Span<byte> data = result.Buffer.FirstSpan;