首页 / C# 入门教程 / 字符串常用操作

C# 入门教程

字符串常用操作

本教程共 100 篇 · 第 14 篇 · 更新于 2026-07-31 · 约 9 分钟阅读

C#C# 入门教程字符串SubstringStringBuilder格式化

14. 字符串常用操作

本节目标:能对字符串做拼接、截取、比较和格式化,并理解为什么大量拼接要用 StringBuilder。

上一章讲”怎么写”字符串,这章讲”怎么用”字符串。日常开发里,文本的增删改查几乎无处不在。

先记住:字符串不可变

这是理解一切操作的地基:字符串创建后就不能改。所谓”改字符串”,其实是生成了一个新字符串,原串还在原地。

namespace CSharpDemo;

string s = "hello";
s.ToUpper();              // 不改变 s
Console.WriteLine(s);    // 仍是 hello
string upper = s.ToUpper();
Console.WriteLine(upper); // HELLO(新字符串)

ToUpper 返回新串,原 s 不受影响。所以任何”修改”都要接住返回值。

Note

因为不可变,字符串可以放心地在多处共享,不用担心被谁偷偷改掉。但代价是:反复”小幅修改”会产生大量临时串,性能堪忧——这正是 StringBuilder 存在的理由(见后文)。

为什么设计成不可变

不可变听起来不方便,其实是刻意为之。好处有三点:一是可安全共享,多个地方引用同一串,谁都不必担心被别人篡改;二是线程安全,多线程同时读不会出竞争;三是能当字典键(key),因为内容不变,哈希码才稳定。代价只是”频繁修改”时性能差,而这个问题用 StringBuilder 专门解决。所以不可变不是缺点,而是用一处不便换来了全局的稳妥。

拼接:把多段连起来

最简单用 + 或插值 $

string a = "你好";
string b = "世界";
string c = a + "," + b;          // 你好,世界
string d = $"{a}{b}";           // 同样结果,推荐

少量拼接用插值最清晰。要拼一个集合,用 string.Join 更合适:

List<string> names = ["小明", "小红", "小刚"];
string all = string.Join("、", names);
Console.WriteLine(all);    // 小明、小红、小刚

string.Join(分隔符, 集合) 自动在元素间插入分隔符,省去手动判断。

Tip

三两段文本用插值 $"..." 最直观;可是一个集合的元素挨个拼,就别手写循环了,string.Join 一句话搞定,还自动处理”最后一项不加分隔符”的细节。

截取:Substring

Substring 从指定位置取出一段。参数一是起始索引(从 0 数),参数二是长度:

string text = "HelloWorld";
string part = text.Substring(0, 5);   // 从 0 取 5 个 → Hello
string tail = text.Substring(5);      // 不写长度则取到末尾 → World
Console.WriteLine(part);
Tip

起始索引从 0 开始,这是编程惯例。Substring(0,5) 取的是第 0 到第 4 个字符,共 5 个,不是到第 5 个。

Warning

Substring 的起始位置若超出字符串长度,或长度算过头,会抛 ArgumentOutOfRangeException。处理用户输入、截取域名这类场景,动手前先确认长度够不够,必要时用 Math.Min 限制长度,避免运行到一半崩掉。

比较:别只用 ==

判断两个字符串内容是否相同,C# 里 ==逐字符比较内容(不是比地址),可以放心用:

string x = "abc";
string y = "abc";
Console.WriteLine(x == y);     // True,比的是内容

但若要考虑大小写、文化区域,就得用 Equals 并指定规则:

bool eq = "ABC".Equals("abc", StringComparison.OrdinalIgnoreCase);
Console.WriteLine(eq);   // True,忽略大小写

StringComparison.OrdinalIgnoreCase 表示”按字节比、忽略大小写”,处理用户名、协议字段时常用。

查找与替换

string text = "hello world";
bool has = text.Contains("world");     // True,是否包含子串
int idx = text.IndexOf("world");        // 6,子串起始位置
string replaced = text.Replace("world", "C#"); // hello C#
Console.WriteLine($"{has}, {idx}, {replaced}");

Contains 问”有没有”,IndexOf 答”在哪”,Replace 生成替换后的新串。

Note

IndexOf 找不到时返回 -1,不是抛异常。所以判断”是否包含”用 Contains 更直白;要拿到位置再操作,才用 IndexOf 并检查是否 >= 0

格式化:ToString 与 string.Format

数字、日期转成特定格式,用 ToString("格式符")

decimal price = 9.99m;
Console.WriteLine(price.ToString("C"));     // 按货币格式,如 ¥9.99
int n = 1234;
Console.WriteLine(n.ToString("D5"));        // 001234,补零到 5 位

string.Format 则像不用 $ 的插值,用 {0} {1} 占位:

string s = string.Format("姓名:{0},年龄:{1}", "小明", 18);
Console.WriteLine(s);

现代写法更推荐插值 $"...",但 string.Format 在动态拼接模板时仍有用。

几种格式化方式怎么选

场景推荐
简单变量插入文本插值 $"..."
模板要从配置/资源读string.Format 占位符
数字/日期特定格式ToString("格式符")

