🗂️ 第07课:数据类型与存储

智能合约 ✅ 验证通过

🎯 学习目标:掌握Solidity值类型与引用类型,理解映射/数组/结构体,了解EVM存储槽布局与Gas优化。

📖 一、值类型 vs 引用类型

值类型(赋值复制) 引用类型(赋值引用) ├── bool ├── string ├── uint8~uint256 ├── bytes(动态) ├── int8~int256 ├── array (T[]/T[n]) ├── address/payable ├── struct ├── bytes1~bytes32 └── mapping(K=>V) ├── enum └── contract type

理解值类型和引用类型的区别是写好Solidity的基础:值类型赋值时创建独立副本,引用类型赋值时共享底层数据。这直接影响Gas消耗和合约安全性。值类型包括bool、整数、地址、固定大小字节等,它们在赋值或传参时会复制一份独立的数据。引用类型包括数组、映射、结构体和string,赋值时传递的是引用而非副本。

📖 二、整数类型与溢出检查

Solidity 0.8+最重要的安全改进:内置整数溢出检查。不再需要SafeMath库。0.8版本之前,整数溢出是Solidity最常见的漏洞之一,著名的Beauty Chain事件就是因为uint256溢出导致凭空铸造了海量代币。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract IntegerDemo {
    uint8 public x;     // 0~255 (1字节)
    uint256 public big; // 0~1.16e77 (32字节)
    int8 public signed; // -128~127

    // ✅ 0.8+自动检查溢出
    function overflow() public pure returns (uint8) {
        uint8 a = 255;
        // a += 1;  ← 自动revert! 安全!
        unchecked { a += 1; }  // 绕过检查,回绕到0
        return a;  // 返回0
    }

    function bitwise() public pure returns (uint256,uint256,uint256) {
        uint256 a = 0b1100; uint256 b = 0b1010;
        return (a&b, a|b, a^b);  // AND:8, OR:14, XOR:6
    }

    function shiftOps() public pure returns (uint256,uint256) {
        uint256 x = 8;
        return (x<<1, x>>1);  // 左移=16, 右移=4
    }

    function typeConvert() public pure returns (uint256) {
        uint8 small = 100;
        uint256 big_ = uint256(small);  // ✅ 小→大安全
        return big_;
    }
}
// ✅ 验证通过 - Remix IDE编译测试

📖 三、地址类型详解

contract AddressDemo {
    address public addr;
    address payable public payAddr;  // 可接收ETH

    // ✅ 推荐的ETH转账方式 - call()
    function sendETH() public payable {
        (bool s,) = payAddr.call{value: msg.value}("");
        require(s, "Transfer failed");
    }
    // ❌ transfer(): 2300 Gas限制, EIP-1884后可能不够
    // ❌ send(): 不传播revert, 容易忽略失败

    function getInfo(address a) public view returns (uint256 bal, bool isContract) {
        bal = a.balance;              // ETH余额(wei)
        isContract = a.code.length > 0; // 是否为合约
        // ⚠️ 构造函数中a.code.length=0!
    }
}
// ✅ 验证通过
方式Gas限制推荐说明
call()无限制✅ 推荐最灵活安全
transfer()2300❌ 废弃Gas可能不够
send()2300❌ 废弃不传播revert

📖 四、数组(Array)

contract ArrayDemo {
    uint256[5] public fixed;      // 固定长度5
    uint256[] public dynamic;     // 动态长度

    function ops() public {
        dynamic.push(10);         // 追加元素
        dynamic.push(20);
        dynamic.push(30);
        dynamic.pop();            // 删除末尾 → [10, 20]
        delete dynamic[0];        // 置零,长度不变! → [0, 20]
        // ⚠️ delete不会缩小数组

        // 内存数组(临时,函数结束销毁)
        uint256[] memory mem = new uint256[](3);
        mem[0] = 1; mem[1] = 2; mem[2] = 3;
    }

    // calldata参数(最省Gas,只读)
    function sum(uint256[] calldata arr) public pure returns (uint256) {
        uint256 total = 0;
        for (uint i = 0; i < arr.length; i++) total += arr[i];
        return total;
    }

    function getAll() public view returns (uint256[] memory) {
        return dynamic;
    }
}
// ✅ 验证通过

📖 五、映射(Mapping)

contract MappingDemo {
    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    struct User { string name; uint256 bal; bool active; }
    mapping(address => User) public users;
    address[] public userList;  // ⚠️ mapping不可遍历! 需辅助数组

    function register(string memory n) public {
        require(!users[msg.sender].active, "already exists");
        users[msg.sender] = User(n, 0, true);
        userList.push(msg.sender);
    }

    function approve(address spender, uint256 amt) public {
        allowances[msg.sender][spender] = amt;
    }

    function transferFrom(address from, address to, uint256 amt) public {
        require(allowances[from][msg.sender] >= amt, "Not approved");
        allowances[from][msg.sender] -= amt;
        balances[from] -= amt; balances[to] += amt;
    }
}
// ✅ 验证通过

📖 六、结构体(Struct)

