Solidity智能合约基础

基础学习使用

remix:ide

Remix - Ethereum IDE

evm:ethreum  virtual machine  evm字节码

强类型脚本语言   compile  =>evm  bytescode =>evm

hello的样例

声明的关键字:contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
//pragma solidity ^0.8.0;//大于8.0版本
//pragma solidity >=0.8.0 <0.9.0;//大于等于8.0版本,小于9.0版本

contract helloDto {
    string public  hello="hello 3.0";
}

一、EVM 的数据结构

  • 基于栈的架构:EVM 使用栈来存储数据,最大支持 1024 个栈元素,每个元素是 256 位的字(word),所有计算都在栈上完成。

  • 三类存储结构:

    1. 程序代码存储区(ROM)(栈):1024个solt,每个solt是32个字节>=256bit,不可变,存储智能合约的字节码。
    2. 内存(Memory):可变,临时存储执行期间的数据,随调用结束而清除。
    3. 存储(Storage):持久化存储,每个智能合约都有一个唯一的存储区,存储合约状态。

二、 EVM 中的两种值类型

  • 基本类型(Value Types)

    • 定义:直接存储值的数据类型,变量之间赋值是复制值本身。
    • 常见类型uintintbooladdressbytes1 到 bytes32
    • 存储特性
      • 分配在固定的 storage slot
      • 赋值是值复制(copy by value)
  • 引用类型(Reference Types)

    • 定义:存储的是对数据的“引用”或“指针”,赋值传的是地址。

    • 常见类型array(数组),mapping(映射),struct(结构体)

    • 存储特性

      • 变量本身保存的是对实际数据的引用

      • 赋值是引用复制(copy by reference)

      • 需要 keccak256(slot) 来定位实际数据地址(尤其是动态结构)

evm特性

交易流程图

1. 用户发起交易
   |
   ↓
2. 创建新的 EVM 实例
   |
   ↓
3. 加载合约字节码
   |
   ↓
4. 分配 Stack 空间(1024 slots)
   |
   ↓
5. 执行合约代码
   |
   ↓
6. 更新区块链状态(如果需要)
   |
   ↓
7. 销毁 EVM 实例

详细说明

  1. 用户发起交易

    • 用户签名交易
    • 设定 gas limit 和 gas price
    • 指定目标合约地址和调用数据
  2. 创建新的 EVM 实例

    • 为每笔交易创建独立的 EVM 环境
    • 初始化执行上下文
    • 准备内存和存储空间
  3. 加载合约字节码

    • 从区块链状态中读取合约字节码
    • 将字节码加载到 EVM 中
    • 准备执行环境
  4. 分配 Stack 空间

    • 分配 1024 个 slots
    • 每个 slot 256 位
    • 用于存储临时计算结果
  5. 执行合约代码

    • 逐条执行操作码
    • 进行状态检查和 gas 计算
    • 处理函数调用和返回值
  6. 更新区块链状态

    • 写入存储变更
    • 更新账户余额
    • 触发事件日志
  7. 销毁 EVM 实例

    • 清理内存
    • 释放资源
    • 返回执行结果

注意事项

  • 整个过程是原子性的:要么全部成功,要么全部失败
  • Gas 限制贯穿整个执行过程
  • 状态变更只在交易成功执行后才会提交

EVM 存储结构

1. Stack (栈)
+----------------+
| 基本类型的值    | <- 函数内的局部变量
| 引用的地址      | <- 指向其他存储位置
+----------------+


2. Memory (内存)
+----------------+
| 临时数据       | <- 函数执行期间的临时数据
| 函数参数       | <- memory 类型的参数
| 返回数据       | <- 函数返回值
+----------------+


3. Storage (存储)
+----------------+
| 状态变量       | <- 合约的永久存储数据
| 映射数据       | <- mapping 数据
| 数组数据       | <- storage 数组
+----------------+

EVM Stack (固定大小的栈空间)

