【仓颉语言入门 · 第6课】字符串详解与字符串插值
上节课我们用循环让程序学会了"重复干活"。不过前几课输出的还都是干巴巴的数字,而真实程序处理得最多的数据其实是文本:用户名、文件路径、一句话、一条日志……第 3 课我们简单用过双引号字符串,这节课把它彻底讲透——怎么把变量嵌进句子里(字符串插值)、怎么取出其中一部分(切片)、英文和汉字长度为什么不一样(UTF-8),以及一大批开箱即用的字符串方法。
本文是系列第 6 课:正式学习仓颉的 String(字符串)、Rune(字符)、转义字符、特殊字符串(raw 串 / 多行串)、切片与常用方法,最后用一个"句子分析器"综合实战收尾。
目录(系列导航)
整套路线共 7 个模块、30 课,按每周 2~3 课的节奏,大约 2~3 个月可以走完一遍:
| 一、环境与入门 | 01~05 | 环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环 |
| 二、常用类型与数据组织 | 06~10 | 字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理 |
| 三、函数与函数式 | 11~14 | 函数、Lambda 与高阶函数、闭包、迭代器与惰性序列 |
| 四、面向对象与类型系统 | 15~20 | struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展 |
| 五、工程化与标准库 | 21~25 | cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试 |
| 六、并发编程 | 26~28 | 线程、Channel 通道与同步原语、并发实战 |
| 七、项目实战 | 29~30 | 命令行小工具、GeoJSON 数据处理实战 |
一、认识 String:文本的容器
用双引号括起来的一段文字就是字符串字面量,类型是 String:
package helloCangjie
main(): Int64 {
let greeting = "你好,仓颉"
let language = "Cangjie"
// ① 用 + 拼接两段字符串
println(greeting + ",欢迎学习 " + language + "!")
// ② 用 * 让字符串重复出现
println("重要的事情说三遍:" + "冲!" * 3)
// ③ 按内容比较,用 == / < / >
println("abc" == "abc")
println("apple" < "banana")
// ④ 判空:字符串里一个字符都没有
println("空串吗:" + "".isEmpty().toString())
return 0
}
运行结果:
你好,仓颉,欢迎学习 Cangjie!
重要的事情说三遍:冲!冲!冲!
true
true
空串吗:true
四点说明:
- + 拼接:左右两边都必须是字符串(数字想拼进来要用下一节的插值,不能直接 "年龄" + 3);
- * 重复:"ab" * 3 得到 "ababab",画分隔线时很好用;
- 比较的是内容:== 判断文字是否完全相同,< / > 按字典序比较,直接写就行;
- "" 是空字符串:它是一个真实存在的对象,只是长度为 0,用 isEmpty() 判断。
💡 String 是不可变类型:所有"修改"操作(拼接、替换、大小写转换……)都不会改动原来的字符串,而是返回一个新字符串。原变量想变,必须用 var 声明并重新赋值:s = s + "!"。
二、字符串插值:让程序"好好说话"
用 + 拼句子很快就会变成灾难:引号、逗号、类型转换混在一起。仓颉提供了字符串模板插值——在字符串里写 ${表达式},运行时自动求值并"嵌"进去:
package helloCangjie
main(): Int64 {
let name = "小明"
let age = 18
let height = 1.75
// ${} 里可以是变量
println("我叫 ${name},今年 ${age} 岁")
// ${} 里也可以是表达式:算术、方法调用、甚至另一个字符串
println("再过 10 年我就 ${age + 10} 岁了")
println("这句话是 ${"abc" == "abc"} 的:身高 ${height} 米")
// 数字转字符串:插值是首选;也可以显式调用 toString()
let scoreText = 95.toString()
println("成绩:" + scoreText)
return 0
}
运行结果:
我叫 小明,今年 18 岁
再过 10 年我就 28 岁了
这句话是 true 的:身高 1.750000 米
成绩:95
从第 1 课的 println("Hello World") 起你其实一直在用插值——${sum}、${i} 都是它。记住三个要点:
反向转换(字符串 → 数字)还是第 3 课的 Int64.parse,它住在 std.convert 包里,使用前要 import:
package helloCangjie
import std.convert.*;
import std.env.getStdIn;
main(): Int64 {
print("请输入一个整数:")
let text: String = getStdIn().readln().getOrThrow();
let n = Int64.parse(text)
println("解析后加 8 等于:${n + 8}")
return 0
}
运行结果:
请输入一个整数:解析后加 8 等于:50
(print 不换行,运行时输入的内容会接在提示后面显示;这里字符串直接给了 "42",所以提示语和结果在同一行。)
三、转义字符与特殊字符串
3.1 转义字符:让字符串"特殊一点"
字符串里有些字符没法直接敲出来(换行、制表符),有些会和语法冲突(字符串里出现双引号)。用反斜杠 \\ 开头的转义字符表达:
| \\n | 换行 |
| \\t | 制表符(Tab,一次跳一段空白) |
| \\" | 双引号(不再被当成字符串结束) |
| \\\\ | 一个反斜杠本身 |
| \\u{4ED3} | 按 Unicode 码点输出字符(4ED3 正是"仓"字) |
package helloCangjie
main(): Int64 {
println("第一行\\n第二行\\t(前面有个 Tab)")
println("他说:\\"仓颉真有意思!\\"")
println("Windows 路径要写双反斜杠:C:\\\\Users\\\\wangpeng")
println("\\u{4ED3}\\u{9889}") // 4ED3=仓,9889=颉
return 0
}
运行结果:
第一行
第二行 (前面有个 Tab)
他说:"仓颉真有意思!"
Windows 路径要写双反斜杠:C:\\Users\\wangpeng
仓颉
3.2 Raw 字符串:反斜杠就是反斜杠
Windows 文件路径里全是 \\,用普通字符串每个都要写成 \\\\,正则表达式更惨。仓颉提供 raw 字符串:在双引号两侧各加一个 #,即 #"…"#,里面的内容原样保留——反斜杠不转义、${} 不插值:
package helloCangjie
main(): Int64 {
let path = #"C:\\Users\\wangpeng\\Desktop\\main.cj"#
println(path)
println(#"${name} 会原样显示,\\n 也不会换行"#)
return 0
}
运行结果:
C:\\Users\\wangpeng\\Desktop\\main.cj
${name} 会原样显示,\\n 也不会换行
写 Windows 路径、正则、带特殊符号的文本时,优先想到 #"…"#。
3.3 多行字符串:三个引号写一段
需要保留换行的大段文字(SQL、模板、诗),用三个双引号 """ 包裹,中间可以直接换行,也同样支持 ${} 插值:
package helloCangjie
main(): Int64 {
let who = "李白"
let poem = """
${who}·静夜思
床前明月光,
疑是地上霜。
"""
println("——")
print(poem)
println("——")
return 0
}
运行结果:
——
李白·静夜思
床前明月光,
疑是地上霜。
——
⚠️ 注意一个实测出来的细节:多行字符串原样保留一切字符,包括每行开头的空格缩进。所以示例里文字故意顶格写(不跟着代码缩进),否则输出里会带上前面的一串空格;结尾换行也会保留("疑是地上霜。"后面那个空行就是这么来的)。
四、字节与字符:UTF-8 的两个"反直觉"
仓颉字符串内部按 UTF-8 编码存储。UTF-8 是一种变长编码:
- 英文、数字、常用符号:1 个字节存一个字符;
- 汉字:3 个字节存一个字符;
- 个别 emoji 甚至要 4 个字节。
这带来两个初学者必踩的坑。
4.1 坑一:size 量的是字节数,不是字数
package helloCangjie
main(): Int64 {
let en = "abc"
let cn = "仓颉语言"
println("${en} 的 size = ${en.size}")
println("${cn} 的 size = ${cn.size}")
println("${cn} 的字数 = ${cn.toRuneArray().size}")
return 0
}
运行结果:
abc 的 size = 3
仓颉语言 的 size = 12
仓颉语言 的字数 = 4
"仓颉语言" 只有 4 个字,size 却是 12(4 × 3 字节)。想得到"人眼看到的字数",先调用 toRuneArray() 转成字符数组再取 size。
4.2 坑二:for-in 遍历字符串得到的是字节
更意外的是,直接 for-in 一个字符串,循环变量不是字符,而是一个个字节(UInt8,数字):
package helloCangjie
main(): Int64 {
let cn = "仓颉语言"
print("直接 for-in:")
for (b in cn) {
print("${b} ")
}
println()
print("调用 runes():")
for (ch in cn.runes()) {
print("${ch} ")
}
println()
return 0
}
运行结果:
直接 for-in:228 187 147 233 162 137 232 175 173 232 168 128
调用 runes():仓 颉 语 言
前三个数字 228 187 147 正是"仓"字 UTF-8 编码的三个字节(十六进制 E4 BB 93)。想逐个人们认识的字符遍历,调用字符串的 runes() 方法。
4.3 Rune:单个字符的类型
仓颉用 Rune 类型表示一个 Unicode 字符,字面量写法是 r'字符'(小写 r 加单引号)。它配合 std.unicode 包还能判断字符种类:
package helloCangjie
import std.unicode.*
main(): Int64 {
let a = r'A'
let five = r'5'
let zhong = r'中'
println("${a} 是字母吗:${a.isLetter()}")
println("${five} 是数字吗:${five.isNumber()}")
println("${zhong} 是字母吗:${zhong.isLetter()}")
return 0
}
运行结果:
A 是字母吗:true
5 是数字吗:true
中 是字母吗:true
(汉字在 Unicode 分类里属于字母,所以 r'中'.isLetter() 是 true。isLetter()、isNumber() 等是 std.unicode 包给 Rune 提供的扩展,记得 import。)
把本节串起来:
| 字节长度 | s.size | 英文 1 字节,汉字 3 字节 |
| 字数 | s.toRuneArray().size | 一个 Rune 对应一个人眼字符 |
| 逐字节遍历 | for (b in s) | 得到 UInt8 |
| 逐字符遍历 | for (ch in s.runes()) | 得到 Rune |
| 取第 i 个字符 | s.toRuneArray()[i] | 安全,不受变长编码影响 |
五、切片:从字符串里取出一段
字符串支持第 5 课学过的区间下标,切一刀取出子串:
package helloCangjie
main(): Int64 {
let s = "HelloCangjie"
println(s[0..5]) // 左闭右开:下标 0~4
println(s[5..=10]) // 左闭右闭:下标 5~10
return 0
}
运行结果:
Hello
Cangji
注意:切片下标同样是字节下标,从 0 开始。纯英文文本每个字符 1 字节,下标和"第几个字母"完全对得上,放心切。
但切中文时必须守住 3 字节边界——每个汉字正好占 3 个字节,按 3 的倍数切才安全:
package helloCangjie
main(): Int64 {
let cn = "仓颉语言"
println(cn[0..3]) // 0~2 字节,正好是"仓"
println(cn[0..=5]) // 0~5 字节,正好是"仓颉"
return 0
}
运行结果:
仓
仓颉
⚠️ 如果下标切到一个汉字的中间(比如对 "仓颉" 取 [0..1]),程序能编译通过,但运行时会抛出异常:
An exception has occurred:
IllegalArgumentException: Invalid utf8 byte sequence.
所以处理含中文的文本,最稳妥的办法是先转字符数组再操作,最后还能用 String(字符数组) 拼回字符串:
package helloCangjie
main(): Int64 {
let cn = "仓颉语言"
let runes = cn.toRuneArray()
println("第 1 个字:${runes[0]}")
println("第 4 个字:${runes[3]}")
// 取出"颉语"两个字,再组回字符串
let part = String(runes[1..3])
println("取出的部分:${part}")
return 0
}
运行结果:
第 1 个字:仓
第 4 个字:言
取出的部分:颉语
(runes[1..3] 是数组切片,第 7 课学数组时还会再见;这里先记住"字符数组可以放心按下标语义操作"。)
六、常用字符串方法全家桶
标准库给 String 准备了一批高频方法,先跑一段总览,再逐个解释:
package helloCangjie
import std.unicode.*
main(): Int64 {
let s = "Hello,Cangjie"
// ① 判断类:包含 / 开头 / 结尾 / 计数
println("包含 Cang:${s.contains("Cang")}")
println("以 Hello 开头:${s.startsWith("Hello")}")
println("以 jie 结尾:${s.endsWith("jie")}")
println("字母 l 出现次数:${s.count("l")}")
// ② 查找位置:找不到返回 None(第 9 课讲 Option)
println("第一个 l 的位置:${s.indexOf("l")}")
println("最后一个 l 的位置:${s.lastIndexOf("l")}")
println("xyz 的位置:${s.indexOf("xyz")}")
// ③ 切分与连接
let fruits = "apple,banana,orange".split(",")
println("切出 ${fruits.size} 段,第二段是 ${fruits[1]}")
println(String.join(fruits, delimiter: " | "))
// ④ 替换
println(s.replace("Cangjie", "仓颉"))
// ⑤ 英文大小写转换(只影响 ASCII 字母)
println("cangjie".toAsciiUpper())
println("CANGJIE".toAsciiLower())
// ⑥ 去首尾空白(trim 系列是 std.unicode 的扩展)
let padded = " hello cangjie "
println("[${padded.trim()}]")
return 0
}
运行结果:
包含 Cang:true
以 Hello 开头:true
以 jie 结尾:true
字母 l 出现次数:2
第一个 l 的位置:Some(2)
最后一个 l 的位置:Some(3)
xyz 的位置:None
切出 3 段,第二段是 banana
apple | banana | orange
Hello,仓颉
CANGJIE
cangjie
[hello cangjie]
逐个解释:
- 判断三件套:contains(是否包含子串)、startsWith / endsWith(前缀后缀),返回 Bool,最适合放在 if 条件里;
- count("l"):数子串出现了几次(这里 l 在 Hello 里出现 2 次);
- indexOf / lastIndexOf:返回子串第一次/最后一次出现的字节下标。结果长得有点怪:找到是 Some(2),找不到是 None——它是第 9 课要学的 Option 可选值,现阶段只要记住"判断有没有用 contains,要位置才用 indexOf,看到 None 就是没找到";
- split(","):按分隔符切成字符串数组(Array<String>,第 7 课细讲);String.join(数组, delimiter: " | ") 反过来用指定分隔符拼回一句——注意 join 是 String 的静态方法,分隔符是命名参数 delimiter:;
- replace(旧, 新):把所有旧子串替换成新的,返回新字符串;
- toAsciiUpper() / toAsciiLower():只转换英文字母(名字带 Ascii 就是提醒你它不管汉字);
- trim():去掉首尾的空格、Tab、换行等空白,处理用户输入的利器;只想去一头用 trimStart() / trimEnd()。这三个是 std.unicode 包的扩展函数,必须 import std.unicode.*,否则编译器会报 'trim' is not a member of struct 'String'。
七、StringBuilder:循环拼接的正确姿势
因为 String 不可变,下面这种循环里用 += 拼字符串的写法,每拼一次都会造一个全新字符串、把旧内容整体复制一遍,数据量大时很浪费:
package helloCangjie
main(): Int64 {
// 能跑,但循环次数很多时效率低(每次 += 都在复制旧内容)
var s = ""
for (i in 1..=5) {
s += "${i}"
}
println("朴素拼接:${s}")
return 0
}
运行结果:
朴素拼接:12345
更好的工具是 StringBuilder(字符串构建器,在核心包 std.core 里,无需 import):它内部维护一块可增长的缓冲区,append 不断往里追加,最后 toString() 一次性取出:
package helloCangjie
main(): Int64 {
let sb = StringBuilder()
for (i in 1..=5) {
sb.append(i)
if (i < 5) {
sb.append(", ") // 数字之间加分隔符,最后一个不加分号
}
}
let result = sb.toString()
println(result)
return 0
}
运行结果:
1, 2, 3, 4, 5
经验法则:偶尔拼一两句,用插值或 +;在循环里成百上千次追加,用 StringBuilder。 append 不挑类型——字符串、数字、Rune 都能直接塞进去,它会自动转成文本。
八、CIDE 实操:英文句子分析器(综合实战)
综合本课知识:让用户输入一句英文,程序自动完成去空白、统计长度/字数、大小写转换、关键词判断、切词编号、查找替换等一整套分析。
8.1 编写程序
继续使用 helloCangjie 项目,把 src/main.cj 替换为:
package helloCangjie
import std.env.*
import std.unicode.*
main(): Int64 {
print("请输入一句英文:")
let raw = getStdIn().readln().getOrThrow()
let sentence = raw.trim()
println("—— 句子分析报告 ——")
println("去掉首尾空白后:[${sentence}]")
println("字节长度:${sentence.size}")
println("字符个数:${sentence.toRuneArray().size}")
println("全大写:${sentence.toAsciiUpper()}")
// 不区分大小写判断是否包含 cangjie:先统一转小写再 contains
if (sentence.toAsciiLower().contains("cangjie")) {
println("关键词检查:这句话里提到了 cangjie ✅")
} else {
println("关键词检查:没有提到 cangjie")
}
// 按空格切词,逐个编号并统计每个词的字节长度
let words = sentence.split(" ")
println("单词数:${words.size}")
for ((index, word) in words.enumerate()) {
println(" 第 ${index + 1} 个词:${word}(${word.size} 个字节)")
}
// 查找、连接与替换
println("第一个字母 o 的位置:${sentence.indexOf("o")}")
println("下划线连接:${String.join(words, delimiter: "_")}")
println("字母 o 替换成 0:${sentence.replace("o", "0")}")
return 0
}
几个设计要点:
- trim() 给输入"洗澡":用户输入时手抖多敲的首尾空格不参与分析,raw 和 sentence 分开保存看得最清楚;
- 大小写不敏感的判断技巧:先 toAsciiLower() 整体转小写,再 contains("cangjie"),这样 Cangjie、CANGJIE 都能命中;
- enumerate():遍历数组时同时拿到"下标 + 元素",编号输出时不用自己维护计数器(第 7 课还会用);
- indexOf 只用于展示位置:真正判断有没有出现用的是上面的 contains。
8.2 运行与验证
按 Ctrl + R 运行,输入 I love Cangjie (故意在首尾多敲几个空格):
请输入一句英文: I love Cangjie
—— 句子分析报告 ——
去掉首尾空白后:[I love Cangjie]
字节长度:14
字符个数:14
全大写:I LOVE CANGJIE
关键词检查:这句话里提到了 cangjie ✅
单词数:3
第 1 个词:I(1 个字节)
第 2 个词:love(4 个字节)
第 3 个词:Cangjie(7 个字节)
第一个字母 o 的位置:Some(3)
下划线连接:I_love_Cangjie
字母 o 替换成 0:I l0ve Cangjie
可以自己再换几句试试:
- 输入全中文的句子,观察"字节长度"和"字符个数"的差距(一个汉字 3 字节);
- 输入不带任何空格的 hello,单词数应为 1;
- 输入不含字母 o 的句子,最后一行位置会显示 None,替换行原文不变。
📸 配图建议:Result 面板完整显示句子分析报告的运行截图,能看到 enumerate 编号列表和 Some(3)。
8.3 用调试器观察字符串的"不可变"
字符串方法都返回新串、不改原串,这个特性在调试器里一目了然:
📸 配图建议:断点命中时 Variables 面板中 raw 带空格、sentence 不带空格、words 含 3 个元素的截图。
调试结束点停止按钮。
九、常见问题 FAQ
Q1:为什么 "仓颉语言".size 是 12 而不是 4? size 返回的是 UTF-8 字节长度,汉字一个字占 3 个字节。想要字数,用 s.toRuneArray().size。
Q2:for (c in "仓颉") 为什么打印出 228、187 这种数字? 直接 for-in 字符串遍历的是字节(UInt8),228 187 147 正是"仓"字的三个 UTF-8 字节。想逐字符遍历用 for (ch in s.runes()),或 s.toRuneArray()。
Q3:切字符串报 IllegalArgumentException: Invalid utf8 byte sequence 是怎么回事? 切片下标是字节下标,切中文时把一个汉字(3 字节)从中间切断了,产生了不合法的 UTF-8 数据。处理中文请先 toRuneArray() 转成字符数组再按字符下标操作,需要字符串时用 String(数组) 拼回。
Q4:调用 trim() 编译报错"not a member of struct ‘String’"? trim() / trimStart() / trimEnd() 是 std.unicode 包提供的扩展函数,文件开头加 import std.unicode.* 即可。contains、split 等则是 String 自带的,不用 import。
Q5:Windows 路径里的反斜杠一定要写两遍吗? 普通字符串里 \\\\ 才表示一个反斜杠;更省心的办法是用 raw 字符串 #"C:\\Users\\name\\a.cj"#,里面所有字符原样保留,连 ${} 都不会被插值。
Q6:indexOf 返回的 Some(3)、None 是什么?能直接当数字用吗? 它是 Option 可选值(第 9 课详解):找到时 Some(字节下标),找不到时 None。入门阶段只要知道"看到 None 就是没找到";只想判断"有没有",直接用返回 Bool 的 contains 更方便。
Q7:循环里用 += 拼字符串不行吗? 能跑,但 String 不可变,每次 += 都会复制整个旧字符串,循环次数多了很慢。少量拼接用插值;循环里大量追加请用 StringBuilder 的 append + toString。
Q8:怎么把数字变成字符串、字符串变成数字? 数字转字符串:插值 "${n}" 或 n.toString();字符串转数字:Int64.parse(text)(需 import std.convert.*),文本不是合法数字时会抛出异常,第 10 课错误处理会讲怎么优雅地接住它。
十、课后练习
下节预告
这节课我们学会了处理"一段文本",但程序里经常要处理的是一排数据:一个班 50 个同学的成绩、购物车里的多件商品。第 7 课学习 数组 Array 与区间 Range——把同类型的数据整齐地装进一个容器,按下标存取、整体遍历、切片,还会正式认识这节课反复出现的 split(" ") 到底切出了什么。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。
� 遇到问题?扫码联系作者
跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):
离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。
�📥 工具下载
本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):
- GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
- 打开页面后进入 发行版(Releases),下载 CIDE-<版本>-x64-Setup.exe 安装即可
- 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn
网硕互联帮助中心


评论前必须登录!
注册