3. Solidity数据类型(上):布尔型、整型、地址类型、字节数组
好,咱们正式开始接触 Solidity 的“血肉”了——数据类型。
说实话,很多新手学 Solidity 容易卡在这一块。为什么?因为区块链上的数据类型,跟普通编程语言比,多了不少“心眼”。比如地址类型,你写个普通程序哪来什么 address?但智能合约里,它就是命根子。
这一节,咱们先啃掉四个最基础、也最常用的:布尔型、整型、地址类型、字节数组。我保证,讲完这些,你写个简单的存证合约、转账合约,手到擒来。
3.1 布尔型(bool)—— 最简单的开关
布尔型,说白了就是“是”或“否”。在 Solidity 里,它只有两个值:true 和 false。
// 声明一个布尔变量
bool public isActive = true;
// 在函数里用
function toggle() public {
isActive = !isActive; // 取反
}
function check() public view returns (bool) {
return isActive;
}
嗯,这里要注意:Solidity 的布尔型不支持隐式转换。你不能像在 C 语言里那样,写个 if(1) 就当成 true。必须写成 if(1 == 1) 或者 if(boolVar)。
我曾经在审计一个众筹合约时,发现有人用
if(msg.value) 来判断是否转账。这在 Solidity 0.8 之前是可行的(因为 0 被视为 false),但 0.8 之后编译器会直接报错。所以,永远显式比较:if(msg.value > 0)。
布尔型的操作符也很简单:!(非)、&&(与)、||(或)、==(等于)、!=(不等于)。
我写合约时,布尔变量命名喜欢用
isXxx 或 hasXxx 开头。比如 isPaused、hasVoted。这样读代码时一眼就能看出它的语义。
3.2 整型(int / uint)—— 小心别溢出
整型是 Solidity 里最“坑”的数据类型之一。为什么?因为区块链上的整数是固定长度的。
Solidity 提供两种整型:
uint:无符号整数(只能是非负数)。从uint8到uint256,步长 8。int:有符号整数(可正可负)。从int8到int256,步长 8。
如果不指定长度,默认就是 uint256 和 int256。我个人建议:能用 uint 就别用 int,除非你真的需要负数。因为 uint 的运算在 gas 上更省。
uint256 public count = 0;
int8 public temperature = -10; // 有符号,范围 -128 到 127
function increment() public {
count += 1;
}
// 注意:uint8 的范围是 0 到 255
uint8 public smallNum = 255;
function addOne() public {
// 这行会报错!因为 255 + 1 = 256,超出了 uint8 的范围
// smallNum += 1;
// 正确做法:使用 unchecked 块(Solidity 0.8+)
unchecked {
smallNum += 1; // 会溢出变成 0
}
}
你想想看,为什么 Solidity 0.8 之后默认会检查溢出?因为我在 2018 年参与过一个 DeFi 项目,当时用的 0.4 版本,结果一个整数溢出漏洞导致几十万美金的损失。从那以后,Solidity 团队就把溢出检查做成了默认行为。
- Solidity 0.8+:默认算术运算会检查溢出,溢出则回滚交易
- 使用
unchecked { ... }可以关闭溢出检查(节省 gas) - 使用
type(uint256).max可以获取该类型的最大值
3.3 地址类型(address)—— 合约的身份证
地址类型是 Solidity 独有的,也是最重要的类型之一。它代表一个以太坊账户地址,长度 20 字节(160 位)。
// 普通地址
address public owner = 0x1234567890123456789012345678901234567890;
// payable 地址(可以接收 ETH)
address payable public receiver = payable(0x...);
// 从地址转账
function sendEther() public {
// 注意:只有 payable 地址才能调用 transfer 和 send
receiver.transfer(1 ether);
}
// 获取合约的地址
address public contractAddress = address(this);
这里有个重要的区别:address 和 address payable。后者多了两个方法:transfer() 和 send()。说白了,如果你想从合约里转 ETH 出去,接收方必须是 address payable 类型。
我曾经见过一个新手写的合约,他想把 ETH 转给用户,但接收地址声明成了
address 而不是 address payable。结果编译报错,他折腾了半天才发现是类型不匹配。记住:转账用 payable,查询用普通 address。
地址类型还有一些内置成员:
.balance:查询地址的余额(单位 wei).code:查询地址上的合约代码(如果是合约地址).codehash:合约代码的哈希值
function getBalance(address addr) public view returns (uint256) {
return addr.balance;
}
function isContract(address addr) public view returns (bool) {
return addr.code.length > 0;
}
3.4 字节数组(bytes)—— 灵活的数据容器
字节数组在 Solidity 里分两种:固定大小和动态大小。
3.4.1 固定大小字节数组
从 bytes1 到 bytes32,每个占 1 到 32 字节。我个人习惯用 bytes32 来存储哈希值或短字符串。
bytes32 public hash = keccak256(abi.encodePacked("hello"));
bytes4 public selector = bytes4(keccak256("transfer(address,uint256)"));
// 固定大小字节数组可以比较
function compare(bytes32 a, bytes32 b) public pure returns (bool) {
return a == b;
}
3.4.2 动态大小字节数组
bytes 是动态数组,可以任意长度。它和 string 很像,但 bytes 更适合处理原始字节数据。
bytes public data = hex"001122ff";
function append(bytes memory newData) public {
data = abi.encodePacked(data, newData);
}
// 获取长度
function getLength() public view returns (uint256) {
return data.length;
}
如果你要存储任意长度的字符串,用
string;如果你要处理二进制数据(比如签名、哈希),用 bytes。记住:string 底层其实就是 bytes,但加了 UTF-8 编码的约束。
3.5 类型转换——小心别丢数据
Solidity 的类型转换比较严格。我总结了几条规则:
- 隐式转换:只能从“小”转“大”。比如
uint8转uint256没问题。 - 显式转换:用
type(value)语法。比如uint8(256)会截断成 0。 - 地址转换:
address(uint160(addr))可以把整数转成地址。
uint8 small = 255;
uint256 big = small; // 隐式转换,没问题
uint256 large = 1000;
uint8 truncated = uint8(large); // 显式转换,结果变成 232(1000 - 256*3)
// 地址和整数的互转
address addr = 0x...;
uint160 addrAsInt = uint160(addr);
address backToAddr = address(addrAsInt);
我曾经审计过一个 NFT 合约,开发者在计算 tokenId 时用了
uint8 来接收一个 uint256 的输入。结果用户传入 300 时,实际存储的 tokenId 变成了 44。这种 bug 在测试时很难发现,但上线后就会造成严重的资产混乱。
3.6 实战:写一个简单的存证合约
好了,理论讲完了。咱们来写个真正能用的东西——一个数据存证合约。它可以把一段数据的哈希值存到链上,证明某个时间点你拥有这份数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DataNotary {
// 存储数据的哈希和提交时间
mapping(bytes32 => uint256) public proofs;
// 事件:当数据被存证时触发
event DataStored(bytes32 indexed hash, uint256 timestamp);
// 存证数据
function store(bytes32 dataHash) public {
// 要求数据没有被存证过
require(proofs[dataHash] == 0, "Data already exists");
proofs[dataHash] = block.timestamp;
emit DataStored(dataHash, block.timestamp);
}
// 验证数据是否存在
function verify(bytes32 dataHash) public view returns (bool) {
return proofs[dataHash] > 0;
}
// 获取存证时间
function getTimestamp(bytes32 dataHash) public view returns (uint256) {
require(proofs[dataHash] > 0, "Data not found");
return proofs[dataHash];
}
}
这个合约用到了我们刚学的:
bytes32:存储哈希值uint256:存储时间戳bool:验证结果address:虽然没直接用到,但msg.sender就是地址类型
嗯,到这里,Solidity 的四个基础数据类型就讲完了。下一节咱们会继续讲引用类型——数组、结构体、映射。那些才是真正让合约“活”起来的东西。
记住我今天说的:类型安全是智能合约的第一道防线。写合约时多想想“这个变量会不会溢出?”“这个地址能不能收 ETH?”,能帮你省下不少 debug 的时间。