+------------------+
|     空闲空间     | <- 1024 个槽位(slots)
|        ⬇        |    每个槽位 32 字节(256 位)
+------------------+
|    当前使用空间   | <- 随函数执行压入/弹出
+------------------+

Memory: 存在于 EVM 执行环境中 临时性的,交易执行完就清除 线性寻址(0x00, 0x20, 0x40...)

Storage: 存在于区块链状态中 永久性的,写入区块 使用 slot 和 keccak256 哈希定位

基本类型

在 Solidity 中,直接存储在栈(Stack)中的基本类型包括:

1. 整型(Integer)

contract StackTypes {
    function integerTypes() public pure {
        // 所有整型都存储在栈中
        uint256 a = 1;
        uint8 b = 2;
        int256 c = -1;
        int8 d = -2;
    }
}
2. 布尔型(Boolean)

function booleanTypes() public pure {
    bool isTrue = true;
    bool isFalse = false;
}
3. 地址(Address)

function addressTypes() public pure {
    address addr = 0x123...;
    address payable payableAddr = payable(0x123...);
}
  1. 固定大小字节数组(Fixed-size Bytes)
function bytesTypes() public pure {
    bytes1 b1 = 0x12;
    bytes32 b32 = 0x123...;
}
  1. 枚举(Enum)
enum Status { Active, Inactive }

function enumTypes() public pure {
    Status status = Status.Active;  // 实际存储为 uint8
}

重要说明:

  1. 这些类型在函数(相关文档:数据位置修饰符)调用时:
  • 作为参数传递时是值传递
  • 不需要指定 memory 等位置修饰符
  // ✅ 正确:不需要 memory
  function example(uint256 num, bool flag, address addr)
  public pure {
      // ...
  }

  // ❌ 错误:不能为基本类型指定 memory
  function wrong(uint256 memory num) public pure {
      // ...
  }
  1. 大小限制:
function stackLimits() public pure {
    // EVM 栈深度限制为 1024
    // 每个值占用一个栈槽(32字节)
}
  1. 与引用类型对比:
contract TypeComparison {
    // 基本类型:直接存储在栈中
    uint256 public stackVar = 123;
    
    // 引用类型:存储引用在栈中,数据在其他位置
    string public stringVar = "hello";  // 数据在存储中
    uint256[] public arrayVar;          // 数据在存储中
}
  1. 在函数(文档地址:数据位置修饰符)中的使用:
contract StackUsage {
    function calculate() public pure returns (uint256) {
        // 这些变量都在栈中
        uint256 a = 1;
        uint256 b = 2;
        uint256 c = a + b;
        
        return c;
    } // 函数结束时栈变量自动清除
}

注意事项:

  1. 栈变量的生命周期仅限于函数执行期间
  2. 栈变量不需要手动管理内存
  3. 栈操作的 gas 成本较低
  4. 需要注意栈深度限制(1024层)

这些类型的特点:

  1. 大小固定
  2. 值类型(非引用)
  3. 操作简单直接
  4. gas 成本低

主要的引用类型:

特点: 在栈中:只存储引用(地址/指针) 实际数据:存储在 Memory 或 Storage 中

  1. 数组(Array)
contract ArrayExample {
    // Storage 数组(状态变量)
    uint[] public storageArray;  // 存储在链上
    
    function example() public {
        // Memory 数组(局部变量)
        uint[] memory memoryArray = new uint[](3);
        
        // 栈中只存储数组的引用(指针)
        // 实际数据在 Memory 或 Storage 中
    }
}
  1. 字符串(String)
contract StringExample {
    // Storage 字符串
    string public storageStr = "hello";  // 存储在链上
    
    function example() public pure {
        // Memory 字符串
        string memory memoryStr = "world";
        
        // 栈中存储字符串的引用
        // 实际字符串数据在 Memory 或 Storage
    }
}
  1. 结构体(Struct)
