第 17 节 · 第二部分 流程、方法与数据

字符串(String)

字符串是一排字符,索引从 0 开始,它不可变,所有方法都返回新的结果,原来的字符串不动。

时长 8:31授课计划第 6 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1字符串是什么

一段文字就是一排格子,每格放一个字符,格子的编号从 0 开始,叫索引。字符可以存成 char 数组,但更常用 string 关键字;string 是 System.String 类的别名,两种写法是同一个类型。下面是顶级语句写法(不写类和 Main,代码从第一行开始执行):

C#
1string a = "Hello";// 用小写 string 声明字符串变量 a
2String b = "Hello";// 用大写 String(System.String 类)声明 b
3Console.WriteLine(typeof(string) == typeof(String));// typeof 取出类型,比较两种写法是不是同一个类型:True
4Console.WriteLine(typeof(string).FullName);// .FullName 是类型的全名:System.String
5Console.WriteLine(a == b);// 比较两个变量里的文字是否相同:True

2创建字符串

教程列了五种办法:直接赋值双引号里的文字、用 String 的构造函数、用 + 连接、读属性或调用返回字符串的方法、用格式化方法。教程示例把它们都用上了:

C# · 第 14-15 行
14string fullname = fname + lname;// 用 + 连接:"Rowan" + "Atkinson",中间没有空格
15Console.WriteLine("Full Name: {0}", fullname);// {0} 处换成 fullname:RowanAtkinson
查看完整代码(35 行)
C#
1using System;// 引入 System 命名空间
2
3namespace StringApplication// 定义命名空间 StringApplication
4{// StringApplication 命名空间开始
5 class Program// 定义 Program 类
6 {// Program 类开始
7 static void Main(string[] args)// 程序入口 Main 方法
8 {// Main 开始
9 //字符串,字符串连接
10 string fname, lname;// 一次声明两个字符串变量
11 fname = "Rowan";// 第一种:直接赋值双引号里的文字
12 lname = "Atkinson";// 给 lname 赋值
13
14 string fullname = fname + lname;// 第三种:用 + 连接,中间没有空格
15 Console.WriteLine("Full Name: {0}", fullname);// 输出 Full Name: RowanAtkinson
16
17 //通过使用 string 构造函数
18 char[] letters = { 'H', 'e', 'l', 'l','o' };// char 数组,5 个字符
19 string greetings = new string(letters);// 第二种:构造函数把字符数组变成字符串
20 Console.WriteLine("Greetings: {0}", greetings);// 输出 Greetings: Hello
21
22 //方法返回字符串
23 string[] sarray = { "Hello", "From", "Tutorials", "Point" };// 4 个单词的字符串数组
24 string message = String.Join(" ", sarray);// 第四种:调用返回字符串的方法 Join
25 Console.WriteLine("Message: {0}", message);// 输出 Message: Hello From Tutorials Point
26
27 //用于转化值的格式化方法
28 DateTime waiting = new DateTime(2012, 10, 10, 17, 58, 1);// 一个日期时间:2012-10-10 17:58:01
29 string chat = String.Format("Message sent at {0:t} on {0:D}",// 第五种:格式化方法,{0:t} 短时间,{0:D} 长日期
30 waiting);// 两个 {0} 都换成 waiting
31 Console.WriteLine("Message: {0}", chat);// 输出格式化后的文字
32 Console.ReadKey() ;// 等待按键(现在的 VS 不写也会停住)
33 }// Main 结束
34 }// Program 类结束
35}// StringApplication 命名空间结束
查看完整输出(4 行)
运行结果
Full Name: RowanAtkinson
Greetings: Hello
Message: Hello From Tutorials Point
Message: Message sent at 17:58 on Wednesday, 10 October 2012

其他几种写法见折叠的完整代码。最后一行的日期写法随电脑的区域设置变:教程的英文日期是英国英语区域设置下的结果,换成中文区域设置就打印中文日期。

C# · 第 6-7 行
6Console.WriteLine(String.Format(uk, "Message sent at {0:t} on {0:D}", waiting));// uk 是英国英语区域设置 en-GB
7Console.WriteLine(String.Format(cn, "Message sent at {0:t} on {0:D}", waiting));// cn 是中文区域设置 zh-CN
运行结果
Message sent at 17:58 on Wednesday, 10 October 2012
Message sent at 17:58 on 2012年10月10日 星期三

3两个属性:Length 和索引

Length 是字符的个数;取某个位置上的字符,在变量后面加方括号写索引,最后一格的索引是 Length - 1。

C#
1string s = "Hello";// 5 个字符,索引 0 到 4
2Console.WriteLine(s.Length);// 字符个数:5
3Console.WriteLine(s[0]);// 第 0 格:H
4Console.WriteLine(s[4]);// 第 4 格,也是最后一格(Length - 1):o

