3. Solidity数据类型(上):布尔型、整型、地址类型、字节数组

好,咱们正式开始接触 Solidity 的“血肉”了——数据类型。

说实话,很多新手学 Solidity 容易卡在这一块。为什么?因为区块链上的数据类型,跟普通编程语言比,多了不少“心眼”。比如地址类型,你写个普通程序哪来什么 address?但智能合约里,它就是命根子。

这一节,咱们先啃掉四个最基础、也最常用的:布尔型、整型、地址类型、字节数组。我保证,讲完这些,你写个简单的存证合约、转账合约,手到擒来。

3.1 布尔型(bool)—— 最简单的开关

布尔型,说白了就是“是”或“否”。在 Solidity 里,它只有两个值:truefalse

// 声明一个布尔变量
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)

布尔型的操作符也很简单:!(非)、&&(与)、||(或)、==(等于)、!=(不等于)。

💡 个人习惯
我写合约时,布尔变量命名喜欢用 isXxxhasXxx 开头。比如 isPausedhasVoted。这样读代码时一眼就能看出它的语义。

3.2 整型(int / uint)—— 小心别溢出

整型是 Solidity 里最“坑”的数据类型之一。为什么?因为区块链上的整数是固定长度的。

Solidity 提供两种整型:

  • uint:无符号整数(只能是非负数)。从 uint8uint256,步长 8。
  • int:有符号整数(可正可负)。从 int8int256,步长 8。

如果不指定长度,默认就是 uint256int256。我个人建议:能用 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);

这里有个重要的区别:addressaddress 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 固定大小字节数组

bytes1bytes32,每个占 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 的类型转换比较严格。我总结了几条规则:

  1. 隐式转换:只能从“小”转“大”。比如 uint8uint256 没问题。
  2. 显式转换:用 type(value) 语法。比如 uint8(256) 会截断成 0。
  3. 地址转换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 的时间。