contract StructExample {
    struct Person {
        string name;
        uint age;
    }
    
    // Storage 结构体
    Person public storagePerson;
    
    function example() public {
        // Memory 结构体
        Person memory memoryPerson = Person("Alice", 20);
        
        // 栈中存储结构体的引用
        // 实际数据在 Memory 或 Storage
    }
}
  1. 映射(Mapping)
contract MappingExample {
    // 只能声明为 Storage
    mapping(address => uint) public balances;
    
    function example() public {
        // ❌ 不能创建 Memory 映射
        // mapping(address => uint) memory memoryMap;  // 错误
        
        // 只能在 Storage 中使用
        balances[msg.sender] = 100;
    }
}
  1. 不固定长度的字节数组(bytes)
// 引用类型示例
bytes public dynamicBytes;     // 动态字节数组,存储在存储区(storage)
bytes32 public fixedBytes;     // 固定长度字节数组,是值类型

function example() public {
    // 动态分配内存
    dynamicBytes = new bytes(2);
    dynamicBytes[0] = 0x12;
    dynamicBytes[1] = 0x34;
    
    // 可以改变长度
    dynamicBytes.push(0x56);
}

存储位置总结:

  1. Storage(链上存储)
  • 状态变量
  • 永久存储
  • Gas 成本高 string public storageStr; // 状态变量自动存储在 Storage
  1. Memory(临时内存)
  • 函数参数
  • 函数(相关文档:数据位置修饰符)内的临时变量
  • 函数返回值 function example(string memory param) public { string memory temp = "temp"; }
  1. 栈(Stack)
  • 只存储引用(指针)
  • 指向 Memory 或 Storage 的实际数据 // 栈中存储的是引用,指向实际数据 uint[] memory arr = new uint;

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
//pragma solidity ^0.8.0;//大于8.0版本
//pragma solidity >=0.8.0 <0.9.0;//大于等于8.0版本,小于9.0版本

contract helloDto {
    //string public  hello="hello 3.0";
    int public a = 1 * 2 ** 255-1;
    uint public b =1 *2**256 - 1; // u =>unsinged 没有符号 + -
    bool public c =false;

    address public add =钱包地址;   //16字节

    bytes32 public d=hex"1000";

    enum  Status{
        Active,
        Bob
    }
    int[] public arr;

    struct Person{
        int8 Age;
        bool Sex;
        string Name;
    }
    Person public lihua =Person(18,false,"lihua");

    Person public Tom =Person({Age:18,Sex :false,Name:"Tom"});

}

 Solidity 函数调用

函数的基本定义
  • 函数定义语法
    • 在 Solidity 中,函数的定义形式如下:
function 函数名(< 参数类型 > < 参数名 >) < 可见性 > < 状态可变性 > [returns(< 返回类型 >)] {
// 函数体
}
    string private hello ="hello";

    function say (string memory name) public  view  returns (string memory) {
        
        return string.concat(hello,name);
    }

    function cancat (string memory base,string memory name) public pure returns (string memory){
        return string.concat(base,name);
    }
    function setHello(string memory str) public {
        hello=str;
    }

  • 函数可以包含输入参数、输出参数、可见性修饰符、状态可变性修饰符和返回类型。

  • 自由函数

    • 函数不仅可以在合约内部定义,还可以作为自由函数在合约外部定义。
    • 自由函数的使用可以帮助分离业务逻辑,使代码更具模块化。

函数参数的使用
  • 参数声明
    • 函数参数与变量声明类似,输入参数用于接收调用时传入的值,输出参数用于返回结果。
    • 未使用的参数可以省略其名称。
    • 示例代码:
pragma solidity >0.5.0;

contract Simple {
    function taker(uint _a, uint _b) public pure {
        // 使用 _a 和 _b 进行运算
    }
}
  • 返回值
    • Solidity 函数可以返回多个值,这通过元组(tuple)来实现。
    • 返回值可以通过两种方式指定:
    • 使用返回变量名
function arithmetic(uint _a, uint _b) public pure returns (uint o_sum, uint o_product) {
o_sum = _a + _b;
o_product = _a * _b;
}
  • 直接在return语句中提供返回值