C# 里取字符只能用方括号,写成 s.Chars(0) 会报错(报错里的“程序集”指编译出来的程序文件,比如 .dll):

C# · 第 2 行
2Console.WriteLine(s.Chars(0));// 照教程的名字写 Chars
编译报错
Program.cs(2,21): error CS1061: “string”未包含“Chars”的定义,并且找不到可接受第一个“string”类型参数的可访问扩展方法“Chars”(是否缺少 using 指令或程序集引用?)

索引写到 5 呢?编译能通过,一运行就抛出越界异常 System.IndexOutOfRangeException: Index was outside the bounds of the array.,索引只能是 0 到 Length - 1。

C# · 第 2 行
2Console.WriteLine(s[5]);// "Hello" 只有 0 到 4,5 越界了

4不可变

想把第 0 格的 H 换成 J 也不行:

C# · 第 2 行
2s[0] = 'J';// 想改第 0 格:方括号取字符只能读,不能写

编译器报 error CS0200: 无法为属性或索引器“string.this[int]”赋值 - 它是只读的。字符串一旦创建,里面的字符就不能再改,这叫不可变。

视频里用锁杆压住字模来比喻;真实的字符串是内存里一段连续的字符编码,没有槽也没有锁。

5方法分六组

String 类的方法很多,教程的表列了 36 行,按用途分成六组:比较、连接、查找、生成新字符串、拆分、其他。先记住一点:这些方法都不改原来的字符串,要得到改过的文字,都是返回一个新字符串。

6比较

Compare 比较两个字符串在排序里谁前谁后,返回的是整数,不是 true/false;默认区分大小写,第三个参数写 true 就忽略大小写。Equals 只回答一样不一样。

C#
1string a = "apple", b = "Apple";// a 是小写 apple,b 是大写开头的 Apple
2Console.WriteLine(String.Compare("apple", "banana"));// apple 排在前面:-1
3Console.WriteLine(String.Compare("banana", "apple"));// 反过来比:1
4Console.WriteLine(String.Compare(a, b));// 默认区分大小写,结果不是 0:-1
5Console.WriteLine(String.Compare(a, b, true));// 第三个参数 true:忽略大小写,0 表示相等
6Console.WriteLine(a.Equals(b));// Equals 区分大小写:False
7Console.WriteLine(String.Equals(a, "apple"));// 两个参数的写法:True

教程的比较示例里,"This is test" 和 "This is text" 只差一个字母,Compare 结果不是 0,所以打印 This is test and This is text are not equal.。

7连接

Concat 把两个、三个或四个字符串首尾接起来;Join 多一个分隔符,把数组的每个元素用它隔开再拼起来,还能只取数组的一段。

C#
1Console.WriteLine(String.Concat("C", "#"));// 两个接起来:C#
2Console.WriteLine(String.Concat("Hello", " ", "World"));// 三个接起来:Hello World
3Console.WriteLine(String.Concat("a", "b", "c", "d"));// 四个接起来:abcd
4string[] colors = { "red", "green", "blue", "black" };// 4 个颜色的数组
5Console.WriteLine(String.Join(", ", colors));// 用逗号加空格隔开:red, green, blue, black
6Console.WriteLine(String.Join("-", colors, 1, 2));// 从索引 1 开始取 2 个,用 - 连:green-blue

教程的连接示例用换行 "\n" 当分隔符,五句歌词就各占一行:

C# · 第 15 行
15string str = String.Join("\n", starray);// \n 表示换行,每个元素之间插一个换行

8查找

拿 banana 来试:6 个字符,索引 0 到 5。Contains("nan")、StartsWith("ba")、EndsWith("na") 分别问“里面有没有”“开头是不是”“结尾是不是”,实测都是 True。教程示例里 str.Contains("test") 找到了 test,就打印 The sequence 'test' was found.。

IndexOf 返回第一次出现的位置,第二个参数是起点,找不到返回 -1:

C# · 第 2-6 行
2Console.WriteLine(s.IndexOf('a'));// 第一个 a 在索引 1
3Console.WriteLine(s.IndexOf("na"));// 找字符串,返回它开头的位置:2
4Console.WriteLine(s.IndexOf('a', 2));// 从索引 2 开始找 a:3
5Console.WriteLine(s.IndexOf("na", 3));// 从索引 3 开始找 na:4
6Console.WriteLine(s.IndexOf('z'));// 没有 z:-1

IndexOfAny 给一组字符,谁先出现就停在哪:找 { 'n', 'x' } 时 n 先出现,结果是 2;LastIndexOf 找最后一次出现的位置:s.LastIndexOf('a') 是 5。

9生成新字符串

拿 Hello World 来试:每个方法都返回一个新字符串,最后打印 s 本身,一个字都没变。