三者的输出一致,差异在”写法便利”和”模板来源”。日常首选插值,因为它能在编译期检查变量名。

大量拼接:用 StringBuilder

当要在循环里拼成百上千段时,别用 + 或插值——每次都生成新串,内存和速度都吃亏。用 StringBuilder

using System.Text;   // StringBuilder 在此命名空间

var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append("行").Append(i).AppendLine();
}
string result = sb.ToString();   // 最后一次性转成 string
Console.WriteLine(result.Length);

StringBuilder 在内部管理一个可变的缓冲区,循环里只往里”追加”,最后 ToString() 一次性产出。拼接量越大,优势越明显。

Note

本教程前面说过”常用命名空间已隐式全局 using”。但 System.Text 不在默认集合里,用 StringBuilder 时要自己加 using System.Text;。这是少数需要手动引入的例子之一。

拆分:把一串文字切开

和拼接相反,Split 按分隔符把一个字符串拆成多段,得到数组:

string csv = "小明,小红,小刚";
string[] parts = csv.Split(',');
foreach (var p in parts)
{
    Console.WriteLine(p);
}

Split(',') 在逗号处切开,得到三个名字。读 CSV、解析用户输入时极常用。还能按多个分隔符、按换行拆,进阶时再展开。

去除空白:Trim

用户输入常带首尾空格,Trim 帮你清理:

string input = "  hello  ";
Console.WriteLine(input.Trim());        // "hello"
Console.WriteLine(input.TrimStart());   // "hello  "
Console.WriteLine(input.TrimEnd());     // "  hello"

Trim 去两端,TrimStart/TrimEnd 只去一边。配合 Trim() 再判断是否为空,是处理表单输入的标配动作。

字符串很”重”吗

单个字符串很轻。问题出在”反复修改”:因为不可变,每次 Replace+ 都生新串,旧串等回收。做一次两次无所谓,在万次循环里就是性能隐患——这正是上一章说的 StringBuilder 出场时机。

Tip

判断该不该用 StringBuilder 的简单标准:拼接次数是否很大(比如成百上千)或在循环里。是就用它;只是拼两三段的普通代码,插值 $"..." 就够清晰又好用,不必过度优化。

常见疑问解答

问:为什么改了字符串却没变化? 因为字符串不可变,像 s.ToUpper() 返回的是新串,原 s 没动。务必用 s = s.ToUpper() 接住返回值,否则”修改”白改。这是字符串操作第一坑。

问:Equals 和 == 在字符串上有区别吗?string 上两者都按内容比较,通常等价。但 Equals 可指定比较规则(如忽略大小写、按区域),更灵活;== 写法更短。需要特殊规则时用 Equals(... , StringComparison)

问:拼接几百个字符串用 + 行不行? 少量拼接没问题;但成百上千次在循环里 +,会反复生成临时串,又慢又费内存。此时务必换 StringBuilder。判断标准就一条:是否在循环里大量拼接。

要点速记

  • 字符串不可变,修改要接住返回值。
  • 拼接用 $"...";集合用 string.Join
  • 截取 Substring、查找 Contains/IndexOf、替换 Replace
  • 大量拼接换 StringBuilder,需 using System.Text

动手练一练

  1. string.Join("、", names)List<string> names = ["甲","乙","丙"] 拼成一句,体会比循环拼接省事。
  2. Substring 从一个邮箱字符串里取出 @ 之后的域名部分。
  3. StringBuilder 在循环里追加 100 行文字,最后 ToString 打印长度,对比若用 + 拼接的思路差异。

不可变的真实代价

前面说”字符串不可变”,代价在大量修改时显现。比如循环里反复 +=

string s = "";
for (int i = 0; i < 10000; i++)
{
    s += i;   // 每次都生成新串,旧的等回收
}

这一万次拼接,会创建上万个临时字符串,既占内存又慢。改用 StringBuilder 后,只在内部缓冲区追加,最后一次性产出,性能天差地别。

Note

不可变不是缺点,而是安全性的来源(可放心共享、线程安全)。它只是要求你在”频繁修改”场景换对工具。日常少量拼接,插值 $ 完全够用,不必过早优化。

字符串比较里的文化陷阱

"abc" == "ABC" 在 C# 里是 False(区分大小写)。但某些语言区域下,字母等价规则不同。处理用户名、协议字段这类”非自然语言”文本,务必用 StringComparison.Ordinal 明确按字节比,别让系统区域影响结果。

bool same = "abc".Equals("ABC", StringComparison.Ordinal);     // False
bool ignore = "abc".Equals("ABC", StringComparison.OrdinalIgnoreCase); // True

记一条经验:比”展示给人看”的文字,可以容忍区域差异;比”机器协议/标识”,一律用 Ordinal 系列,结果才稳定。

小结与下一步

字符串不可变,所以”修改”都要接住返回值。日常用插值拼接、Substring 截取、Equals 比较、ToString 格式化;循环里大量拼接换 StringBuilder。这些操作是文本处理的基本功。一个核心直觉:字符串像刻在石头上的字,改它只能重刻一块新的——理解这点,所有”修改无效”的困惑都迎刃而解。

下一章进入运算:算术、关系、逻辑、位运算,以及谁先算的优先级。