1.Memory,Storage & Calldata
在 Solidity 中,有以下几种数据存储位置:
- 栈(Stack):栈是一种临时存储区域,用于存储局部变量和函数参数。在函数执行期间,栈上的数据会被分配和释放,当函数执行完成时,栈上的数据也会被销毁。
- 内存(Memory):内存是一种临时存储区域,用于存储动态分配的数据,比如动态数组和字符串。与栈不同,内存中的数据不会随着函数执行的结束而销毁,需要手动清除。在函数调用期间,内存中的数据可以被读取和修改。
- 存储(Storage):存储是永久存储在区块链上的位置,用于存储合约的状态变量。存储中的数据会一直保存在区块链上,直到合约被销毁。存储是最昂贵的一种存储位置,因为它需要永久存储在区块链上,并且对存储操作收费。
- 调用数据(Calldata):调用数据是用于存储外部函数调用的参数和返回值的位置。在函数调用期间,输入参数会被复制到调用数据中,函数执行完成后,返回值也会被写入调用数据中。
- 代码(Code):代码用于存储合约本身的字节码,即合约的函数实现、逻辑等内容。
- 日志(Logs):日志用于记录合约的事件和状态变化,可以通过日志来实现合约的事件通知和审计功能。
本章节三个最重要的,就是Calldata,Memory和Storage,这是一个稍微进阶的知识点,所以,如果你第一次没有完全掌握它,那也完全没关系。
Storage
定义:storage是合约状态变量的默认存储位置。这意味着状态变量存储在区块链上,并在整个合约的生命周期内持续存在。
特点:
- 数据在函数调用之间持续存在。
- 对storage变量的修改会直接反映在合约的状态中。
- storage变量是可变的,可以在函数中修改。
- 访问storage变量通常比访问memory或calldata变量消耗更多的gas。
使用:状态变量自动存储在storage中,不需要显式指定。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract SimpleStorage {
uint[] public numbers;//默认存在storeage
function store(uint number) public {
numbers.push(number);//修改storage中的状态变量
}
}
memory
定义:memory是函数内部变量的默认存储位置,用于存储临时数据。
特点:
- 数据仅在函数执行期间存在,函数执行结束后数据会被清除。
- memory变量是可变的,可以在函数中修改。
- memory变量的赋值是独立的,对memory变量的修改不会影响storage中的数据。
- 访问memory变量比访问storage变量消耗的gas要少。
使用:当需要在函数中创建结构体、数组或映射的临时副本时,需要显式指定memory。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MemoryExample {
struct Person {
string name;
uint age;
}
function createPerson(string memory name, uint age) public pure returns (Person memory) {
Person memory person = Person(name, age); // 创建 memory 中的结构体实例
return person;
}
}
Calldata
定义:calldata是外部函数参数的默认存储位置,用于存储函数调用的输入数据。
特点:
- 数据仅在函数执行期间存在,与memory类似。
- calldata是不可修改的,即不能在函数内部修改calldata变量。
- 使用calldata可以节省gas,因为它不需要复制数据。
使用:通常用于外部函数的参数,特别是当参数是大型数组或结构体时。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MemoryExample {
function process(uint[] calldata data) external pure returns(uint sum){
for(uint i=0;i<data.length;i++){
sum+=data[i];//直接在calldata中读取数据
}
}
}
总结
- storage用于合约的状态变量,数据在函数调用之间持续存在。
- memory用于函数内的临时变量,数据仅在函数执行期间存在。
- calldata用于外部函数的输入参数,数据在函数执行期间存在,且不可修改。
正确使用这些数据位置关键字可以优化合约的性能和减少gas消耗。
2.如何选择这三个存储方式
在Solidity中,选择使用storage和memory主要取决于数据的使用场景和生命周期。以下是一些指导原则来帮助你决定何时使用storage和memory:
使用 storage 的情况:
- 1. 状态变量:合约的状态变量默认存储在storage中,并且应该在合约的生命周期内持久存在。
- 2. 持久化数据:当你需要保存数据以供合约的多个函数调用之间使用时,应该使用storage。
- 3. 大数组或映射:如果你需要频繁地更新大数组或映射,将这些数据存储在storage中会更加高效,因为每次修改都会直接发生在区块链的状态上。
使用 memory 的情况:
- 1. 临时变量:当你在函数中需要创建临时变量,并且这些变量不需要在函数调用之间保留时,应该使用memory。
- 2. 函数参数和返回值:对于需要传递给函数的复杂类型(如数组、结构体)的参数,应该使用memory。同样,如果你要从函数返回一个复杂类型,也应该使用memory。
- 3. 节省 gas:对于不需要持久化的数据,使用memory可以节省gas,因为不需要写入区块链的状态。
选择 storage 或 memory 的具体步骤:
1. 确定数据的使用频率:
- 如果数据需要在多个函数调用之间共享,使用storage。
- 如果数据仅在一个函数调用中需要,使用memory。
2. 考虑数据的持久性:
- 如果数据需要在合约的生命周期内持续存在,使用storage。
- 如果数据仅需要临时存储,使用memory。
3. 考虑成本和效率:
- 如果数据量大,频繁地写入storage会消耗大量gas,考虑是否可以优化数据结构或使用memory来减少成本。
- 对于大型数据结构,如果不需要修改,使用calldata(对于外部函数的参数)可以进一步节省gas。
4. 考虑数据是否需要修改:
- 如果需要在函数内部修改数据,使用memory(因为calldata是不可修改的)。
- 如果数据在函数调用中不需要修改,可以使用calldata作为外部函数的参数。
以下是一个简单的示例,说明如何选择storage和memory:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MemoryExample {
//状态变量
struct User{
string name;
uint age;
}
User[] public users;
//将新的用户添加到状态变量中
function addUser(string memory name,uint age)public{
User memory newUser =User(name,age);
//将临时变量复制到storage中的状态变量
users.push(newUser);
}
//处理用户数据,返回一个总和
function sumUserAges() public view returns(uint) {
uint sum =0;
//在memory中创建一个临时数组副本
User[] memory usersCopy =users;
for(uint i=0;i<usersCopy.length;i++){
sum+=usersCopy[i].age;
}
return sum;
}
}
在这个例子中,users是一个状态变量,存储在storage中。函数addUser接收一个memory参数,并创建一个memory中的User结构体实例,然后将其添加到storage中的users数组中。函数sumUserAges创建了一个memory中的数组副本,以避免直接在storage上操作,从而节省gas。
Mappings
1.Mapping
在Solidity中,mapping是一种可以被视为哈希表的键-值数据结构,它能够以非常高效的方式存储和查找数据。映射在Solidity中是非常有用的,因为它们允许你以O(1)的时间复杂度存储和检索键对应的值,而不需要遍历整个数据结构。
基本概念
- - 键(Key):映射中的键可以是任何除了映射、动态数组、合约、枚举和结构体以外的类型。这包括基本类型如uint、address、bytes32等。
- - 值(Value):映射中的值可以是任何类型,包括映射本身。
声明映射
映射的声明格式如下:
solidity
mapping(keyType => valueType) public mappingName;
例如,以下是如何声明一个将address映射到uint的映射:
solidity
mapping(address => uint) public balances;
这个balances映射可以被用来跟踪每个地址的余额。
访问映射
映射的值可以通过将键放入方括号中来访问和修改。如果映射中不存在该键,则其值默认为类型的初始值(例如,数字类型的初始值为0,布尔类型的初始值为false)。
以下是如何设置和检索映射中的值:
solidity
function setBalance(address _addr, uint _value) public {
balances[_addr] = _value;
}
function getBalance(address _addr) public view returns (uint) {
return balances[_addr];
}
映射的特点
- - 不存在长度或键的集合:由于映射的设计是为了优化查找效率,因此它不支持返回映射中的所有键或值的集合,也不支持获取映射的长度。
- - 存储位置:映射总是存储在storage中,不能在memory或calldata中声明映射。
- - 初始化:映射不需要显式初始化,因为所有键的值默认为初始值。
- - 可递归:映射可以作为值或键的类型,例如,你可以创建一个映射的映射。
- 映射的限制
- - 不能迭代:你不能遍历映射的所有键或值,因为没有提供获取所有键的方法。
- - 不能删除键:虽然你可以将映射中的值设置为其类型的初始值,但这并不等同于删除键。键仍然存在于映射中,并且其值可以被重新设置。
示例:使用映射的简单合约
以下是一个简单的合约示例,它使用映射来跟踪地址的余额:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MemoryExample {
mapping(address => uint) public balances;
function updateBalance(uint newBalance) public {
balances[msg.sender] =newBalance;
}
}
在这个合约中,任何调用updateBalance函数的地址都可以设置自己的余额。由于映射的键是address类型,所以每个地址都可以有一个唯一的余额。
映射是Solidity中非常有用的数据结构,特别是当你需要快速查找和更新与特定键相关联的值时。然而,由于它们的限制(如无法迭代),在设计合约时,需要仔细考虑是否使用映射。
接下来让我来看一个详细的例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract SimpleStorage {
// This gets initialized to zero!
// <- This means that this section is a comment!
uint256 public favoriteNumber;
mapping(string => uint256) public nameToFavoriteNumber;
struct People {
uint256 favoriteNumber;
string name;
}
// uint256[] public favoriteNumbersList;
People[] public people;
function store(uint256 _favoriteNumber) public {
favoriteNumber = _favoriteNumber;
retrieve();
}
// view, pure
function retrieve() public view returns(uint256) {
return favoriteNumber;
}
// calldata, memory, storage
function addPerson(string memory _name, uint _favoriteNumber) public {
people.push(People(_favoriteNumber, _name));
nameToFavoriteNumber[_name] = _favoriteNumber;
}
}
当你创建一个映射时,会把所有东西都初始化为空值,现在这里每一个可能的字符串,都有一个对应的初始值favoriteNumber为0。
所以我们要手动添加值,就利用我们的addPerson()函数,添加一个人到我们的映射中,等待这个交易确实完成了,然后,让我们多添加几个人。
现在,如果我们查找某个人最喜欢数字,将会立即得到结果,当然我们也可以在people数组中找到它们。