在以太坊的Solidity语言中,“memory”关键字的作用是什么?

144

查看示例合同时,有时方法中声明了带有“memory”的数组,有时则没有。这有什么区别?

4个回答

169

如果没有使用memory关键字,Solidity 将尝试在storage中声明变量。

Solidity 领头开发人员 Chriseth: "您可以将存储视为具有虚拟结构的大型数组…一个在运行时无法更改的结构-它由合同中的状态变量确定"。

也就是说,在合同创建时,存储的结构是根据您的合同级别变量声明设置的,并且不能通过将来的方法调用进行更改。但是--该存储的内容可以通过 sendTransaction 调用进行更改。这样的调用会改变“状态”,这就是为什么合同级别变量被称为“状态变量”的原因。因此,在合同级别声明的变量 uint8 storage var; 可以更改为 uint8 的任何有效值(0-255),但是类型为 uint8 的“槽”将始终存在。

如果在函数中没有使用 memory 关键字声明变量,则 Solidity 将尝试使用存储结构,这目前可以编译,但可能会产生意外结果。 memory 告诉 Solidity 在方法运行时为变量创建一块空间,在该方法中保证其大小和结构可供将来使用。

memory 不能在合同级别使用,只能在方法中使用。

请参见 FAQ 中的“什么是 memory 关键字?它有什么作用?” 条目。以下是该条目的引用:

Ethereum 虚拟机有三个区域可以存储项。

第一个是“存储”,其中包含所有合同状态变量。每个合同都有自己的存储,它在函数调用之间是持久的,而且使用起来非常昂贵。

第二个是“内存”,这用于保存临时值。它在(外部)函数调用之间被擦除,使用起来更便宜。

第三个是堆栈,用于保持小的本地变量。它几乎是免费的,但只能容纳有限数量的值。

对于绝大多数类型,你不能指定它们应该存储在哪里,因为每次使用时都会被复制。

所谓存储位置很重要的类型是结构体和数组。例如,如果你在函数调用中传递这样的变量,如果它们可以留在内存或存储器中,则不会复制它们的数据。这意味着你可以在被调用的函数中修改它们的内容,并且这些修改仍然可见于调用者。

关于存储位置,有一些默认值取决于其所涉及的变量类型:

  • 状态变量始终存储在存储器中
  • 函数参数始终存储在内存中
  • 结构体、数组或映射类型的局部变量默认引用存储器
  • 值类型的局部变量(即既不是数组、结构体也不是映射的)存储在栈中

1
您有关于此的文档链接吗?我想更多地了解存储是如何工作的。 - Acapulco
2
常见问题解答链接无法使用,但如果您想阅读类似的链接,我建议访问https://docs.soliditylang.org/en/v0.5.3/introduction-to-smart-contracts.html?highlight=memory#storage-memory-and-the-stack。 - Quentin Gibson
3
我已经阅读了它,但仍需要一个初学者的解释。所以基本上为了避免昂贵的操作(节省存储空间),我们应该在函数参数之前使用memory关键字?如果内存是短暂的,那么使用它的原因是什么?合约如何调用这些函数并在部署后修改内存? - Null isTrue
2
作为一个从未使用过Solidity的人来说,变量默认不在内存中似乎很奇怪,而显式声明需要将它们持久化。 - Dominic
1
你能否添加一下与 calldata 的区别是什么? - Qwerty
显示剩余3条评论

26
  • 存储在函数调用之间保留数据。它类似于计算机硬盘。状态变量是存储的数据。这些状态变量驻留在区块链上智能合约数据部分中。将变量写入存储非常昂贵,因为运行事务的每个节点都必须执行相同的操作,这使得事务更加昂贵,并使区块链更大。

  • 内存是临时存储数据的地方,类似于RAM。函数参数和函数中的本地变量是内存数据。(如果函数是外部的,则args将存储在栈(calldata)中)。以太坊虚拟机对内存的空间有限,因此在函数调用之间存储在此处的值将被清除。

全局存储的成本为:第一次写入20000 wei,更新相同的存储位置的成本为5000 wei,读取存储的成本为200 wei。需要注意的是,这些成本是每32字节的存储成本。例如,读取64个字节将花费2 * 200 wei,即400 wei。

读写32字节数据的内存存储成本为2 wei。内存的成本远比全局存储便宜。

如您所知,访问数据库内部的数据比访问内存(会话,缓存)内部的数据更昂贵。

假设我们想要在函数中修改顶层状态变量。

this inside the function int[] public numbers

function Numbers()public{
    numbers.push(5)
    numbers.push(10)
    int[] storage myArray=numbers

   // numbers[0] will also be changed to 1
   myArray[0]=1 

  //Imagine you have an NFT contract and store the user's purchased nfts in a state variable on top-level
  // now inside a function maybe you need to delete one of the NFT's, since user sold it
  // so you will be modifying that list, inside a function using "storage"
}

int[] storage myArray=numbers在这种情况下,myArray会指向与“numbers”相同的地址(类似于JavaScript中引用对象的行为)。在函数中,我向“numbers”添加了5和10,这些数字放置到Storage中。但是如果您在remix上部署代码并获取numbers[0],由于myArray[0]=1,您将得到1。

如果将myArray定义为memory,那么情况就不同了。