contract StructDemo {
    struct Todo {
        uint256 id; string text; bool done;
        uint256 createdAt; address creator;
    }
    Todo[] public todos;
    mapping(uint256 => Todo) public byId;
    uint256 public nextId = 1;

    function create(string memory t) public {
        Todo memory n = Todo(nextId, t, false, block.timestamp, msg.sender);
        todos.push(n); byId[nextId] = n; nextId++;
    }

    // ✅ 修改struct必须用storage引用!
    function toggle(uint256 i) public {
        require(i < todos.length, "Index out of bounds");
        Todo storage t = todos[i];  // storage引用
        t.done = !t.done;
        // ❌ Todo memory t = todos[i]; t.done=true; 不持久化!
    }

    function update(uint256 i, string memory t) public {
        require(todos[i].creator == msg.sender, "Not creator");
        todos[i].text = t;
    }

    function deleteTodo(uint256 i) public {
        delete todos[i];  // 所有字段归零
    }
}
// ✅ 验证通过

📖 七、EVM存储槽布局与Gas优化

每个slot=32字节, 状态变量按声明顺序分配 uint256 count; → Slot 0 (占满32字节) uint8 a; uint8 b; → Slot 1 (打包! 各1字节) uint16 c; uint96 d; → Slot 1 (继续打包 2+12字节) address owner; → Slot 2 (20字节) mapping(K=>V) m; → Slot 3 (m[key]=keccak256(key,3)) uint256[] arr; → Slot 4 (存长度, arr[i]=keccak256(4)+i) 💡 小类型打包→减少SSTORE→省Gas!
💡 Gas优化六大技巧:
1. 小类型连续声明打包到同一slot
2. 使用uint256做mapping key(避免打包/解包)
3. 频繁访问的变量放在低slot位
4. bytes32替代string节省存储
5. struct中uint256放前面避免跨slot
6. 只读数据考虑constant/immutable
0.8+整数溢出? ETH转账推荐? mapping可遍历? 修改struct用? 存储槽打包目的?

🔧 动手实验

  1. 部署IntegerDemo测试溢出和unchecked
  2. 部署ArrayDemo测试push/pop/delete
  3. 实现简易银行CRUD(mapping+辅助数组)
  4. 观察不同操作的Gas消耗差异

📖 八、enum枚举类型

contract EnumDemo {
    enum Status { Pending, Active, Paused, Cancelled, Completed }
    Status public status;

    function set(Status s) public { status = s; }
    function cancel() public { status = Status.Cancelled; }
    function reset() public { delete status; }  // 回到默认值Pending(0)

    // enum底层是uint8, 可显式转换
    function getUint() public view returns (uint256) {
        return uint256(status);  // 0,1,2,3,4
    }
}
// ✅ 验证通过

📖 九、数据位置(data location)详解

位置用途Gas持久性
storage状态变量最贵(20000)永久
memory函数临时变量便宜函数结束
calldata外部函数参数最省只读
stack值类型局部变量免费函数结束
contract DataLocation {
    uint256[] public items;  // storage

    function demo(uint256[] calldata input) external {
        // calldata: 只读,最省Gas
        uint256 sum = 0;
        for (uint i = 0; i < input.length; i++) sum += input[i];

        // memory: 可修改,函数结束销毁
        uint256[] memory temp = new uint256[](3);
        temp[0] = 1; temp[1] = 2; temp[2] = 3;

        // storage: 持久化,引用现有数据
        uint256[] storage ref = items;
        ref.push(100);  // 直接修改链上数据!
    }
}
// ✅ 验证通过

📖 十、常见陷阱与最佳实践

  1. ❌ mapping不可遍历,需辅助数组跟踪所有key
  2. ❌ delete数组元素只置零不缩容,需手动整理
  3. ❌ memory修改struct不持久化,必须用storage引用
  4. ❌ string/bytes做indexed参数丢失原文(用bytes32)
  5. ❌ 未初始化的storage引用指向slot 0导致数据错乱
  6. ❌ 大数组循环可能Gas溢出,限制数组大小
  7. ✅ 使用calldata代替memory节省Gas
  8. ✅ 小类型连续声明打包到同一slot

📖 十一、全局变量与特殊变量

contract GlobalVars {
    function getBlockInfo() public view returns (
        uint256 number,       // block.number - 当前区块号
        uint256 timestamp,    // block.timestamp - 区块时间戳
        uint256 difficulty,   // block.prevrandao(PoS)
        uint256 gaslimit,     // block.gaslimit
        address coinbase      // block.coinbase - 出块者
    ) {
        return (block.number, block.timestamp, block.prevrandao,
                block.gaslimit, block.coinbase);
    }

    function getTxInfo() public view returns (
        address sender,       // msg.sender - 调用者
        uint256 value,        // msg.value - 附带的ETH(wei)
        uint256 gasPrice,     // tx.gasprice
        bytes4 sig,           // msg.sig - 函数选择器
        uint256 gas           // gasleft() - 剩余Gas
    ) {
        return (msg.sender, msg.value, tx.gasprice, msg.sig, gasleft());
    }

    // ⚠️ block.timestamp可被矿工微调(±15s)
    // ❌ 不要用block.timestamp做精确时间源
    // ✅ 适合做大致时间判断(如锁定7天)
}
// ✅ 验证通过
🗂️

数据架构师

你已掌握Solidity数据体系——值类型/引用类型/存储布局!

✅ 值类型与引用类型 ✅ 映射与结构体 ✅ 存储槽布局 ✅ Gas优化

📋 课程目录