function arithmetic(uint _a, uint _b) public pure returns (uint o_sum, uint o_product) {
    return (_a + _b, _a * _b);
}
  • 元组与多值返回
    • Solidity 支持通过元组返回多个值,并且可以同时将这些值赋给多个变量。
    • 示例代码:
pragma solidity >0.5.0;

contract C {
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}

function g() public {
    (uint x, bool b, uint y) = f();  // 多值赋值
}
}

函数可见性修饰符
  • Solidity中的函数可见性修饰符有四种,决定了函数在何处可以被访问:

    1. private (私有)
      • 只能在定义该函数的合约内部调用。
    2. internal (内部)
      • 可在定义该函数的合约内部调用,也可从继承该合约的子合约中调用。
    3. external (外部)
      • 只能从合约外部调用。如果需要从合约内部调用,必须使用this关键字。
    4. public (公开)
      • 可以从任何地方调用,包括合约内部、继承合约和合约外部。
  • 示例代码

contract VisibilityExample {
    function privateFunction() private pure returns (string memory) {
        return "Private";
    }
    
    function internalFunction() internal pure returns (string memory) {
        return "Internal";
    }
    
    function externalFunction() external pure returns (string memory) {
        return "External";
    }
    
    function publicFunction() public pure returns (string memory) {
        return "Public";
    }
}

状态可变性修饰符
  • 状态可变性修饰符
    • Solidity 中有三种状态可变性修饰符,用于描述函数是否会修改区块链上的状态:
    • view
      • 声明函数只能读取状态变量,不能修改状态。
      • 示例:
function getData() external view returns(uint256) {
return data;
}
  • pure
    • 声明函数既不能读取也不能修改状态变量,通常用于执行纯计算。
    • 示例:
function add(uint _a, uint _b) public pure returns (uint) {
    return _a + _b;
}
  • payable
    • 声明函数可以接受以太币,如果没有该修饰符,函数将拒绝任何发送到它的以太币。
    • 示例:
function deposit() external payable {
// 接收以太币
}

payable 是 Solidity 中的一个函数修饰符,用于标识一个函数可以接收以太币(ETH)。当一个函数被标记为 payable 时,它允许在调用该函数时向合约发送以太币。这是合约接收以太币的唯一方式。

  • 接收以太币:只有被标记为 payable 的函数才能接收以太币。如果尝试向一个没有 payable 修饰符的函数发送以太币,交易将会失败。
  • 合约余额:通过 payable 函数接收到的以太币会增加合约的余额。
  • 使用场景:通常用于实现合约的支付功能,比如购买、捐赠等。
contract PayableDemo {
    function deposit() public payable {
        // 函数可以接收 ETH
    }
}
  • 完整示例
contract SimpleStorage {
    uint256 private data;

    function setData(uint256 _data) external {
        data = _data;  // 修改状态变量
    }

    function getData() external view returns (uint256) {
        return data;  // 读取状态变量
    }

    function add(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;  // 纯计算函数
    }

    function deposit() external payable {
        // 接收以太币
    }
}

什么是 StateDB?

StateDB状态数据库是区块链系统中用于存储每个账户(或合约)的最新状态的组件。它记录了包括账户余额、合约存储变量、nonce 等所有与当前区块状态相关的数据。


 它解决了什么问题?

在区块链中,每个新区块的生成都伴随着系统状态的更新(比如账户余额变化、合约变量修改等),因此必须有一个机制来追踪和存储这些变化。这就是 StateDB 的作用:

  • 保存所有账户和合约的当前状态
  • 提供查询更新状态的能力。
  • 支持区块的回滚状态快照,便于处理链的重组(reorg)或回退。

StateDB 通常包含哪些内容?

以以太坊为例,StateDB 中包含:

项目说明
地址(Address)每个用户或合约的唯一标识
余额(Balance)账户中持有的 ETH 数量
Nonce账户已发送交易的数量,用于防止重放攻击
存储(Storage)合约中的变量(键值对),通过 Merkle Patricia Tree 组织
代码(Code)智能合约的字节码

