什么是 Solidity Array?动态数组如何增删,为什么 delete 不会自动缩短长度

Solidity Array 按索引保存同类元素。本文讲清固定与动态数组、数据位置、push/pop、delete、删除中间元素、Gas、遍历与索引风险。

什么是 Solidity Array?动态数组如何增删,为什么 delete 不会自动缩短长度

Solidity 新手经常写出这样一行代码:delete users[i],然后以为第 i 个元素已经从数组里拿走,后面的元素会自动前移,length 也会减一。实际结果却是位置还在,只是内容恢复成默认值。这个小误解会进一步变成重复记录、错误计数,甚至让链上遍历永远处理不完。

Solidity Array 是按索引保存同一类型元素的有序容器。理解它,不能只看方括号语法,还要同时看长度是固定还是动态、数据位于 storage、memory 还是 calldata。

固定数组与动态数组

uint256[3] 是固定长度数组,声明时长度已经写进类型;uint256[] 是动态数组,长度可以在 storage 中通过 push 和 pop 改变。两者的元素类型都必须一致,索引从 0 开始。

uint256[3] public fixedScores; // 永远有 3 个位置
uint256[] public scores;      // storage 动态数组

function add(uint256 value) external {
    scores.push(value);
}

固定数组不是“没有 length”,而是 length 不会变。动态也不代表任何位置都能随便写:如果当前 scores.length 为 2,直接写 scores[2] = 10 会越界。必须先 push 增加位置,或在创建 memory 数组时确定长度。

三个数据位置改变了哪些行为

位置生命周期长度与修改
storage状态持久保存在链上动态数组可用 push/pop;写入通常消耗 Gas
memory仅存在于一次函数执行用 new T[](n) 创建后长度固定,不能 push/pop
calldata外部调用的只读输入不可修改,适合只读取参以避免不必要复制

“动态数组”描述的是类型是否带固定编译期长度,不等于它在所有数据位置都能动态扩容。memory 中的 new uint256[](n) 可以让 n 在运行时决定,但创建后长度仍固定。

Solidity 数组长度索引和数据位置的配图
数组行为由元素类型、长度形式与数据位置共同决定,不能只看一对方括号。

push、pop 和 length 的准确含义

最新 Solidity 里,storage 动态数组可以调用 push() 添加一个默认值元素,也可以调用 push(value) 追加指定值。无参数 push() 会返回新元素的 storage 引用,适合先开位置再填写 struct 字段;带值版本不返回新长度。

pop() 删除最后一个元素并让长度减一,同时会对被移除元素执行清理。若元素本身很大或包含嵌套数组,清理成本也可能增加。数组的 length 是只读成员,不能靠 length-- 缩短。

struct Item {
    address owner;
    uint256 amount;
}

Item[] public items;

function create(uint256 amount) external {
    Item storage item = items.push();
    item.owner = msg.sender;
    item.amount = amount;
}

function removeLast() external {
    require(items.length != 0, "empty");
    items.pop();
}

为什么 delete arr[i] 不会缩短数组

delete 的通用语义是把目标恢复为该类型的默认值。对 uint256 是 0,对 address 是零地址,对 bool 是 false,对 struct 则重置其中可重置的字段。因此 delete arr[i] 只清理第 i 个槽位,其他元素和数组长度保持不变。

uint256[] numbers = [10, 20, 30];
delete numbers[1];
// 结果相当于 [10, 0, 30],length 仍为 3

这不是编译器遗漏,而是可预测的数据语义:如果每次删除都自动移动后续元素,删除一个位置的成本会随数组长度增长,而且已有索引会全部改变。

Solidity delete 数组元素后留下默认值位置的配图
delete 是清零,不是移位删除;是否保序决定你该采用哪种删除算法。

删除中间元素的两种常见方案

方案一:移动元素,保持顺序

function removeOrdered(uint256 index) external {
    require(index < numbers.length, "index");
    for (uint256 i = index; i + 1 < numbers.length; ++i) {
        numbers[i] = numbers[i + 1];
    }
    numbers.pop();
}

