第4章:Solidity数据类型——值类型、引用类型与全局变量
说实话,数据类型这块,是每个Solidity开发者绕不开的基础。我见过太多新手在合约里因为类型搞混而丢币的案例了。今天咱们就把这块彻底捋清楚。
4.1 值类型:最基础的数据单元
值类型,说白了就是数据直接存在变量里的类型。你赋值给另一个变量时,会完整拷贝一份。就像你复印身份证,原件和复印件是独立的两张纸。
4.1.1 布尔型(bool)
只有两个值:true 和 false。简单吧?但坑也不少。
bool public isActive = true;
bool public isAdmin = false;
// 注意:Solidity里没有「真值」概念
// 你不能写 if(msg.sender) ,必须写 if(msg.sender != address(0))
! 运算符对布尔值的处理有细微差别。我有个朋友在条件判断里写了 if(!isActive),结果因为变量未初始化(默认false),逻辑完全反了。记住:未初始化的bool默认是 false。
4.1.2 整型(int/uint)
Solidity提供了从 uint8 到 uint256 的多种无符号整数,以及对应的有符号 int 系列。我个人习惯:能用 uint256 就别用 uint8,除非你明确知道数值范围。
uint256 public balance = 1000; // 无符号,不能为负
int256 public temperature = -5; // 有符号,可以为负
uint8 public smallNum = 255; // 最大255,再大就溢出了
unchecked 包裹,溢出不会报错但会回滚。我在做DeFi项目时,就利用这个特性优化过gas。
4.1.3 地址类型(address)
地址类型存储以太坊地址,20字节。它有两个变体:address 和 address payable。后者可以调用 transfer 和 send 方法。
address public owner = msg.sender;
address payable public recipient = payable(0x123...);
// 转换方式
address addr = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B;
address payable payAddr = payable(addr); // 显式转换
你想想看,为什么要有 payable?因为不是所有地址都需要接收以太币。合约地址默认不能接收转账,除非你显式声明。嗯,这里要注意:address payable 可以隐式转换为 address,但反过来不行。
4.2 引用类型:数据存在哪里很重要
引用类型和值类型最大的区别:变量存的是数据的「指针」或「引用」。赋值时不会拷贝数据,而是共享同一份数据。这就好比你把家里的钥匙给了别人,你们俩都能开门。
4.2.1 字符串(string)
Solidity的字符串其实是动态字节数组的封装。它不支持直接拼接或索引访问,这点和JavaScript完全不同。
string public greeting = "Hello, Web3!";
string public name = "Alice";
// 错误写法:不能直接拼接
// string full = greeting + name;
// 正确做法:用abi.encodePacked或bytes转换
function concat(string memory a, string memory b) public pure returns (string memory) {
return string(abi.encodePacked(a, b));
}
bytes32 更划算。
4.2.2 数组(array)
数组分固定长度和动态长度两种。固定数组在编译时确定大小,动态数组可以 push 和 pop。
// 固定数组
uint[5] public fixedArray = [1, 2, 3, 4, 5];
// 动态数组
uint[] public dynamicArray;
dynamicArray.push(10); // 添加元素
dynamicArray.push(20);
uint len = dynamicArray.length; // 获取长度
// 删除元素(不会缩小数组)
delete dynamicArray[0]; // 元素变为0,但长度不变
为什么会这样?因为Solidity的数组删除只是重置为默认值,不会改变数组长度。如果你想真正移除元素,得自己写循环或者用 pop。
4.2.3 映射(mapping)
映射是Solidity里最常用的数据结构之一。它像是一个哈希表,键可以是任意值类型,值可以是任意类型。
mapping(address => uint256) public balances;
mapping(uint256 => address) public idToOwner;
mapping(address => mapping(uint256 => bool)) public userPermissions;
// 使用
balances[msg.sender] = 100;
idToOwner[1] = msg.sender;
userPermissions[msg.sender][1] = true;
for 循环遍历所有键值对。我在做一个空投合约时,就因为想遍历映射而卡了两天。解决方案:配合数组记录所有键,或者用 EnumerableSet 库。
4.3 全局变量:Solidity给你的「超能力」
全局变量是Solidity内置的特殊变量,你不需要声明就能直接用。它们提供了区块链的上下文信息。
| 变量名 | 类型 | 说明 |
|---|---|---|
msg.sender |
address | 当前调用者地址 |
msg.value |
uint | 随交易发送的以太币数量(wei) |
block.number |
uint | 当前区块高度 |
block.timestamp |
uint | 当前区块时间戳(秒) |
tx.origin |
address | 交易发起者(整个调用链的起点) |
gasleft() |
uint | 剩余gas量 |
function deposit() public payable {
require(msg.value > 0, "必须发送以太币");
require(block.timestamp > startTime, "还没到开始时间");
balances[msg.sender] += msg.value;
// 注意:tx.origin 和 msg.sender 的区别
// tx.origin 是原始调用者,msg.sender 是直接调用者
emit Deposited(msg.sender, msg.value, block.number);
}
msg.sender 是当前函数的直接调用者,而 tx.origin 是整个交易链的起点。我在做多签钱包时,就利用 tx.origin 来防止中间合约的钓鱼攻击。但要注意:tx.origin 有安全风险,不建议在权限检查中使用。
4.4 实战中的类型选择建议
说了这么多,到底怎么选?我总结了几条实用原则:
- 能用值类型就别用引用类型——值类型操作更省gas,也更安全
- 存储字符串时优先考虑
bytes32——除非你真的需要动态长度 - 映射配合数组实现可遍历——单独用映射没法遍历,这是很多新手翻车的地方
- 全局变量别滥用——比如
block.timestamp可以被矿工轻微操纵,不适合做精确的时间锁 - 地址类型记得用
payable——如果你需要向该地址发送以太币
我记得有一次审计一个DeFi合约,开发者把所有地址都声明为 address,结果需要转账时还得强制转换,不仅代码难看,还多花了gas。这种细节,写的时候多花10秒,后面能省一小时。
好了,数据类型这块就聊到这儿。下一章咱们会深入函数修饰符和事件,这些东西在实际开发中天天用。有什么问题,欢迎在评论区交流。