自学内容网 自学内容网

solidity中的mapping以及Memory,Storage & Calldata

1.Memory,Storage & Calldata

在 Solidity 中,有以下几种数据存储位置:

  1. 栈(Stack):栈是一种临时存储区域,用于存储局部变量和函数参数。在函数执行期间,栈上的数据会被分配和释放,当函数执行完成时,栈上的数据也会被销毁。
  2. 内存(Memory):内存是一种临时存储区域,用于存储动态分配的数据,比如动态数组和字符串。与栈不同,内存中的数据不会随着函数执行的结束而销毁,需要手动清除。在函数调用期间,内存中的数据可以被读取和修改。
  3. 存储(Storage):存储是永久存储在区块链上的位置,用于存储合约的状态变量。存储中的数据会一直保存在区块链上,直到合约被销毁。存储是最昂贵的一种存储位置,因为它需要永久存储在区块链上,并且对存储操作收费。
  4. 调用数据(Calldata):调用数据是用于存储外部函数调用的参数和返回值的位置。在函数调用期间,输入参数会被复制到调用数据中,函数执行完成后,返回值也会被写入调用数据中。
  5. 代码(Code):代码用于存储合约本身的字节码,即合约的函数实现、逻辑等内容。
  6. 日志(Logs):日志用于记录合约的事件和状态变化,可以通过日志来实现合约的事件通知和审计功能。

本章节三个最重要的,就是Calldata,Memory和Storage,这是一个稍微进阶的知识点,所以,如果你第一次没有完全掌握它,那也完全没关系。

Storage

 定义:storage是合约状态变量的默认存储位置。这意味着状态变量存储在区块链上,并在整个合约的生命周期内持续存在。

 特点:

  1.    数据在函数调用之间持续存在。
  2.    对storage变量的修改会直接反映在合约的状态中。
  3.    storage变量是可变的,可以在函数中修改。
  4.    访问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是函数内部变量的默认存储位置,用于存储临时数据。

 特点:

  1.    数据仅在函数执行期间存在,函数执行结束后数据会被清除。
  2.    memory变量是可变的,可以在函数中修改。
  3.    memory变量的赋值是独立的,对memory变量的修改不会影响storage中的数据。
  4.    访问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是外部函数参数的默认存储位置,用于存储函数调用的输入数据。

 特点:

  1.    数据仅在函数执行期间存在,与memory类似。
  2.    calldata是不可修改的,即不能在函数内部修改calldata变量。
  3.    使用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中读取数据
    }
   }
}

 总结

  1.  storage用于合约的状态变量,数据在函数调用之间持续存在。
  2.  memory用于函数内的临时变量,数据仅在函数执行期间存在。
  3.  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];
}

 映射的特点

  1. - 不存在长度或键的集合:由于映射的设计是为了优化查找效率,因此它不支持返回映射中的所有键或值的集合,也不支持获取映射的长度。
  2. - 存储位置:映射总是存储在storage中,不能在memory或calldata中声明映射。
  3. - 初始化:映射不需要显式初始化,因为所有键的值默认为初始值。
  4. - 可递归:映射可以作为值或键的类型,例如,你可以创建一个映射的映射。
  5.  映射的限制
  6. - 不能迭代:你不能遍历映射的所有键或值,因为没有提供获取所有键的方法。
  7. - 不能删除键:虽然你可以将映射中的值设置为其类型的初始值,但这并不等同于删除键。键仍然存在于映射中,并且其值可以被重新设置。

 示例:使用映射的简单合约

以下是一个简单的合约示例,它使用映射来跟踪地址的余额:

// 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数组中找到它们。 


原文地址:https://blog.csdn.net/2302_79993788/article/details/142865515

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!