前置小说明: 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 或者 ""
csstring 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 【原字符串完全没变!】
✅ 机理拆解:
ToCharArray():复制原有字符串的字符,生成独立的 char 数组;- char 数组是可变类型,可以修改里面元素;
new string(chars):拿修改后的数组,新建一个字符串对象;- 原始字符串
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 自始至终完全没有变化。