C# 系列教程_12 字符串 string 保姆级系统教程(面向新手,大量示例 + 原理讲解)

前置小说明: C# 里有两个概念:string 和 String

  • string:C# 关键字,是 System.String 的简写(日常写代码优先用小写 string)

  • String:是 .NET 类库中的完整类名 System.String 两者完全同一个东西,只是写法区别。

    string s1 = "hello";
    String s2 = "hello";

上面两行代码效果一模一样。
最重要核心特性(必须先记住,整个字符串体系都建立在这个基础上 ) ✅ string 是【不可变(immutable)】的引用类型 含义:字符串一旦创建,里面的字符就不能修改。任何看起来 "修改字符串" 的操作,本质都是新建一个字符串对象,旧字符串留在内存等待垃圾回收。 这是新手最容易踩坑的点,后面所有例子都会反复验证这条规则。


一、字符串的声明与创建

1. 直接字面量创建(最常用)

cs 复制代码
using System;

class Program
{
    static void Main()
    {
        // 双引号包裹内容,直接赋值
        string str1 = "我是普通字符串";
        Console.WriteLine(str1);
    }
}

✅ 机理:编译器遇到双引号 "内容",会在字符串池(String Interning,字符串常量池) 里查找有没有一模一样的字符串;有就直接复用对象,没有就在池里新建。目的:节约内存,避免重复创建相同文本。

2. 逐字字符串 @""(原样输出,不转义)

普通字符串 \ 是转义符,\n换行、\t制表符; 加 @ 前缀的逐字字符串:忽略转义符,所有字符原样保留。

cs 复制代码
string path1 = "C:\\Users\\test\\1.txt"; // 普通写法,\必须写成\\
string path2 = @"C:\Users\test\1.txt";  // @逐字写法,更直观,推荐写文件路径

Console.WriteLine(path1);
Console.WriteLine(path2);

输出两行完全一样:

cs 复制代码
C:\Users\test\1.txt
C:\Users\test\1.txt

机理:@告诉编译器,不要解析里面的转义字符。唯一例外:双引号要写两个""代表一个 "

cs 复制代码
string str = @"他说:""你好""";
Console.WriteLine(str); // 输出:他说:"你好"

