【Solidity】引用类型, array, struct
发布时间:2025-09-01 00:00       
【从0学习Solidity】引用类型:array 与 struct 一次讲透
Solidity 引用类型的灵魂在于数据位置(data location)与深浅拷贝语义。只要吃透 storage / memory / calldata 的差异,再掌握数组与结构体的创建、读写与返回方式,就能写出既安全又省 gas 的合约。🚀
1. 数据位置与拷贝规则(核心原则)
| 位置 | 存在位置 | 读写成本 | 典型用法 | 拷贝/引用语义 |
|---|---|---|---|---|
storage |
链上持久化 | 高 | 状态变量、映射、存储数组/结构体 | S storage x = ...为引用;S memory y = x为深拷贝到内存 |
memory |
函数执行期内存 | 中 | 外部返回值、临时计算、返回 struct/数组 | memory之间赋值为深拷贝 |
calldata |
只读调用数据区 | 低 | external函数入参(数组/字符串/struct) |
只读视图,无拷贝(最省 gas) |
结论:能用
calldata就别用memory作入参;修改状态请拿到storage引用再写。
2. 数组(array):定长、动态、bytes/string
分类与基本操作
- 定长数组:
uint[3] a;长度编译期确定;不可push/pop,越界即异常。 - 动态数组(storage):
uint[] a;支持push(value)、pop()、delete a[i](置默认值)、delete a(清零长度)。 - 动态数组(memory):
new T[](len)分配后不可push/pop,只能按下标赋值。 bytes/string:本质是动态字节数组;string语义化为 UTF-8 文本,二进制更推荐bytes。读多写少的外部函数,参数请优先使用
bytes calldata/string calldata。
示例:数组入参/返回、storage 修改
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract Arrays {
uint256[] private store; // storage 动态数组
// 读多写少:用 calldata 最省 gas
function sum(uint256[] calldata xs) external pure returns (uint256 s) {
for (uint i; i < xs.length; i++) s += xs[i];
}
// 返回 memory 数组(深拷贝)
function snapshot() external view returns (uint256[] memory out) {
out = new uint256[](store.length);
for (uint i; i < store.length; i++) out[i] = store[i];
}
// 修改 storage:必须拿 storage 引用或直接操作状态变量
function append(uint256 v) external { store.push(v); }
function popLast() external {
require(store.length > 0, "EMPTY");
store.pop();
}
}
高频坑位
- memory 数组不能
push/pop:只能new T[](len)后按下标赋值。 - 不要给数组
.length赋值(storage 动态数组):请用push/pop。 - 大数组返回昂贵且可能超限:优先设计分页、事件(event)或仅返回摘要哈希。
delete a[i]不等于移除:它仅置零,不会改变顺序或长度;“删除并压缩”请自行移动元素或使用“末尾覆盖 + pop”技巧。
3. 结构体(struct):组合数据与存储布局
定义与使用要点
struct可包含值类型、bytes/string、数组、mapping(仅storage)。- storage 引用修改原数据;memory 变量是拷贝。
- 返回给外部时,
struct会被拷贝到 memory 并 ABI 编码。 - 存储打包:按 32 字节槽位打包,小类型(如
bool/uint8/uint16)相邻更省空间。
示例:结构体 + 动态数组 + 安全更新
pragma solidity ^0.8.25;
contract Students {
struct Student {
uint256 id;
uint64 joinedAt; // 小类型靠拢有助于打包
uint8 age;
bytes32 tag;
string name; // 动态数据单独存储
uint256[] scores; // 动态数组
}
Student[] private list; // 学生表
mapping(uint256 => uint256) idxPlus1; // id => index+1(0 表示不存在)
function add(Student memory s) external {
require(idxPlus1[s.id] == 0, "EXISTS");
list.push(); // 先扩容
Student storage st = list[list.length - 1]; // 取 storage 引用
st.id = s.id;
st.joinedAt = uint64(block.timestamp);
st.age = s.age;
st.tag = s.tag;
st.name = s.name; // memory → storage 拷贝
st.scores = s.scores; // 动态数组内容拷贝
idxPlus1[s.id] = list.length;
}
function addScore(uint256 id, uint256 v) external {
uint256 k = idxPlus1[id]; require(k != 0, "NOT_FOUND");
list[k - 1].scores.push(v); // 直接改 storage
}
// 对外返回 struct:自动拷贝为 memory 再编码
function get(uint256 id)