这种方式符合普通列表直觉,但需要移动后续每个元素,复杂度为 O(n)。数组可能很长时,一笔交易会因 Gas 太高而失败。不要让任何用户都能把数组无限扩张,再让关键业务依赖全量移动。

方案二:swap and pop,不保持顺序

function removeFast(uint256 index) external {
    require(index < numbers.length, "index");
    uint256 last = numbers.length - 1;
    if (index != last) numbers[index] = numbers[last];
    numbers.pop();
}

它用最后一个元素覆盖待删位置,再弹出末尾,操作数量基本固定,适合集合式数据。但顺序会改变,外部系统如果把索引当稳定 ID 就会出错。更稳妥的做法是为记录设置独立 ID,并在需要 O(1) 删除时维护 id => index 映射;交换后别忘了更新被搬动元素的新索引。

留下“空洞”有时也是设计选择

如果索引必须永久稳定,可以不移动元素,而给记录增加 active 标志,删除时将其设为 false。代价是读取方必须跳过无效项,数组会持续膨胀,链上全量遍历越来越贵。你需要分页、计数和离链索引,不能假设“以后再清理”一定可行。

单独使用默认值判断是否存在也容易含糊,因为 0、false、零地址可能本来就是合法输入。struct 中显式的 exists 或 active 字段,往往比猜默认值更清楚。

什么时候应该用 mapping 而不是 array

需求更常见选择原因
按地址或 ID 快速读取mapping无需遍历查找键
需要顺序与按索引分页array天然保存顺序和 length
既要快速查找又要枚举mapping + array分别承担索引与列表职责,但要维护一致性
集合成员增删且不关心顺序带索引映射的 array可用 swap and pop 实现近似 O(1) 删除

mapping 没有内建长度,也不能列出所有键;array 能枚举,却不适合按值线性搜索。很多成熟设计会组合两者,但每多一份索引,就多一份更新遗漏的风险。新增、交换、删除必须在同一交易中同步完成并配套测试。

public 数组 getter 的一个误区

把状态数组声明为 public 后,编译器会生成按索引读取的 getter,并不等于免费生成“返回整个数组”的接口。前端通常要先读取长度,再分页或逐项查询;更复杂的产品会依赖事件与离链索引服务。

直接在链上函数里返回或遍历超大数组,可能造成 RPC 响应、内存和 Gas 问题。视图函数在外部调用时不收交易 Gas,不代表节点计算没有上限,也不代表另一个合约在链上调用它时免费。

数组还容易踩哪些坑

  • 越界:读取或写入不存在索引会触发失败,不要只相信前端传参。
  • 下溢:空数组上计算 length - 1 会失败,应先检查非空。
  • storage 引用:局部 storage 变量是别名,修改它会改链上原对象;memory 通常是副本。
  • 嵌套清理:元素越复杂,pop 或 delete 的实际存储工作越多。
  • 重复项:若业务要求唯一,单靠 array 不会自动阻止重复,需要额外映射或校验。
  • 无界循环:由不断增长数组控制的关键交易,迟早可能超过区块 Gas 预算。
  • 索引即身份:使用 swap and pop 后索引会变化,事件与前端应使用稳定业务 ID。

上线前检查清单

  1. 明确使用固定还是动态数组,以及具体数据位置。
  2. 所有外部 index 都验证 index < array.length。
  3. 空数组调用 pop 或计算末尾索引前先检查。
  4. 确认删除是否必须保序,再选择移动、swap and pop 或 active 标志。
  5. 如果同时维护 mapping 和 array,测试新增、删除、交换和重复操作的一致性。
  6. 为数组设置合理上限或分页路径,避免关键流程依赖无界循环。
  7. 用事件提供稳定 ID 和变更信息,让离链索引不依赖易变位置。
  8. 针对默认值、重复值、首尾位置和只有一个元素的情况写测试。

Solidity 官方类型文档说明了数组的 length、storage 动态数组的 push/pop,以及 memory 数组创建后长度固定;同页的 delete 说明明确指出,删除单个数组元素不会改变其他元素和数组长度。


本文仅用于智能合约技术科普,不构成投资建议、安全审计结论或部署承诺。合约一经部署可能承载真实资产,请锁定编译器版本,编写边界测试,并在生产上线前接受独立安全审计。