3. 字符串插值 $""(C#6.0+,拼接变量,最推荐)

$ 符号开启插值,{变量名} 可以直接嵌入变量

cs 复制代码
int age = 18;
string name = "小明";
string info = $"姓名:{name},年龄:{age}";
Console.WriteLine(info);

输出:姓名:小明,年龄:18 机理:编译器底层自动调用 String.Format(),把大括号里的变量转成字符串,拼接生成新字符串。

4. 使用 new String () 构造函数创建

cs 复制代码
// 构造函数:重复某个字符多次
string s = new String('*', 5); 
Console.WriteLine(s); // *****

机理:直接在堆上创建字符串对象,不会放进字符串池,适合批量生成重复字符。

5. 空字符串 vs null(新手高频坑!)

cs 复制代码
string s1 = "";      // 空字符串:对象存在,里面0个字符,长度=0
string s2 = null;    // null:没有创建字符串对象,s2不指向任何内存地址

Console.WriteLine(s1.Length); // 0 ✅ 对象存在,可以访问Length
// Console.WriteLine(s2.Length); // 报错!NullReferenceException

机理:

  • "":内存里有字符串对象,只是里面没有字符;
  • null:变量没有引用任何对象,访问它的任何属性 / 方法直接空引用崩溃。

判断空字符串推荐写法:string.IsNullOrEmpty(s) 同时判断 null 或者 ""

cs 复制代码
string test = null;
if (string.IsNullOrEmpty(test))
{
    Console.WriteLine("字符串是null或者空");
}

二、字符串本质:字符数组,索引访问字符

string 内部存储的是一个 char[] 字符数组。可以读取指定位置字符,但不能修改!

索引从 0 开始,第 0 位是第一个字符。

cs 复制代码
string str = "ABCDE";
char c = str[0];
Console.WriteLine(c); // A
Console.WriteLine(str[2]); // C

机理:str[索引] 是只读访问器,只能读,不能赋值修改!

cs 复制代码
string str = "ABC";
// str[0] = 'X'; // 编译报错!字符串不可变,不能修改单个字符

如果想修改单个字符,只能转成 char 数组,修改数组,再转回字符串

cs 复制代码
string str = "ABC";
char[] chars = str.ToCharArray(); // 把字符串复制一份变成字符数组(数组是可变的)
chars[0] = 'X';
string newStr = new string(chars); // 根据修改后的数组,生成全新字符串
Console.WriteLine(newStr); // XBC
Console.WriteLine(str);   // ABC 【原字符串完全没变!】

✅ 机理拆解:

  1. ToCharArray():复制原有字符串的字符,生成独立的 char 数组;
  2. char 数组是可变类型,可以修改里面元素;
  3. new string(chars):拿修改后的数组,新建一个字符串对象;
  4. 原始字符串 str 从头到尾没有任何改动。完美印证 string 不可变特性。

获取字符串长度 Length

cs 复制代码
string s = "Hello";
Console.WriteLine(s.Length); // 5

Length 属性:返回字符数量,只读。注意:中文、英文都一个字符占 1 个长度

cs 复制代码
string s = "你好";
Console.WriteLine(s.Length); // 2

三、字符串常用方法(每一个示例 + 底层机理)

重要提醒:所有下面的字符串处理方法,都不会修改原字符串!全部返回新字符串! 新手最常犯错误:调用方法后,不接收返回值,以为原字符串变了。

1. 字符串比较 Equals / ==

cs 复制代码
string a = "apple";
string b = "apple";
string c = new string('a',5);

Console.WriteLine(a == b);       // True
Console.WriteLine(a.Equals(b));  // True

机理:

  • == 对于 string 做了重载,比较字符内容,不是比较内存地址;
  • Equals() 专门比较两个字符串的字符是否完全一致;

忽略大小写比较

cs 复制代码
string s1 = "Hello";
string s2 = "hello";
bool same = s1.Equals(s2, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(same); // True

StringComparison.OrdinalIgnoreCase:按原始字符对比,忽略大小写,推荐做英文匹配。

2. 查找字符 / 子串:IndexOf、LastIndexOf

IndexOf(子串):从前往后找 ,返回第一个匹配的索引;找不到返回 -1 LastIndexOf(子串):从后往前找,返回最后一个匹配的索引;找不到返回 -1

cs 复制代码
string str = "abcabc";
int pos1 = str.IndexOf("bc");
int pos2 = str.LastIndexOf("bc");
Console.WriteLine(pos1); // 1
Console.WriteLine(pos2); // 4

机理:逐个遍历内部字符数组比对,找到立刻停止;返回索引。返回 - 1 代表不存在。

3. 截取子串 Substring

Substring(startIndex):从 start 索引,截取到末尾 Substring(startIndex, length):从 start 索引,截取指定长度字符

cs 复制代码
string text = "0123456";
string s1 = text.Substring(2);    // 从索引2开始到结尾
string s2 = text.Substring(2,3);  // 索引2,取3个字符
Console.WriteLine(s1); // "23456"
Console.WriteLine(s2); // "234"
Console.WriteLine(text);// "0123456" 原字符串不变!

机理:创建新字符串,复制原字符串对应区间的字符。原字符串完全不动。

4. 截取分割 Split,把字符串切成字符串数组

Split:根据分隔符,把字符串拆成多个子字符串,返回string[]数组

cs 复制代码
string data = "苹果,香蕉,橙子";
string[] fruits = data.Split(',');
foreach(var item in fruits)
{
    Console.WriteLine(item);
}

输出:

cs 复制代码
苹果
香蕉
橙子

机理:遍历字符,遇到分隔符就切断,每一段生成新字符串,打包进数组。

5. 替换 Replace

Replace(旧内容,新内容):把所有匹配的旧文本替换成新文本

cs 复制代码
string s = "aaabbb";
string result = s.Replace("aaa","xxx");
Console.WriteLine(result); // xxxbbb
Console.WriteLine(s);      // aaabbb 原串不变!

机理:扫描原字符串,遇到匹配片段,用新字符构建全新字符串返回。

6. 去除首尾空白 Trim / TrimStart / TrimEnd

空白包含:空格、制表符 \t、换行 \n

cs 复制代码
string raw = "   hello world   ";
string res = raw.Trim();
Console.WriteLine($"|{res}|"); // |hello world|
  • Trim():去掉开头 + 结尾空白
  • TrimStart():只去掉开头空白
  • TrimEnd():只去掉结尾空白

7. 大小写转换 ToUpper () / ToLower ()

cs 复制代码
string s = "Hello";
string upper = s.ToUpper();
string lower = s.ToLower();
Console.WriteLine(upper); // HELLO
Console.WriteLine(lower); // hello

注意:中文无大小写,不会变化。依然返回新字符串,原串不变。

8. 判断开头 / 结尾 StartsWith / EndsWith

cs 复制代码
string url = "https://www.baidu.com";
bool start = url.StartsWith("https");
bool end = url.EndsWith(".com");
Console.WriteLine(start); // True
Console.WriteLine(end);   // True

机理:对比字符串头部 / 尾部字符序列,返回布尔。

9. Contains 判断是否包含子串(C#2.0+)

cs 复制代码
string text = "今天天气很好";
bool has = text.Contains("天气");
Console.WriteLine(has); // True

机理:底层就是封装了 IndexOf,如果 IndexOf != -1,返回 true。

10. 字符串拼接 + 和 string.Concat

cs 复制代码
string a = "A";
string b = "B";
string c = a + b;
Console.WriteLine(c); // AB

机理:+ 遇到字符串,编译器自动转为 string.Concat(a,b),每次 + 都会生成新字符串。

⚠️ 重要性能警告: 如果循环里大量使用 s = s + "xxx",会疯狂创建大量临时字符串,内存压力巨大! 循环拼接大量文本请使用 StringBuilder,后面专门讲。

cs 复制代码
string s = string.Concat("1","2","3");
Console.WriteLine(s); //123

11. PadLeft / PadRight 补字符到固定长度

常用于格式化输出,对齐文本

cs 复制代码
string num = "123";
string pad = num.PadLeft(5,'0'); //总长度5,不够左边补0
Console.WriteLine(pad); //00123

四、StringBuilder:解决字符串频繁拼接性能问题(对比 string)

string:不可变,每次修改新建对象,循环多次拼接效率很差 StringBuilder:可变字符缓冲区,在同一个缓冲区修改字符,最后一次性转 string,适合大量拼接场景

使用前要引入命名空间 using System.Text;

cs 复制代码
using System;
using System.Text;

class Program
{
    static void Main()
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("A");   //追加文本
        sb.Append("B");
        sb.AppendLine("C");//追加+换行
        sb.Insert(0,"Start:"); //在索引0插入
        sb.Replace("A","X"); //替换缓冲区字符
        
        string finalStr = sb.ToString(); //一次性生成最终字符串
        Console.WriteLine(finalStr);
    }
}

机理: StringBuilder 内部维护一个char[]缓冲区数组,数组容量不够时才扩容。Append/Insert/Replace 都直接操作这个数组,不会每次新建字符串 ;直到调用.ToString()才生成唯一的 string 对象。

✅ 场景选择口诀:

  • 少量拼接、简单文本:直接用 string / 插值字符串 $""
  • 循环几百次以上拼接大量文本、日志、长报文:必须用 StringBuilder

对比示例:坏写法 vs 推荐写法

❌ 低效写法(循环 +=,大量临时字符串)

cs 复制代码
string s = "";
for(int i=0;i<1000;i++)
{
    s += i.ToString(); //每一轮都新建字符串!
}

✅ 高效写法

cs 复制代码
StringBuilder sb = new StringBuilder();
for(int i=0;i<1000;i++)
{
    sb.Append(i); //操作缓冲区,极少扩容
}
string s = sb.ToString();

五、字符串池(字符串驻留 String Interning)深度原理

cs 复制代码
string s1 = "test";
string s2 = "test";
// s1 和 s2 指向内存中**同一个对象**!
Console.WriteLine(object.ReferenceEquals(s1,s2)); //True

string s3 = new string("test".ToCharArray());
Console.WriteLine(object.ReferenceEquals(s1,s3)); //False

机理: 编译期的字符串字面量会加入字符串池 。当写"test",编译器先去池查找,存在就直接复用引用,减少内存占用。 但是动态生成的字符串(new、Substring、Split 等)默认不进池,是独立对象,哪怕内容一样,地址不同。

手动入池:string.Intern(),把动态字符串加入池,复用。

cs 复制代码
string temp = new string("abc".ToCharArray());
string internStr = string.Intern(temp);

一般新手不用手动控制字符串池,理解概念即可;大量重复固定文本场景才考虑。


六、转义字符表(普通双引号字符串内)

转义符 含义
\n 换行
\t 水平制表符 Tab
\\ 反斜杠 |
\" 双引号
\r 回车

逐字字符串 @"" 除了""代表双引号,其余转义全部失效。


七、新手常见错误汇总

错误 1:以为方法会修改原字符串

cs 复制代码
string s = "abc";
s.ToUpper(); // ❌ 白写!返回新字符串,但没有赋值接收
Console.WriteLine(s); //仍然输出 abc
// ✅ 正确写法
string res = s.ToUpper();

错误 2:null 调用方法,空引用崩溃

cs 复制代码
string s = null;
Console.WriteLine(s.Length); //直接报错 NullReferenceException

习惯先判断 if(string.IsNullOrEmpty(s))

错误 3:尝试直接修改字符串里的单个字符

cs 复制代码
string s="abc";
s[0]='x'; //编译报错,不可变

错误 4:循环拼接大量文本,直接 +=,程序卡顿、内存暴涨

解决:StringBuilder

错误 5:区分 "" 和 null

"" 对象存在;null 没有对象。


八、综合完整示例:小实战(把一段文本处理,演示多个方法串联)

需求:输入一句话,去掉前后空格,全部小写,判断是否包含关键词,分割成单词,替换关键词

cs 复制代码
using System;

class Program
{
    static void Main()
    {
        string rawText = "   HELLO C# STRING, String is immutable!   ";
        
        //1. 去除首尾空白
        string trimStr = rawText.Trim();
        Console.WriteLine($"去空格:{trimStr}");
        
        //2. 转小写
        string lowerStr = trimStr.ToLower();
        Console.WriteLine($"小写:{lowerStr}");
        
        //3. 判断是否包含关键词
        bool hasKey = lowerStr.Contains("immutable");
        Console.WriteLine($"是否包含immutable:{hasKey}");
        
        //4. 替换单词
        string replaced = lowerStr.Replace("immutable", "不可变");
        Console.WriteLine($"替换后:{replaced}");
        
        //5. 按逗号分割
        string[] parts = replaced.Split(',');
        Console.WriteLine("分割结果:");
        foreach(var p in parts)
        {
            Console.WriteLine(p.Trim());
        }
    }
}

机理:每一步操作,都生成一个全新字符串;原始 rawText 自始至终完全没有变化。

相关推荐
eggwyw14 分钟前
PHP8.5怎么用ffmpeg提取视频里的字幕轨道
开发语言·php
韩曙亮18 分钟前
【错误记录】安卓 adb 调试 5037 端口占用排查 ( adb 的三层结构 | 找出本机占用 5037 的进程 )
android·数据库·adb·安卓·错误记录
瑞雪兆丰年兮31 分钟前
JavaWeb-day07-MySQL(DQL&多表设计)
java·开发语言·数据库·sql·mysql·javaweb·web
ShineWinsu3 小时前
对于Qt:QDial常见属性以及信号的解析
开发语言·c++·qt·gui·客户端·旋钮·qdial
lsc_blog6 小时前
年终自评别贴提交记录,把技术产出讲成变化
数据库·面试·职场和发展·跳槽·求职招聘
Java后端的Ai之路6 小时前
RAG 还是 Agent 自主检索?Claude Code 删掉向量库之后,我把选型讲透了
开发语言·人工智能·prompt·agent·rag
starzy19906 小时前
深入理解Scala高阶函数:函数式编程核心
开发语言·scala
czq_268671948712 小时前
Python打卡第46天
开发语言·python
Zan13 小时前
SQLite + SQLx 实战:为什么已经发布的数据库迁移文件,绝对不能再修改?
数据库·后端·面试