基础学习使用
remix: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),所有计算都在栈上完成。
-
三类存储结构:
- 程序代码存储区(ROM)(栈):1024个solt,每个solt是32个字节>=256bit,不可变,存储智能合约的字节码。
- 内存(Memory):可变,临时存储执行期间的数据,随调用结束而清除。
- 存储(Storage):持久化存储,每个智能合约都有一个唯一的存储区,存储合约状态。
二、 EVM 中的两种值类型
-
基本类型(Value Types)
- 定义:直接存储值的数据类型,变量之间赋值是复制值本身。
- 常见类型:
uint
,int
,bool
,address
,bytes1
到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 实例
详细说明
-
用户发起交易
- 用户签名交易
- 设定 gas limit 和 gas price
- 指定目标合约地址和调用数据
-
创建新的 EVM 实例
- 为每笔交易创建独立的 EVM 环境
- 初始化执行上下文
- 准备内存和存储空间
-
加载合约字节码
- 从区块链状态中读取合约字节码
- 将字节码加载到 EVM 中
- 准备执行环境
-
分配 Stack 空间
- 分配 1024 个 slots
- 每个 slot 256 位
- 用于存储临时计算结果
-
执行合约代码
- 逐条执行操作码
- 进行状态检查和 gas 计算
- 处理函数调用和返回值
-
更新区块链状态
- 写入存储变更
- 更新账户余额
- 触发事件日志
-
销毁 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...); }
- 固定大小字节数组(Fixed-size Bytes)
function bytesTypes() public pure { bytes1 b1 = 0x12; bytes32 b32 = 0x123...; }
- 枚举(Enum)
enum Status { Active, Inactive } function enumTypes() public pure { Status status = Status.Active; // 实际存储为 uint8 }
重要说明:
- 这些类型在函数(相关文档:数据位置修饰符)调用时:
- 作为参数传递时是值传递
- 不需要指定 memory 等位置修饰符
// ✅ 正确:不需要 memory function example(uint256 num, bool flag, address addr) public pure { // ... } // ❌ 错误:不能为基本类型指定 memory function wrong(uint256 memory num) public pure { // ... }
- 大小限制:
function stackLimits() public pure { // EVM 栈深度限制为 1024 // 每个值占用一个栈槽(32字节) }
- 与引用类型对比:
contract TypeComparison { // 基本类型:直接存储在栈中 uint256 public stackVar = 123; // 引用类型:存储引用在栈中,数据在其他位置 string public stringVar = "hello"; // 数据在存储中 uint256[] public arrayVar; // 数据在存储中 }
- 在函数(文档地址:数据位置修饰符)中的使用:
contract StackUsage { function calculate() public pure returns (uint256) { // 这些变量都在栈中 uint256 a = 1; uint256 b = 2; uint256 c = a + b; return c; } // 函数结束时栈变量自动清除 }
注意事项:
- 栈变量的生命周期仅限于函数执行期间
- 栈变量不需要手动管理内存
- 栈操作的 gas 成本较低
- 需要注意栈深度限制(1024层)
这些类型的特点:
- 大小固定
- 值类型(非引用)
- 操作简单直接
- gas 成本低
主要的引用类型:
特点: 在栈中:只存储引用(地址/指针) 实际数据:存储在 Memory 或 Storage 中
- 数组(Array)
contract ArrayExample { // Storage 数组(状态变量) uint[] public storageArray; // 存储在链上 function example() public { // Memory 数组(局部变量) uint[] memory memoryArray = new uint[](3); // 栈中只存储数组的引用(指针) // 实际数据在 Memory 或 Storage 中 } }
- 字符串(String)
contract StringExample { // Storage 字符串 string public storageStr = "hello"; // 存储在链上 function example() public pure { // Memory 字符串 string memory memoryStr = "world"; // 栈中存储字符串的引用 // 实际字符串数据在 Memory 或 Storage } }
- 结构体(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 } }
- 映射(Mapping)
contract MappingExample { // 只能声明为 Storage mapping(address => uint) public balances; function example() public { // ❌ 不能创建 Memory 映射 // mapping(address => uint) memory memoryMap; // 错误 // 只能在 Storage 中使用 balances[msg.sender] = 100; } }
- 不固定长度的字节数组(bytes)
// 引用类型示例 bytes public dynamicBytes; // 动态字节数组,存储在存储区(storage) bytes32 public fixedBytes; // 固定长度字节数组,是值类型 function example() public { // 动态分配内存 dynamicBytes = new bytes(2); dynamicBytes[0] = 0x12; dynamicBytes[1] = 0x34; // 可以改变长度 dynamicBytes.push(0x56); }
存储位置总结:
- Storage(链上存储)
- 状态变量
- 永久存储
- Gas 成本高 string public storageStr; // 状态变量自动存储在 Storage
- Memory(临时内存)
- 函数参数
- 函数(相关文档:数据位置修饰符)内的临时变量
- 函数返回值 function example(string memory param) public { string memory temp = "temp"; }
- 栈(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中的函数可见性修饰符有四种,决定了函数在何处可以被访问:
- private (私有):
- 只能在定义该函数的合约内部调用。
- internal (内部):
- 可在定义该函数的合约内部调用,也可从继承该合约的子合约中调用。
- external (外部):
- 只能从合约外部调用。如果需要从合约内部调用,必须使用
this
关键字。
- 只能从合约外部调用。如果需要从合约内部调用,必须使用
- public (公开):
- 可以从任何地方调用,包括合约内部、继承合约和合约外部。
- private (私有):
-
示例代码:
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 会:
- 查找 Alice 和 Bob 的账户状态
- 减少 Alice 的余额,增加 Bob 的余额
- 更新这些状态在 Merkle Trie 中的位置
- 生成新的根哈希,写入区块头
整型
整数类型用 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 支持三种基本的循环结构:
- for 循环
- while 循环
- 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;
} }
循环控制语句
-
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; } }
-
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 优化考虑
-
避免无限循环: 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++) { // 循环体 } } }
-
优化数组循环: 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++) { // 循环体 } } }
- 映射(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。
- 结构体(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 合约开发中最重要的数据结构之一,合理结合两者可以构建高效的数据存储模型。