// state variables are placed in Storage
int[] public numbers

function Numbers() public{
    numbers.push(5)
    numbers.push(10)
    // we are telling Solidity make numbers local variable using "memory"
    // That reduces gas cost of your contract
    int[] memory myArray=numbers
    myArray[0]=1 

   // Now, this time maybe you want to user's NFT's where price is less than 100 $
   // so you create an array stored in "memory" INSIDE the function
   // You loop through user's Nft's and push the ones that price<100
   // then return the memory variable
   // so, after you return the memory variable, it will be deleted from the memory

}

在这种情况下,“numbers”数组被复制到内存中,myArray现在引用一个与“numbers”地址不同的内存地址。如果您部署此代码并调用numbers[0],您将得到5。

  • 通过将存储变量复制到内存中,我们防止状态变量受到意外更改。每次客户端调用公共函数都会修改存储变量,想象一下如果成千上万的客户端调用相同的函数,如何跟踪状态变量。

我展示了一个简单的函数来说明它们之间的差异,这样可以在Remix上轻松测试。


1
由于int[] storage myArray只是指向numbers变量的指针,而没有为myArray在存储中保留空间。那么将myArray分配给numbers的燃气成本是多少? - 0xAnon
另外,myArray是一个存储引用,那么这个指针是存储在内存还是存储本身? - 0xAnon
简单来说(如果我理解有误请纠正):memory 关键字有两个含义:(1) 按值复制。 (2) 将变量声明为指向新分配的复制值的指针。 storage 的意思是:(1) 不按值复制;复制引用。 (2) 将变量声明为指向新分配的--复制值的指针。 - Stav Alfi
使用memory关键字,您可以将存储变量设为本地变量。已更新答案。 - Yilmaz

11

memory 在 Solidity 中定义了一种数据位置,可以在运行时临时保存值。Solidity 中的 memory 变量只能在方法中声明,并且通常用于方法参数。它是一个短期变量,不能保存在区块链上;它仅在函数执行期间保持其值,并在执行后销毁。

看一下示例函数 f(),其中我使用 memory 关键字声明了一个指针。它不会改变变量 User 的值,而如果使用 storage 声明,则会更改存储在区块链上的变量 User 的值,该值将不会被销毁...

struct User {
 string name;
}
User[] users;

function f() external {
 User memory user = users[0]; // create a pointer
 user.name = "example name" // can't change the value of struct User
}

1
当人们谈论Solidity中的存储和内存时,实际上可以涉及到这些词的两种不同用法。这会引起很多困惑。
这两种用法是:
1. Solidity合约存储数据的位置。 2. Solidity变量存储值的方式。
每种用法的示例:
1. Solidity合约存储数据的位置:正如Yilmaz正确指出的那样,在第一种用法中,存储和内存可以类比于硬盘(长期持久存储)和RAM(临时存储)。
例如:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract StorageMemory1{
    uint storageVariable;

    constructor() {
    }

    function assignToValue(uint memoryVariable) public {
        storageVariable = memoryVariable;
    }
}

在上面的示例中,'storageVariable'的值会在我们随着时间执行不同函数时被保存下来。然而,'memoryVariable'是在调用'assignToValue'函数时创建的,在函数完成后就永远消失了。 2. Solidity变量如何存储值: 如果你看到一个错误,说类似于“数据位置必须是“storage”,“memory”或“calldata”变量,但没有给出。”那么这就是它所指的。这个错误最好通过一个例子来理解。
例如:
使用以下代码会得到上述错误:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract StorageMemory2 {
    uint[] public values;

    function doSomething() public
    {
        values.push(5);
        values.push(10);

        uint[] newArray = values; // The error will show here
    }
}

但是如果你添加单词“memory”:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import 'hardhat/console.sol'; // to use console.log

contract StorageMemory2 {
    uint[] public values;

    function doSomething() public
    {
        values.push(5);
        values.push(10);

        console.log(values[0]); // it will log: 5

        uint[] storage newArray = values; // 'newArray' references/points to 'values'

        newArray[0] = 8888;

        console.log(values[0]); // it will log: 8888
        console.log(newArray[0]); // it will also log: 8888
    }
}

请注意添加单词“storage”所起到的作用:它使“newArray”变量引用(或指向)“values”变量,并且修改“newArray”也会修改“values”
然而,如果我们使用“memory”,请注意记录下来的内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import 'hardhat/console.sol'; // to use console.log

contract StorageMemory2 {
    uint[] public values;

    function doSomething() public
    {
        values.push(5);
        values.push(10);

        console.log(values[0]); // it will log: 5

        uint[] memory newArray = values; // 'newArray' is a separate copy of 'values'

        newArray[0] = 8888;

        console.log(values[0]); // it will log: 5
        console.log(newArray[0]); // it will log: 8888
    }
}

使用内存创建一个“副本”变量,它不引用“values”数组。
如果您感兴趣,“calldata”可以用来传递只读变量:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract CallDataExample {
    uint[] public values;

    function doSomething() public
    {
        values.push(5);
        values.push(10);

        modifyArray(values);
    }

    function modifyArray(uint[] calldata arrayToModify) pure private {
        arrayToModify[0] = 8888; // you will get an error saying the array is read only
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接