C#
1string s = "Hello World";// 原字符串,索引 0 到 10
2Console.WriteLine(s.Insert(5, ","));// 在索引 5 插进逗号:Hello, World
3Console.WriteLine(s.Remove(5));// 从索引 5 删到最后:Hello
4Console.WriteLine(s.Remove(0, 6));// 从索引 0 开始删 6 个:World
5Console.WriteLine(s.Replace('o', '0'));// 把所有字符 o 换成数字 0:Hell0 W0rld
6Console.WriteLine(s.Replace("World", "C#"));// 把字符串 World 换成 C#:Hello C#
7Console.WriteLine(s);// s 本身没变:Hello World

Remove 不会动原来的字符串,返回的是一个新字符串,上面最后一行就是证据。ToUpper、ToLower 变大写、小写,Trim 去掉开头和结尾的空白:

C# · 第 4-6 行
4string t = " Hi ";// 前后各有 3 个空格
5Console.WriteLine("[" + t + "]");// 加中括号看清空格:[ Hi ]
6Console.WriteLine("[" + t.Trim() + "]");// Trim 去掉开头和结尾的空白:[Hi]

很多人以为单独写一句 s.ToUpper(); 就把 s 改成大写了,实测还是小写;要用新结果,得赋值回给 s:

C#
1string s = "hello";// 原字符串是小写
2s.ToUpper();// 返回了新字符串 HELLO,但没人接住,就丢了
3Console.WriteLine(s);// s 没变:hello
4s = s.ToUpper();// 把新字符串赋值回给 s
5Console.WriteLine(s);// 这回是 HELLO

10拆分和字符数组

Split 按分隔符切开,返回字符串数组;ToCharArray 拆成字符数组;CopyTo 把一段字符复制进已有的数组。

C#
1string csv = "red,green;blue";// 用逗号和分号隔开的三段
2string[] parts = csv.Split(',', ';');// 逗号和分号都算分隔符
3Console.WriteLine(parts.Length);// 切出 3 段
4Console.WriteLine(String.Join(" | ", parts));// 用 | 连起来看清每一段:red | green | blue
5string[] two = csv.Split(new char[] { ',', ';' }, 2);// 数量 2:最多切两段
6Console.WriteLine(String.Join(" | ", two));// 剩下的都留在第二段:red | green;blue

"Hello".ToCharArray() 拆出 5 个字符,ToCharArray(1, 3) 从索引 1 开始拆 3 个,得到 ell。CopyTo 的例子:

C# · 第 5-7 行
5char[] dest = { '*', '*', '*', '*', '*' };// 一个装着 5 个 * 的数组
6"Hello".CopyTo(1, dest, 0, 3);// 从 Hello 的索引 1 复制 3 个字符,放进 dest 的 0 号位置起
7Console.WriteLine(new string(dest));// 前 3 格被换成 ell,后 2 格还是 *:ell**

11其他方法

String.Copy 现在已经被标成过时,编译会给警告 CS0618;字符串本来就不可变,直接赋值共用就行。Format 把 {0} 换成后面参数的文字(a 是 "abc" 时,String.Format("长度是 {0}", a.Length) 得到“长度是 3”),IsNullOrEmpty 判断是不是空值(null)或空字符串。

C# · 第 5-7 行
5Console.WriteLine(String.IsNullOrEmpty(""));// 空字符串:True
6Console.WriteLine(String.IsNullOrEmpty(null));// null 表示什么都没有:True
7Console.WriteLine(String.IsNullOrEmpty(" "));// 有一个空格,不算空:False

完整的方法列表查微软的 Microsoft Learn 文档(教程里的 MSDN 已改名)。

12取子字符串

教程还有一个示例用到 Substring,表里没列,但很常用:

C# · 第 8-11 行
8string str = "Last night I dreamt of San Pedro";// 原字符串
9Console.WriteLine(str);// 先原样输出
10string substr = str.Substring(23);// 从索引 23(S 那一格)一直取到最后
11Console.WriteLine(substr);// 得到最后两个词:San Pedro

13小结

  • 字符串是一排字符,索引从 0 开始,Length 是个数,最后一格是 Length - 1。
  • 字符串不可变,方法都返回新的字符串,原来的不动;要用结果就接住它。

下一节:想把一个学生的名字、年龄、成绩打包成一个值,怎么办?

检查题

5 道,选完马上告诉你对不对。

  1. 1string s = "Hello"; 之后 Console.WriteLine(s[5]); 会怎样?

  2. 2string s = "hello"; s.ToUpper(); Console.WriteLine(s); 打印?

  3. 3"banana".IndexOf("na") 的结果?

  4. 4String.Compare("apple", "Apple", true) 的结果?

  5. 5string s = "Hello"; s[0] = 'J'; 会怎样?