# 顺序集合:收集任务与容量扩展
学习目标:理解动态列表、切片与固定数组在内存追加、索引访问与遍历上的核心差异。
前置知识与范围:基本数据类型与循环。演示在内存中动态维护有序的任务字符串列表。
# 问题与边界
模拟按到达顺序收集任务项:
- 初始化一个空的动态集合。
- 依次追加三个任务:
"build","test","deploy"。 - 契约要求:集合保持插入顺序,总长度为 3;首个元素为
"build",末尾元素为"deploy"。 - 越界访问必须在语言层面引发安全拦截(如抛出异常或返回 Option),不得产生未定义内存悬垂。
# 关键差异
| 语言 | 动态顺序结构 | 追加操作 | 长度与容量 | 越界保护机制 |
|---|---|---|---|---|
| Rust | Vec<T> |
vec.push(item) |
len() / capacity() |
vec[i] 索引越界触发 panic;vec.get(i) 返回 Option |
| Go | 切片 []T |
slice = append(slice, item) |
len(s) / cap(s) |
越界引发运行时 panic |
| Java | ArrayList<T> |
list.add(item) |
size() |
IndexOutOfBoundsException |
| JavaScript | Array |
arr.push(item) |
arr.length |
越界访问返回 undefined,不报错! |
| TypeScript | 沿用 JS Array |
arr.push(item) |
arr.length |
默认类型为 T,越界返回 undefined 存在类型盲区 |
| Kotlin | ArrayList<T> / mutableListOf |
list.add(item) |
list.size |
IndexOutOfBoundsException |
| Python | 原生 list |
list.append(item) |
len(l) |
IndexError |
| C++ | std::vector<T> |
vec.push_back(item) |
vec.size() / vec.capacity() |
vec[i] 不检查越界(UB);vec.at(i) 抛出 std::out_of_range |
# 各语言实现
# Rust
Rustfn main() {
let mut tasks: Vec<&str> = Vec::new();
tasks.push("build");
tasks.push("test");
tasks.push("deploy");
assert_eq!(tasks.len(), 3);
assert_eq!(tasks[0], "build");
assert_eq!(tasks[2], "deploy");
assert_eq!(tasks.get(3), None); // 安全访问边界
println!("tasks: {}, first: {}", tasks.len(), tasks[0]);
}
Rust 的 Vec 在连续堆内存中动态增长;get() 返回安全保护的 Option。
# Go
Gopackage main
import "fmt"
func main() {
var tasks []string
tasks = append(tasks, "build")
tasks = append(tasks, "test")
tasks = append(tasks, "deploy")
if len(tasks) != 3 || tasks[0] != "build" || tasks[2] != "deploy" {
panic("mismatch")
}
fmt.Printf("tasks: %d, first: %s\n", len(tasks), tasks[0])
}
Go 的切片是对底层数组的轻量三元头(指针、长度、容量)。append 在容量不足时会分配新底层数组,因此必须重新赋值给切片变量。
# Java
Javaimport java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> tasks = new ArrayList<>();
tasks.add("build");
tasks.add("test");
tasks.add("deploy");
assert tasks.size() == 3;
assert "build".equals(tasks.get(0));
assert "deploy".equals(tasks.get(2));
System.out.println("tasks: " + tasks.size() + ", first: " + tasks.get(0));
}
}
Java 推荐面对接口 List 编程,底层由可扩容数组 ArrayList 支撑。
# JavaScript
JavaScriptconst assert = {
equal: (a, b) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a, b) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn, errType) => {
try { fn(); throw new Error("expected throw"); } catch (e) {
if (e.message === "expected throw") throw e;
if (errType && !(e instanceof errType)) throw e;
}
}
};
const tasks = [];
tasks.push("build");
tasks.push("test");
tasks.push("deploy");
assert.equal(tasks.length, 3);
assert.equal(tasks[0], "build");
assert.equal(tasks[2], "deploy");
assert.equal(tasks[3], undefined); // 越界返回 undefined
console.log(`tasks: ${tasks.length}, first: ${tasks[0]}`);
JavaScript 的数组是动态异构对象,索引越界不会抛出异常而是直接求值为 undefined。
# TypeScript
TypeScriptconst assert = {
equal: (a: unknown, b: unknown) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a: unknown, b: unknown) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn: () => void, errType?: unknown) => {
try { fn(); throw new Error("expected throw"); } catch (e: unknown) {
if (e instanceof Error && e.message === "expected throw") throw e;
if (typeof errType === "function" && !(e instanceof errType)) throw e;
}
}
};
const tasks: string[] = [];
tasks.push("build");
tasks.push("test");
tasks.push("deploy");
assert.equal(tasks.length, 3);
assert.equal(tasks[0], "build");
assert.equal(tasks[2], "deploy");
console.log(`tasks: ${tasks.length}, first: ${tasks[0]}`);
注意开启 noUncheckedIndexedAccess 时,TypeScript 会将 tasks[0] 视为 string | undefined 以防御越界假定。
# Kotlin
Kotlinfun main() {
val tasks = mutableListOf<String>()
tasks.add("build")
tasks.add("test")
tasks.add("deploy")
check(tasks.size == 3)
check(tasks[0] == "build")
check(tasks[2] == "deploy")
println("tasks: ${tasks.size}, first: ${tasks[0]}")
}
Kotlin 在类型层严格划分只读接口 List<T> 与可变接口 MutableList<T>。
# Python
Pythonif __name__ == "__main__":
tasks = []
tasks.append("build")
tasks.append("test")
tasks.append("deploy")
assert len(tasks) == 3
assert tasks[0] == "build"
assert tasks[-1] == "deploy" # 支持负索引反向访问
print(f"tasks: {len(tasks)}, first: {tasks[0]}")
Python 的内置列表类型底层为动态指针数组,支持通过负数索引反向访问末尾项。
# C++
C++#include <iostream>
#include <vector>
#include <string>
#include <cassert>
int main() {
std::vector<std::string> tasks;
tasks.push_back("build");
tasks.push_back("test");
tasks.push_back("deploy");
assert(tasks.size() == 3);
assert(tasks[0] == "build");
assert(tasks.at(2) == "deploy");
std::cout << "tasks: " << tasks.size() << ", first: " << tasks[0] << "\n";
}
C++ 中 operator[] 出于性能不进行范围检查,若需要越界安全检查应使用成员函数 at()。
# 迁移误区与适用限制
- 预分配容量的重要性:已知元素总数时,预分配容量(如
Vec::with_capacity、ArrayList(size)、make([]T, 0, cap))能避免多次堆内存重新分配与数据搬移。 - JS 数组空洞与越界盲区:在 JS 中给
arr[100] = 'x'赋值会形成稀疏数组;索引越界不报错的特性经常导致undefined渗透并在后续属性访问时暴发TypeError。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本tasks: 3, first: build
元素顺序、长度与索引访问断言全部通过。