一个是连续同质的序列,一个是稀疏的整数键表——xlang 的两种基础序列容器,连同"谁是共享、谁是副本"的完整答案。
从本章开始进入卷四"容器与文本"。数组是几乎每门语言的第一容器,xlang 给了它两副面孔:静态 array<T> 同质、连续、类型受检;var 动态数组 异构、灵活、运行期分派。除此之外还有一个不太常见的成员:list<T>——整数作键但不要求连续的稀疏列表,适合"第几天记了一笔账"这类数据。本章把三种容器的声明、索引、方法、运算符逐一实测,并用一整节回答第 12、15 章遗留的问题:容器传参和赋值,到底是共享还是复制?
掌握 array<T> 的声明与初始化规则;会用负下标并理解越界即 fatal 的取舍;背下实测过的数组方法全集;理解数组运算符 + / += / == 的可用形态与字面量缺陷;掌握 var 动态数组的异构能力与 swap/reserve 专有方法;掌握 list 的稀疏语义与缺键 fatal;学会多维数组在 RC0 的正确打开方式;吃透容器共享引用语义与静态→var 的转换隔离。
静态数组用 array<T> 声明,全部元素同一类型。用 auto 从字面量推断时,同种字面量得到静态数组,混杂字面量则推断为动态数组:
auto a = [1, 2, 3, 4] println(type(a)) // array<int>:同质字面量推断为静态数组 println(string(a.length)) // 4 array<float> b = [1, 2, 3] // 整数字面量自动转 float println(type(b)) // array<float> array<string> names = ["xlang", "rc0"] array<int> empty = [] // 空数组:必须显式写出类型 auto mixed = [1, "s"] println(type(mixed)) // array:混杂字面量推断为动态数组(不叫 array<var>)
三条记忆点:① 类型名是 float 不是 double——写 array<double> 报 [sem] 未知类型: double;② 空数组没有字面量可供推断,必须写清 array<int> empty = [];③ 混杂字面量本身不报错,auto 会把它推断为动态数组,type() 显示为 array;但反过来把它塞进静态声明就会被拒:
.length 是属性不是方法,静态与动态数组通用;空数组 length 为 0,可作空容器判断。注意容器本身不能用 string() 打印(报 [sem] 该表达式不能转换为目标类型),想看内容就遍历或按下标取。
下标从 0 开始,支持负下标(-1 是最后一个元素),读写皆可:
auto c = [10, 20, 30] println(string(c[0]) + " " + string(c[-1])) // 10 30 c[-1] = 99 // 负下标写入 println(string(c[2])) // 99
越界访问不是返回 null,而是进程直接终止,退出码 3,连报错文本都没有(RC0 实测:stdout、stderr 均无任何输出)。这与 var 字典缺失键返回 null 的宽松相反——数组把越界视为编程错误。经手外部输入的下标,先用 i < a.length 判断;第 10 章的 try/catch 也拦不住这类 fatal。
遍历三种形态:单变量得元素值;双变量得 下标, 值;需要步进或倒序时用 while 配 length:
for v in a println(string(v)) // 逐元素 end for i, v in a println(string(i) + ":" + string(v)) // 0:1 1:2 2:3 3:4 end
数组有三个运算符,但 + 与 == 各有一个必须知道的 RC0 缺陷:
auto x = [1, 2] auto y = [3] auto b = x + y println(string(b.length)) // 3:变量对变量的拼接正常 auto bad = x + [3] println(string(bad.length)) // 0:右侧是字面量,结果是空数组(缺陷!) x += [4] println(string(x.length)) // 3:+= 就地追加正常 println(string(x[2])) // 4 auto p = [1, 2] auto q = [1, 2] println(string(p == q)) // true:逐元素相等性比较
规律:+ 的两个操作数都先命名成变量就安全;+= 没有这个问题;== 比较两个变量正常,但把两个字面量直接写进比较([1,2] == [1,2])在 RC0 会让进程直接崩溃(段错误,退出码 139)——同样先赋变量再比较。var 动态数组不受 + 字面量缺陷影响:var v = [1,2]; v + [3] 结果正常。
方法名多为英文常用名。以下每一行都经过 RC0 实测,输出见注释:
auto m = [1, 2, 3] m.push(4) m.append(5) // append 是 push 的同义别名 println(string(m.length)) // 5 m.insert(0, 0) // 在下标 0 处插入 println(string(m[0]) + " " + string(m.length)) // 0 6 println(string(m.indexOf(3))) // 3:找到返回下标 println(string(m.indexOf(99))) // -1:找不到 println(string(m.contains(9))) // false:按值判断存在性 println(string(m.pop())) // 5:删尾并返回尾元素 println(string(m.take(0))) // 0:按下标删并返回该元素 println(string(m.remove(0))) // true:按下标删除,返回是否生效 println(string(m.removeValue(3))) // true:按值删除 m.reverse() // 就地反转 println(string(m[0]) + " " + string(m[1])) // 4 2 m.clear() // 清空 println(string(m.length)) // 0 auto r = [1, 2, 3, 4, 5] r.remove(1, 2) // 从下标 1 起连删 2 个 println(string(r.length) + " " + string(r[1])) // 3 4(r 现为 [1,4,5])
两组容易混淆的对子:
两个直觉上"应该有"的方法并不存在:sort 与 slice 在 RC0 均未实现。在静态数组上调用报 [sem] member key receiver must be var or dynamic dict,在 var 数组上调用报 [checked] 找不到 class 方法——两种报错都只是"没有这个方法"。排序自己写(本章练习 1),切片用循环按段复制(练习 2)。
var 声明的数组可异构混装,通用方法与静态数组一致,还多出两个专有方法:
var da = [1, "two", 3.5] println(string(da.length)) // 3 da.push(4) println(string(da.pop())) // 4 println(string(da.take(0))) // 1 println(string(da.contains("two"))) // true var s = [1, 2, 3] s.swap(0, 2) // 交换两个下标的元素 println(string(s[0]) + " " + string(s[2])) // 3 1 var r = [] r.reserve(100) // 预留容量,减少扩容次数 println(string(r.length)) // 0:reserve 不改 length
swap 与 reserve 是动态数组专有:静态数组调用它们报 [sem] member key receiver must be var or dynamic dict(见 16.9 坑 2)。需要 swap 的场景要么把声明改成 var,要么手写三行交换。
list 用整数作键但不要求连续——它是介于数组与 dict 之间的稀疏容器:
list<string> l = [0: "a", 2: "b", 5: "c"] println(string(l.length)) // 3:条目数,不含空洞 println(l[2]) // b for k, v in l println(string(k) + "=>" + v) // 0=>a 2=>b 5=>c end
list 与 array 的关键差异有四,全部实测:
l[3](不存在的键)进程直接终止(退出码 3,无输出),try/catch 拦不住。l[-1] 不是"最后一个元素",而是真的整数键 -1——没存过就 fatal。数组的负下标语义在 list 上不成立。l.contains("b") 得 true,l.contains(3) 直接报 [sem] container.contains value type mismatch: target string, value int;2 in l 得 false 而 "b" in l 得 true。RC0 没有判断"键是否存在"的手段——要么自己维护键的约定,要么改用 dict(第 17 章)用 string 键。运算符方面:+ 合并(右侧覆盖同键,注意与数组相同的"字面量右操作数得空"缺陷)、+= 可用;- 不存在,调用报 [sem] numeric operator operands must be numeric。方法有 remove(k) / take(k) / clear(),语义与数组一致:
list<int> a = [1: 10, 3: 30] list<int> ov = [3: 99, 5: 50] auto b = a + ov // 键 3 被覆盖为 99,新增键 5 println(string(b.length)) // 3 println(string(b[3]) + " " + string(b[5])) // 99 50 a += ov println(string(a.remove(1))) // true println(string(a.take(3))) // 99:删除并返回值 a.clear() println(string(a.length)) // 0
多维数组的元素类型是另一个 array<T>。array<array<int>> 声明加字面量初始化在 RC0 可以正常工作——但下一步的链式索引会卡住:
静态容器的元素取出后类型信息丢失,第二个下标的 receiver 被判非法。规避办法是两步法——先取行再取列。但要特别注意:静态数组取出的行是副本,改它不影响原矩阵;var 数组取出的行是共享的,改它即改原矩阵:
array<array<int>> mm = [[1, 2], [3, 4]] auto row = mm[1] println(string(row[0])) // 3:两步法读取正常 row[0] = 30 auto chk = mm[1] println(string(chk[0])) // 仍是 3:静态取行是副本,写不回! var m = [] m.push([1, 2, 3]) m.push([4, 5, 6]) println(string(m[0][1])) // 2:var 矩阵连链式索引都直接可用 auto r2 = m[1] r2[0] = 40 auto chk2 = m[1] println(string(chk2[0])) // 40:var 取行共享,写回生效
实践结论:要就地修改的矩阵用 var 构造(逐行 push),静态多维数组只适合只读场景(配合两步法读取)。双层 for-in 遍历对两者都正常。
第 12、15 章曾按文档口径把 var 容器描述为写时复制(COW)。本章以完整实测矩阵更正:RC0 中容器是共享引用语义,COW 在简单场景从未触发(两章原文已同步勘误)。四组实测:
// ① var 数组传 var 形参:函数内 push,调用方可见 func grow(var arr) arr.push(4) end var data = [1, 2, 3] grow(data) println(string(data.length)) // 4:可见! // ② var 赋值 var:共享,无副本分离 var a = [1, 2] var b = a b.push(3) println(string(a.length)) // 3:a 跟着变 b[0] = 99 println(string(a[0])) // 99:索引写同样可见 // ③ 静态数组传 var 形参:转换隔离,唯一"不可见"的情形 auto fixed = [1, 2, 3] grow(fixed) println(string(fixed.length)) // 3:静态→动态转换产生副本 // ④ 静态传静态:同样共享 func grow2(array<int> arr) arr.push(9) end grow2(fixed) println(string(fixed.length)) // 4:可见
规律只有一条:同构传递(var→var、array<T>→array<T>)共享底层数据;静态→var 的跨形态传递会复制转换,形成隔离。反方向也不通:var 容器传给静态 array<T> 形参报 [sem] dynamic var argument requires explicit 'as Type' conversion before overload resolution,需 as array<int> 显式转换。想保护调用方数据,在函数入口手工复制:
var local = [] for v in src local.push(v) end
RC0 对数组方法做两级校验:receiver 级(语义阶段)与方法存在级(checked 阶段)。静态数组调任何"不认识"的方法都先撞 receiver 检查,报 member key receiver;var 数组能过 receiver 关,再撞方法存在关,报"找不到 class 方法"。实测对照:
| 方法 | 静态 array<T> | var 动态数组 |
|---|---|---|
| push / append / insert / remove / removeValue / pop / take / clear / contains / indexOf / reverse | ✓ | ✓ |
| swap / reserve | ✗(member key receiver) | ✓ |
| sort / slice | ✗(member key receiver) | ✗(找不到 class 方法) |
记忆法:改变容量布局的(swap、reserve)归动态数组;只动元素的双向通用;没实现的(sort、slice)两边都报,只是报错文案不同。
m[0][1] 的失败不在运行期,而在 checked 阶段:类型检查器为下标表达式 m[0] 推导结果类型时,静态容器的泛型元素类型在 RC0 没有传递下去,第二个下标的 receiver 失去数组身份。var 矩阵之所以畅通,是因为 var 元素的下标访问走运行期分派,不依赖编译期类型推导。同理可以解释"静态取行是副本":auto row = mm[1] 触发的是一次值拷贝语义(静态数组元素的受管理副本),而 var 取行拿到的是原容器的引用。dict 里嵌套数组(第 17 章)也有同样的逐层取出纪律。
字符串一侧的 split / join / chars 由 string 命名空间提供(第 18 章详解);此处讲的是不依赖库函数的纯手工互转,也是理解"字符串可迭代"的最小例子——字符串 → 字符数组用 for-in 逐字符 push;数组 → 字符串用循环拼接:
var chars = [] for c in "abc" chars.push(c) // 单字符 string end println(string(chars.length)) // 3 string s = "" for c in chars s += c as string end println(s) // abc
第 18 章会系统讲字符串处理;这里的循环法是 RC0 下一切"分割拼接"需求的基础件。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:RC0 未实现 sort / slice(静态数组上则报 member key receiver 错)。正确做法:排序手写双重循环(练习 1);切片用循环按段 push 到新数组(练习 2)。
原因:swap / reserve 是动态数组专有方法。正确做法:声明改成 var;或手写三行交换(temp = a[i]; a[i] = a[j]; a[j] = temp)。
原因:RC0 对字面量右操作数的 + 会产出空数组;字面量直接参与 == 会崩溃。正确做法:+ 与 == 的操作数都先命名成变量再运算;+= 不受此限。
原因:list 读缺失键直接 fatal,try/catch 拦不住;负号在 list 上是普通整数键。更麻烦的是 contains/in 查的是值不是键,RC0 没有键存在判断。正确做法:键集合由程序自己维护约定;需要键存在判断的场景改用 dict(第 17 章)。
| 能力 | array<T> | var 数组 | list<T> |
|---|---|---|---|
| 元素/键类型 | 同质 T,连续下标 | 可异构,连续下标 | 整数键,可稀疏 |
| 负索引 | 尾部倒数 | 尾部倒数 | 普通整数键(非尾部) |
| 越界/缺键读 | fatal(退出码 3) | fatal | fatal |
| length | 元素数 | 元素数 | 条目数(不含空洞) |
| contains / in | 查值 | 查值 | 查值(无键判断手段) |
| 通用方法 | push pop take insert remove removeValue clear contains(indexOf/reverse 仅数组) | ||
| 专有方法 | — | swap reserve | — |
| 运算符 | + += ==(字面量操作数有缺陷) | + += == | + +=(无 -) |
| 传递语义 | 共享引用;仅静态→var 形参转换产生隔离副本 | ||
编写 sort_int.xl:写 func sortInt(array<int> a),用冒泡或插入排序就地升序排列(利用共享语义,无需返回)。用 [5, 2, 8, 1, 9, 3] 验证,排序后逐元素打印。
参考思路:双重循环:外层 i 从 0 到 n-2,内层 j 从 0 到 n-2-i,a[j] > a[j+1] 时三行交换(静态数组无 swap,手写 temp 交换)。
编写 slice.xl:写 func slice(var a, int from, int to) 返回新数组,含 from 不含 to,支持负参数(-1 表示末尾)。用 [10, 20, 30, 40, 50] 验证 slice(a, 1, 3) 与 slice(a, -2, 0)(0 约定为"到末尾")。
参考思路:先把负参数转正:from < 0 时 from += a.length;to 为 0 时视为 a.length;然后 while i < to 逐一下标 push 进结果 var 数组。
编写 ledger.xl:用 list<string> 模拟"只记录了部分天数"的体温台账(键为第几天,只记第 3、7、12 天),遍历时按 k=>v 打印,再演示 contains 按"值"查询(查有没有记过 "37.2")。
参考思路:字面量 [3: "36.5", 7: "37.2", 12: "36.8"];for k, v in t 打印;contains("37.2") 得 true。体会"contains 查值不查键"在本场景的用法。
编写 matrix.xl:用 var 数组构造 3×3 单位矩阵并打印;再写 func matMul(var A, var B) 计算两矩阵相乘返回新矩阵,用两个 2×2 矩阵验证。
参考思路:var m = [] 三行 push([0, 0, 0]),然后 auto r0 = m[0]; r0[0] = 1 依次做对角线(var 取行共享,写回生效)。相乘用三重循环,A 的第 i 行、B 的第 j 列对应元素乘积累加;取行后用下标取元素即可。