第五章 | Solidity 数据类型深度解析
📚 第五章 | Solidity 数据类型深度解析
——彻底搞清类型用法,合约更稳,Gas 更省!
✅ 本章导读
在 Solidity 合约开发中,“类型”决定了一切。
数据类型不仅影响存储和性能,也直接关系到安全性和 Gas 成本。
很多人写合约踩过这些坑:
storage
和memory
傻傻分不清calldata
不会用,导致前端传参效率低address
和address payable
弄混,合约收不到 ETH- 数据结构乱写,Gas 飙升,代码臃肿
- 没搞懂映射和数组,数据丢失或者被覆盖
这一章我们不打哑谜,手把手讲明白每个数据类型背后的逻辑和最佳实战方案。
✅ 本章你将掌握
- Solidity 基础类型细讲
- 存储位置:memory、storage、calldata
- address 和 address payable
- 数组、映射、结构体的进阶用法
- 自定义错误(Error)节省 Gas
- 实战案例 + 最佳实践
1️⃣ 基础数据类型
👉 布尔类型(bool)
- 取值只有
true
和false
- 占用 1 个字节空间(256 位中的 8 位)
bool public isActive = true;
❗ 开发建议
- 少用多个
bool
紧挨着声明,否则布局不优(占满 32 字节)。 - 推荐用
uint
表示状态,配合enum
做状态机。
👉 整型(uint / int)
uint
(无符号整数)
- 只接受正整数
- 范围:
0
到2^256-1
uint256 public totalSupply = 10000;
int
(有符号整数)
- 可以存储正数和负数
- 范围:
-2^255
到2^255-1
int256 public balance = -100;
❗ 开发建议
- 默认用
uint256
,兼容性最好 - 不建议用
uint8
、uint16
拼凑优化(可能引发安全隐患)
👉 枚举(enum)
定义有限状态,比如订单状态、投票状态。
enum Status { Pending, Shipped, Delivered }
Status public status;
function ship() public {
status = Status.Shipped;
}
❗ 开发建议
- 枚举用
uint8
存储,更省空间 - 前端配合展示状态解释,链上只存
enum
索引
2️⃣ 存储位置关键字(storage / memory / calldata)
关键字 | 说明 | 典型应用 |
---|---|---|
storage | 永久存储在区块链,读写都贵 | 状态变量 |
memory | 临时存储在内存,函数结束清除 | 临时变量 |
calldata | 外部函数输入参数,只读存储最省 Gas | 函数参数 |
👉 storage
- 读取/写入持久化数据,所有节点都保留副本
- 需要谨慎使用,否则 Gas 会爆炸
string public name;
function updateName(string memory _newName) public {
name = _newName; // 写入 storage,消耗较大
}
👉 memory
- 函数临时变量,生命周期只在当前函数
- 写复杂逻辑或中间处理常用
function concat(string memory a, string memory b) public pure returns (string memory) {
return string(abi.encodePacked(a, b));
}
👉 calldata
- 外部函数输入参数最省 Gas
- 只读,不能被修改
function register(string calldata _username) external {
users[msg.sender] = _username; // 节省 Gas,推荐用法
}
❗ 实战结论
- 外部只读参数优先用
calldata
- 写复杂逻辑时用
memory
- 状态变量操作才用
storage
calldata
不能直接赋值到storage
,必须先memory
3️⃣ 地址类型(address / address payable)
👉 address
- 标准的以太坊地址类型
- 可以调用合约、查询余额,但不能收 ETH
address public owner = msg.sender;
👉 address payable
- 可以
transfer()
/send()
/call{value:}
转账 ETH
address payable public fundReceiver;
constructor() {
fundReceiver = payable(msg.sender);
}
function withdraw(uint256 _amount) public {
fundReceiver.transfer(_amount);
}
❗ 实战技巧
- 任何
address
转payable
,加payable()
强转 transfer()
限 Gas(2300),不推荐大额转账- 推荐
call{value:}
发送 ETH
(bool success, ) = fundReceiver.call{value: _amount}("");
require(success, "Transfer failed");
4️⃣ 数组、映射、结构体进阶用法
👉 数组(Array)
- 动态或定长
- 支持
push()
/pop()
uint[] public numbers;
function add(uint _num) public {
numbers.push(_num);
}
function removeLast() public {
numbers.pop();
}
❗ 注意
- 数组删除元素时需要手动处理索引
- 避免数组循环操作,Gas 昂贵
push()
返回值是元素的索引
👉 映射(mapping)
- 键值对
- 不支持遍历
- 默认值 0 / false / 空
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
❗ 注意
- 不可枚举,前端监听事件收集数据
- 可嵌套:
mapping(address => mapping(uint => bool))
- 避免层级太深,增加复杂性
👉 结构体(struct)
- 自定义数据结构
- 配合
mapping
存储最常用
struct User {
string name;
uint age;
}
mapping(address => User) public users;
function register(string memory _name, uint _age) public {
users[msg.sender] = User(_name, _age);
}
5️⃣ 自定义错误(Error)优化 Gas
👉 什么是自定义错误?
比 require()
更节省 Gas,推荐用法。
Solidity 0.8.4+ 引入。
✅ 定义错误
error Unauthorized(address caller);
✅ 使用错误
function withdraw() public {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
// withdraw logic
}
❗ 优势
- Gas 节省明显
- 错误信息更清晰
- 支持自定义参数,前端更友好
6️⃣ 实战案例 | 安全的转账合约
✅ 场景
用户向合约打款,合约 owner 可以提现。
✅ 合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SafeWallet {
address public owner;
error NotOwner(address caller);
constructor() {
owner = msg.sender;
}
receive() external payable {}
function withdraw(uint256 _amount) external {
if (msg.sender != owner) {
revert NotOwner(msg.sender);
}
(bool success, ) = payable(owner).call{value: _amount}("");
require(success, "Transfer failed");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
✅ 合约亮点
receive()
接收 ETHcall{value:}
发送 ETH- 自定义错误
NotOwner
节省 Gas getBalance()
查询合约余额
✅ 小结
已掌握? ✅ | 内容 |
---|---|
✅ | bool / uint / int / enum 基本类型 |
✅ | memory / storage / calldata 存储优化 |
✅ | address payable ETH 转账最佳实践 |
✅ | 数组 / 映射 / 结构体进阶 |
✅ | 自定义错误 Error 节省 Gas |
🎯 作业挑战
- 写一个“用户注册”合约:
- 用户注册姓名、年龄
- 注册后不可修改
- 只能管理员删除用户
- 每次注册触发事件
- 尝试用
calldata
优化函数参数 - 加入自定义错误处理
- 用 Hardhat 测试注册、删除功能
✅ 下一章预告|第六章
👉 函数与可见性修饰符全面讲解
🚀 函数内部/外部调用优化
🚀 构造函数、回退函数最佳用法
🚀 public
、private
到 external
的安全权衡
🚀 实战构建 DAO 投票系统基础框架