---
title: "Yul storage 是如何工作的？"
description: "了解 Yul 智能合约编程语言中 storage 的工作原理"
---

# Yul storage 是如何工作的？

[Yul](http://www.alchemy.com/overviews/what-is-yul) 是一种中间编程语言，可以用来在[智能合约](https://www.alchemy.com/overviews/solidity-smart-contract)内编写一种形式的汇编语言。理解 Yul 可以提升你的智能合约水平，帮助用户节省 gas 成本。要开始用 Yul 编写智能合约，首先需要理解存储是如何工作的。

## **Yul 存储是如何工作的？**

在深入了解 Yul 的工作原理之前，我们需要先充分理解[智能合约中存储的工作方式](https://www.alchemy.com/docs/smart-contract-storage-layout)。存储由一系列插槽（slot）组成，一个智能合约有 2²⁵⁶ 个插槽。

在声明变量时，我们从插槽 0 开始，依次递增。每个插槽长度为 256 位（32 字节），这也是 uint256 和 bytes32 命名的由来。所有变量都会被转换为十六进制。

如果使用的变量比如 uint128 更小，则不会占满整个插槽，而是在左侧用 0 填充。让我们通过一个例子来更好地理解这一点。

var1：由于 uint256 变量等于 32 字节，var1 占满了插槽 0 的全部空间。

以下是插槽 0 中存储的内容：

`0x0000000000000000000000000000000000000000000000000000000000000100`

var2：地址（address）稍微复杂一些。由于地址只占用 20 字节的存储空间，因此会在左侧用 0 填充。

以下是插槽 1 中存储的内容：

`0x0000000000000000000000009acc1d6aa9b846083e8a497a661853aae07f0f00.`

var3：这个看起来很简单，插槽 2 被这个 bytes32 变量完全占用。

var4 和 var5：还记得我提到过 uint128 会被 0 填充吗？如果我们对变量进行排序，使它们的存储总和小于 32 字节，就可以把它们打包进同一个插槽！这叫做变量打包（packing variables），可以帮你节省 gas。

让我们看看插槽 3 中存储了什么：

`0x0000000000000000000000000000000200000000000000000000000000000001. `

注意 `0x000000000000000000000000000002` 和 `0x000000000000000000000000000001` 完美地放入了同一个插槽。这是因为它们各占 16 字节（半个插槽）。

<EmbeddedTable
  table={{
    columns: [
      { key: "1", width: 200, title: "Instruction", dataType: "object" },
      { key: "2", width: 200, title: "Explanation", dataType: "object" },
    ],
    data: [
      {
        "1": { title: "<p>sload(p)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>Loads the variable in slot p from storage.</p>", tooltip: "", icon: "" },
        id: 0,
      },
      {
        "1": { title: "<p>sstore(p,v)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>Assigns storage slot p value v.</p>", tooltip: "", icon: "" },
        id: 1,
      },
      {
        "1": { title: "<p>v.slot</p>", tooltip: "", icon: "" },
        "2": { title: "<p>Returns the storage slot of variable v.</p>", tooltip: "", icon: "" },
        id: 2,
      },
      {
        "1": { title: "<p>v.offset</p>", tooltip: "", icon: "" },
        "2": { title: "<p>Returns the index in bytes of where variable v begins in a storage slot. Variables are packed from right to left.</p>", tooltip: "", icon: "" },
        id: 3,
      },
    ],
  }}
/>

再看一个例子！

x = 3。这很合理，因为我们知道 var5 被打包进了插槽 3。

y = 16。这也说得通，因为我们知道 var4 占用了插槽 3 的一半。由于变量是从右到左打包的，所以 var5 的起始索引是字节 16。

z = 1。sstore\(\) 将插槽 0 的值设为 1。然后，我们通过 sload\(\) 将插槽 0 的值赋给 z。

在继续之前，你应该把这个函数添加到你的 remix 文件中。它可以帮助你查看每个存储插槽中存储的内容。

现在让我们来看一些更复杂的数据结构！

在处理静态数组时，EVM 知道需要为我们的数据分配多少个插槽。以这个数组为例，我们把 2 个元素打包进一个插槽。

所以如果你调用 `getValInHex\(4\)`，它会返回 `0x0000000000000000000000000000000100000000000000000000000000000000`。

正如预期的那样，从右往左读，我们看到的是值 0 和值 1。

插槽 5 包含 `0x0000000000000000000000000000000300000000000000000000000000000002`。

接下来我们来看动态数组。

试着调用 `getValInHex\(6\)`，你会看到它返回 0x00。由于 EVM 不知道需要分配多少个存储插槽，所以我们无法把数组存储在这里。

取而代之的是，当前存储插槽（插槽 6）的 keccak256 哈希值被用作数组的起始索引。从这里开始，我们只需加上所需元素的索引即可获取该值。

**这是一个演示如何找到动态数组中某个元素的代码示例：**

这里我们先获取数组的插槽，然后执行 add\(\) 操作并配合 sload\(\)，从而得到我们想要的数组元素的值。

你可能会问，是什么阻止了它与另一个变量的插槽发生冲突？

这种情况完全可能发生，但由于 2²⁵⁶ 是一个非常大的数字，发生的概率极低。

映射（mapping）的行为与动态数组类似，不同之处在于我们会把插槽和键（key）一起做哈希运算。

在这个演示中，我设置了映射值 var8\[1\] = 2。现在让我们看一个示例，了解如何获取映射中某个键对应的值。

正如你所看到的，这段代码与我们查找动态数组元素时的代码非常相似。主要区别在于，我们把键和插槽一起做哈希运算。

关于存储部分的最后一个内容是学习嵌套映射（nested mappings）。在继续阅读之前，我建议你根据目前学到的知识，自己尝试实现如何读取嵌套映射的值。

在这个例子中，我设置了映射值 var9\[0\]\[1\] = 2。

以下是代码，让我们来看看！

**具体过程如下：**

1. 我们首先获取第一个键（0）的哈希值。
1. 然后将其与第二个键（1）一起再次做哈希运算。
1. 最后，从存储中加载该插槽，得到我们要的值。

恭喜你，完成了关于 Yul 存储的这部分内容！

延伸阅读：[Yul Memory](http://www.alchemy.com/overviews/yul-memory)、[Yul Contract Calls](http://www.alchemy.com/overviews/yul-contract-calls) 和 [Yul Packed Storage Variables](http://www.alchemy.com/overviews/yul-packed-storage-variables)。