这些数据通过一种叫做 Merkle Patricia Trie 的数据结构存储,从而实现高效的查找、验证与同步。


数据结构与存储形式

在如以太坊这样的系统中,StateDB 不是直接存在于普通的数据库中,而是被组织为:

  • 账户状态树(State Trie):每个账户为一个节点
  • 每个合约的存储又构成一个独立的 trie(合约存储 Trie)

整个状态树的根哈希(stateRoot)会被写入区块头中,确保状态不可篡改且可验证。


举个例子

假如 Alice 向 Bob 转账 1 ETH,StateDB 会:

  1. 查找 Alice 和 Bob 的账户状态
  2. 减少 Alice 的余额,增加 Bob 的余额
  3. 更新这些状态在 Merkle Trie 中的位置
  4. 生成新的根哈希,写入区块头

    整型

    整数类型用 int/uint 表示有符号和无符号的整数。关键字 int/uint 的末尾接上一个数字表示数据类型所占用空间的大小,这个数字是以 8 的倍数,最高为 256,因此,表示不同空间大小的整型有:uint8、uint16、uint32 ... uint256,int 同理,无数字时 uint 和 int 对应 uint256 和 int56。

    因此整数的取值范围跟不同空间大小有关, 比如 uint32 类型的取值范围是 0 到 2^32-1(2 的 32 次方减 1)。

    如果整数的某些操作,其结果不在取值范围内,则会被溢出截断。 数据被截断可能引发严重后果,稍后举例。

    整型支持以下几种运算符:

    比较运算符: <=(小于等于)、<(小于) 、==(等于)、!=(不等于)、>=(大于等于)、>(大于)
    
    位操作符: &(和)、|(或)、^(异或)、~(位取反)
    
    算术操作符:+(加号)、-(减)、-(负号)、* (乘法)、/ (除法), %(取余数), **(幂)
    
    移位: <<(左移位)、 >>(右移位)
    

    这里略作说明:

    ① 整数除法总是截断的,但如果运算符是字面量(字面量稍后讲),则不会截断。

    ② 整数除 0 会抛出异常。

    ③ 移位运算结果的正负取决于操作符左边的数。x << y 和 x * (2**y) 是相等的,x >> y 和 x / (2*y) 是相等的。

    ④ 不能进行负移位,即操作符右边的数不可以为负数,否则会在运行时抛出异常。

    这里提供一段代码来让大家熟练一不同操作符的使用,运行之前,先自己预测一下结果,看是否和运行结果不一样。

    pragma solidity >0.5.0; 
    contract testInt { 
        int8 a = -1; 
        int16 b = 2; 
        uint32 c = 10; 
        uint8 d = 16; 
        function add(uint x, uint y) public pure returns (uint z) {
            z = x + y; 
        }
        function divide(uint x, uint y ) public pure returns (uint z){
            z = x / y; 
        } 
        function leftshift(int x, uint y) public pure returns (int z){
            z = x << y; 
        } 
        function rightshift(int x, uint y) public pure returns (int z){
            z = x >> y; 
        } 
        function testPlusPlus() public pure returns (uint ) { 
            uint x = 1; uint y = ++x; // c = ++a; 
            return y; 
        } 
    }

    整型溢出问题 在使用整型时,要特别注意整型的大小及所能容纳的最大值和最小值,如 uint8 的最大值为 0xff(即:255),最小值是 0,可以通过 Type(T).min 和 Type(T).max 获得整型的最小值与最大值。

    下面这段合约代码用来演示整型溢出的情况,大家可以预测 3 个函数分别的结果是什么?然后运行看看。

    pragma solidity ^0.5.0; 
    contract testOverflow { 
        function add1() public pure returns (uint8) { 
            uint8 x = 128; 
            uint8 y = x * 2; 
            return y; 
        } 
        function add2() public pure returns (uint8) { 
            uint8 i = 240; 
            uint8 j = 16; 
            uint8 k = i + j; 
            return k; 
        } 
        function sub1() public pure returns (uint8) { 
            uint8 m = 1; 
            uint8 n = m - 2; 
            return n; 
        } 
    }

    揭晓一下上述代码的运行结果:add1()的结果是 0,而不是 256,add2() 的结果同样是 0,sub1 是 255,而不是-1。

    溢出就像时钟一样,当秒针走到 59 之后,下一秒又从 0 开始。

    业界名气颇大的 BEC,就曾经因发生溢出问题被交易所暂停交易,损失惨重。

    防止整型溢出问题,一个方法是对加法运算的结果进行判断,防止出现异常值,例如:

    function add(uint256 a, uint256 b) internal pure returns (uint256){ uint256 c = a + b; 
        require(c >= a); // 做溢出判断,加法的结果肯定比任何一个元素大。
        return c; 
    }

