第4章:Solidity数据类型——值类型、引用类型与全局变量

说实话,数据类型这块,是每个Solidity开发者绕不开的基础。我见过太多新手在合约里因为类型搞混而丢币的案例了。今天咱们就把这块彻底捋清楚。

4.1 值类型:最基础的数据单元

值类型,说白了就是数据直接存在变量里的类型。你赋值给另一个变量时,会完整拷贝一份。就像你复印身份证,原件和复印件是独立的两张纸。

4.1.1 布尔型(bool)

只有两个值:truefalse。简单吧?但坑也不少。

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提供了从 uint8uint256 的多种无符号整数,以及对应的有符号 int 系列。我个人习惯:能用 uint256 就别用 uint8,除非你明确知道数值范围。

uint256 public balance = 1000;  // 无符号,不能为负
int256 public temperature = -5; // 有符号,可以为负
uint8 public smallNum = 255;    // 最大255,再大就溢出了
核心要点: Solidity 0.8.0 之后默认带溢出检查。如果你用 unchecked 包裹,溢出不会报错但会回滚。我在做DeFi项目时,就利用这个特性优化过gas。

4.1.3 地址类型(address)

地址类型存储以太坊地址,20字节。它有两个变体:addressaddress payable。后者可以调用 transfersend 方法。

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));
}
💡 我的经验: 字符串操作非常耗gas。我在一个NFT项目里,因为频繁拼接字符串,导致mint费用暴涨。后来改用bytes32存储短字符串,gas省了将近40%。如果你只需要存储短文本(32字节以内),用 bytes32 更划算。

4.2.2 数组(array)

数组分固定长度和动态长度两种。固定数组在编译时确定大小,动态数组可以 pushpop

// 固定数组
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 实战中的类型选择建议

说了这么多,到底怎么选?我总结了几条实用原则:

  1. 能用值类型就别用引用类型——值类型操作更省gas,也更安全
  2. 存储字符串时优先考虑 bytes32——除非你真的需要动态长度
  3. 映射配合数组实现可遍历——单独用映射没法遍历,这是很多新手翻车的地方
  4. 全局变量别滥用——比如 block.timestamp 可以被矿工轻微操纵,不适合做精确的时间锁
  5. 地址类型记得用 payable——如果你需要向该地址发送以太币

我记得有一次审计一个DeFi合约,开发者把所有地址都声明为 address,结果需要转账时还得强制转换,不仅代码难看,还多花了gas。这种细节,写的时候多花10秒,后面能省一小时。

好了,数据类型这块就聊到这儿。下一章咱们会深入函数修饰符和事件,这些东西在实际开发中天天用。有什么问题,欢迎在评论区交流。