数组的基本概念

  • 概述:

    • 在 Solidity 中,数组是一种用于存储相同类型元素的集合;数组类型可以通过在数据类型后添加 [] 来定义。
    • Solidity 支持两种数组类型:静态数组(Fixed-size Arrays)和动态数组(Dynamic Arrays)。
  • 静态数组:

    • 长度固定,数组的大小在定义时确定,之后无法改变。
    • 语法示例: uint[10] tens; // 一个长度为 10 的 uint 类型静态数组 string[4] adaArr = ["This", "is", "an", "array"]; // 初始化的静态数组
  • 动态数组:

    • 长度可变,可以根据需要动态调整。
    • 语法示例: uint[] many; // 一个动态长度的uint类型数组 pop push uint[] public u = [1, 2, 3]; // 动态数组的初始化
  • 通过new关键字声明数组:

    • 动态数组可以使用 new 关键字在内存中创建,大小基于运行时确定。
    • 语法示例: new uint; // 创建一个长度为 7 的动态内存数组 new string; // 创建一个长度为 4 的动态字符串数组
  • 数组元素访问:

    • 使用下标访问数组元素,序号从0开始。
    • 语法示例: tens[0] = 1; // 对第一个元素赋值 uint element = u[2]; // 访问第三个元素

数组的内存(Memory)与存储(Storage)

  • 存储(Storage)数组:
    • 存储在区块链上,生命周期与合约生命周期相同。
    • 语法示例: uint[] public storageArray; // selfdestruct storageArray.push(10); // 修改存储数组
  • 内存(Memory)数组:
    • 临时存在于函数调用中,生命周期与函数相同,函数执行完毕后销毁。
    • 语法示例: function manipulateArray() public pure returns (uint[] memory) { uint[] memory tempArray = new uint; // 内存中创建长度为3的动态数组 tempArray[0] = 10; //sload return tempArray; }

特殊数组类型: bytes 和 string

  • bytes 类型:

    • bytes 是一个动态分配大小的字节数组,类似于 byte[],但 gas 费用更低。
    • 语法示例: bytes bs = "abc\x22\x22"; // 通过十六进制字符串初始化 bytes public _data = new bytes(10); // 创建一个长度为 10 的字节数组
  • string 类型:

    • string 用于存储任意长度的字符串(UTF-8编码),对字符串进行操作时用到。
    • 语法示例: string str0; string str1 = "TinyXiong\u718A"; // 使用Unicode编码值
  • 注意:

    • string 不支持使用下标索引进行访问。bytes 可以通过下标索引进行读访问。
    • 使用长度受限的字节数组时,建议使用 bytes1 到 bytes32 类型,以减少 gas 费用。

数组成员与常用操作

  • 数组成员属性和函数:

    • length 属性:返回数组当前长度(只读),动态数组的长度可以动态改变。
    • push():用于动态数组,在数组末尾添加新元素并返回元素引用。
    • pop():从数组末尾删除元素,并减少数组长度。
  • 代码示例: contract ArrayOperations { uint[] public dynamicArray; function addElement(uint _element) public { dynamicArray.push(_element); // 向数组添加元素 // ArrayList.add(ele) } function removeLastElement() public { dynamicArray.pop(); // 删除数组最后一个元素 } function getLength() public view returns (uint) { return dynamicArray.length; // 获取数组长度 } }


多维数组与数组切片

  • 多维数组:

    • 支持多维数组,可以使用多个方括号表示,例如 uint[][5] 表示长度为 5 的变长数组的数组。
    • 语法示例: uint[][5] multiArray; // 一个元素为变长数组的静态数组 uint element = multiArray[2][1]; // 访问第三个动态数组的第二个元素
  • 数组切片:

    • 数组切片是数组的一段连续部分,通过 [start:end] 的方式定义。
    • 语法示例: bytes memory slice = bytesArray[start:end]; // 创建数组切片
  • 应用示例: function sliceArray(bytes calldata _payload) external { bytes4 sig = abi.decode(_payload[:4], (bytes4)); // 解码函数选择器 address owner = abi.decode(_payload[4:], (address)); // 解码地址 }

循环的基本类型

Solidity 支持三种基本的循环结构:

  1. for 循环
  2. while 循环
  3. do-while 循环

for 循环

  • 基本语法: contract ForLoopExample { function basicFor() public pure returns(uint) { uint sum = 0;

      for(uint i = 0; i < 10; i++) {
          sum += i;
      }
      
      return sum;
    

    } }

  • for 循环的变体: contract ForLoopVariants { // 无初始化语句 function forWithoutInit() public pure { uint i = 0; for(; i < 10; i++) { // 循环体 } }

    // 无条件语句 function forWithoutCondition() public pure { for(uint i = 0;; i++) { if(i >= 10) break; // 循环体 } }

    // 无递增语句 function forWithoutIncrement() public pure { for(uint i = 0; i < 10;) { // 循环体 i++; } } }


while 循环

  • 基本语法: contract WhileLoopExample { function basicWhile() public pure returns(uint) { uint sum = 0; uint i = 0;

      while(i < 10) {
          sum += i;
          i++;
      }
      
      return sum;
    

    } }


do-while 循环

  • 基本语法: contract DoWhileExample { function basicDoWhile() public pure returns(uint) { uint sum = 0; uint i = 0;

      do {
          sum += i;
          i++;
      } while(i < 10);
      
      return sum;
    

    } }


循环控制语句

  1. break 语句: contract BreakExample { function findFirstEven(uint[] memory numbers) public pure returns(uint) { for(uint i = 0; i < numbers.length; i++) { if(numbers[i] % 2 == 0) { return numbers[i]; } } return 0; } }

  2. continue 语句: contract ContinueExample { function sumOddNumbers(uint[] memory numbers) public pure returns(uint) { uint sum = 0;

     for(uint i = 0; i < numbers.length; i++) {
         if(numbers[i] % 2 == 0) {
             continue;
         }
         sum += numbers[i];
     }
     
     return sum;
    

    } }


Gas 优化考虑

  1. 避免无限循环: contract GasOptimization { // 不推荐:可能导致 gas 耗尽 function riskyLoop() public pure { uint i = 0; while(true) { i++; if(i >= 10) break; } }

    // 推荐:明确的循环边界 function safeLoop() public pure { for(uint i = 0; i < 10; i++) { // 循环体 } } }

  2. 优化数组循环: contract ArrayLoopOptimization { // 不推荐:每次循环都要读取数组长度 function inefficientLoop(uint[] memory arr) public pure { for(uint i = 0; i < arr.length; i++) { // 循环体 } }

    // 推荐:缓存数组长度 function efficientLoop(uint[] memory arr) public pure { uint length = arr.length; for(uint i = 0; i < length; i++) { // 循环体 } } }

  1. 映射(Mapping) 1.1 什么是映射? 映射(Mapping)是 Solidity 中的一种特殊数据结构,类似于哈希表(或字典),用于存储键值对。 语法:
mapping(KeyType => ValueType) visibility variableName;
  • KeyType:键的类型,支持 uint、address、bytes32 等,不支持 struct 或 mapping。
  • ValueType:值的类型,可以是任何 Solidity 变量类型,包括 struct 和 mapping。
  • visibility:存储变量的可见性,如 public、private 等。 1.2 示例:账户余额存储
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BalanceTracker {
    mapping(address => uint256) public balances;
    function setBalance(uint256 amount) public {
        balances[msg.sender] = amount;
    }
    function getBalance(address user) public view returns (uint256) {
        return balances[user];
    }
}
  • balances 映射 address 到 uint256,用于存储用户的余额。
  • setBalance 允许用户设置自己的余额。
  • getBalance 获取指定用户的余额。

多级映射(嵌套映射)

contract MultiMapping {
    mapping(address => mapping(string => uint256)) public userBalances;
    function setUserBalance(string memory currency, uint256 amount) public {
        userBalances[msg.sender][currency] = amount;
    }
    function getUserBalance(address user, string memory currency) public view returns (uint256) {
        return userBalances[user][currency];
    }
}
  • 这里 userBalances 是一个 嵌套映射,存储用户对不同币种的余额。

映射的特点

  • 默认值: 未初始化的映射键会返回 ValueType 的默认值(例如 uint256 默认 0)。
  • 不可遍历: Solidity 不支持遍历 mapping,只能通过 key 访问特定 value。
  • 可修改但不可删除: 可以修改 mapping 中的值,但不能删除整个 mapping。
  1. 结构体(Struct) 2.1 什么是结构体? 结构体(Struct)是一种自定义数据类型,用于存储多个不同类型的数据。 语法:
struct StructName {
    DataType1 variable1;
    DataType2 variable2;
    ...
}

 示例:用户信息存储

contract UserManager {
    struct User {
        string name;
        uint256 age;
        address wallet;
    }
    mapping(address => User) public users;
    function setUser(string memory name, uint256 age) public {
        users[msg.sender] = User(name, age, msg.sender);
    }
    function getUser(address userAddress) public view returns (string memory, uint256, address) {
        User memory user = users[userAddress];
        return (user.name, user.age, user.wallet);
    }
}
  • User 结构体包含 name、age、wallet 三个字段。
  • users 是一个 mapping,将 address 映射到 User 结构体。
  • setUser 允许用户存储他们的信息。
  • getUser 允许查询用户信息。

结构体数组 如果要存储多个 User 结构体,可以使用数组:

contract UserList {
    struct User {
        string name;
        uint256 age;
    }
    User[] public users;
    function addUser(string memory name, uint256 age) public {
        users.push(User(name, age));
    }
}
  • users 数组存储 User 结构体。
  • addUser 添加新用户。

结构体的应用场景

  • 组织和存储复杂数据
  • 结合 mapping 创建去中心化存储
  • 用于 NFT、DAO、投票等应用
  • 结合映射和结构体 映射和结构体经常结合使用来构建复杂的数据存储。 示例:去中心化用户管理
contract UserRegistry {
    struct User {
        string name;
        uint256 age;
        bool isRegistered;
    }
    mapping(address => User) public users;
    function registerUser(string memory name, uint256 age) public {
        require(!users[msg.sender].isRegistered, "User already registered");
        users[msg.sender] = User(name, age, true);
    }
    function getUser(address user) public view returns (string memory, uint256, bool) {
        require(users[user].isRegistered, "User not registered");
        User memory u = users[user];
        return (u.name, u.age, u.isRegistered);
    }
}
  • 这里 User 结构体增加了 isRegistered 标志位。
  • mapping(address => User) 记录已注册的用户。
  • registerUser 确保用户只能注册一次。
  • getUser 只允许查询已注册用户的信息总结

 映射和结构体是 Solidity 合约开发中最重要的数据结构之一,合理结合两者可以构建高效的数据存储模型。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值