Solidity开发模式 - 内存数组构建

本文是Solidity开发模式系列第二篇

目的

高效地使用gas从合约中汇总或提取数据

动机

与区块链合约的存储进行交互是EVM最昂贵的操作之一。因此,只应保存必要的数据,果可能,应避免冗余。这与传统的软件架构形成鲜明对比,传统的软件架构存储成本低,数据存储用来优化性能。虽然大多数情况下,传统系统查询的唯一成本就是时间,但在以太坊中,即使最简单的查询也会花费大量具有货币价值的gas。降低gas成本的一个方式是声明变量为public,后台会自动生成变量的getter方法从而免费获取变量值。但是,如果我们想要从多个来源聚合数据,就需要读取大量存储,导致非常昂贵。

使用此模式,我们利用Solidity的view修饰符,它允许我们从合约存储中聚合和读取数据而不需要任何成本。每次请求是,都会在内存中重建数组,而不是保存在合约存储中。这在传统系统中是低效的,但在Solidity中,却很有效,因为使用view修饰符声明的方法不允许写入存储,因此不会修改区块链状态。执行这些方法所需的数据都可以从本地节点获取。由于区块链状态保持不变,无需向网络广播交易。没有交易意味着没有gas,使得view方法调用免费,只要是从外部调用,而不是从另个合约调用,这种情况下,交易是必须的,必将消耗gas。

适用性

在以下条件时使用内存数组构建模式

  • 希望从合约存储中获取聚合数据
  • 获取数据时希望避免支付gas
  • 数据会发生变化

参与者和协作

此模式的参与者是合约本身以及请求存储数据的实体。要达到完全免费,必须从外部发出请求,这意味着不能从另一个合约中发出请求,因为这将导致大量消耗gas的交易。

实现

实现分为两部分。第一部分介绍请求数据的存储方式,第二部分介绍数据的聚合和获取。

  1. 为了方便获取数据,应选择易于迭代的数据结构,在Solidity中是数组。需要聚合时,数据通常有多个属性,这可以通过自定义数据类型结构体实现。结合这两者,我们最终得到一个结构体数组,每个数组元素结构体包含所有的属性。另外需要一个映射,它保存聚合实例的数据条目数,将在第二部分中用到。
  2. 在view方法中执行聚合不会花费gas。一个问题是Solidity不允许方法返回结构体数组。对此,建议只返回所需结构体的ID,然后根据ID逐个请求结构体。由于这些操作都不会改变合约状态,因此也都是免费的。为了收集所需的ID,首先创建一个内存数组保存它们。而Solidity不允许创建动态内存数组,这里可以利用第一部分的映射获得条目数,并以此创建数组。实际的聚合是通过for循环完成,符合聚合条件的ID被保存到内存数组中,并在循环结束后返回。因为这些结算都是在本地节点执行的而不是网络节点,所以在动态数组中执行这种昂贵迭代没有问题也不会耗尽gas。

代码示例

这个示例将展示如何聚合属于事务发送者的条目。

// This code has not been professionally audited, therefore I cannot make any promises about
// safety or correctness. Use at own risk.
contract MemoryArrayBuilding {

    struct Item {
        string name;
        string category;
        address owner;
        uint32 zipcode;
        uint32 price;
    }

    Item[] public items;

    mapping(address => uint) public ownerItemCount;

    function getItemIDsByOwner(address _owner) public view returns (uint[]) {
        uint[] memory result = new uint[](ownerItemCount[_owner]);
        uint counter = 0;
        
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

第5行定义一个示例结构体包含多个属性,包括等会儿用作聚合检查的属性所有者。第13行的数组包含所有的结构体。第15行映射保存每个所有者拥有的结构体数量,第18行初始化内存数组需要使用它。返回给定所有者的条目ID的方法在第17行定义并有view修饰符。第21行的for循环,对所有结构体进行迭代,并判断它们的所有者是否和给定的相同(第22行)。如果相同,将ID保存在内存数组中。检查完所有条目后,返回数据。由于第13行中的items数组是public,可以直接按照ID查询结构体而无需事务。

Gas分析

这种模式的gas分析很容易。同样,使用Solidity在线编译器Remix计算所需gas。实验代码可以在GitHub上找到。实验中,使用代码示例中的设置,并用10个结构体初始化它,其中两个属于给定地址。然后分别从外部账户和另一个合约中调用getitemIDsbyowner(address _owner)方法两次。一次方法含有view修饰符,另一次不包含。结果如下表所示,只有来自外部调用和view方法的组合才能免费,而其他组合都需要花费gas,因为事务会被广播到网络中。

view方法普通方法
外部调用032623
合约调用3262332623

结果

采用本模式最明显的结果是完全规避了事务成本,在频繁使用该方法时可以节约大量资金。解决上面问题的另个方法是为每个要聚合的示例存储一个数组(例如,为每个所有者)。一旦修改条目所有者,这将导致大量的gas消耗。必须从一个数组删除该项,将其添加到另一个数组中,并移动第一个数组中的每个元素以填充空白以及减少数组长度。所有这些都是合约存储的昂贵操作。要在建议方案中更改所有者,只需修改相关属性和保存条目数的映射即可。

但这种模式不是只有好处。实现它,增加了复杂性。同单独数组分离方案相比,将所有条目保存在一个数组中有违直观。另外,每次调用做聚合而不是聚合一次并保存结果这种理念在开始时可能会令人困惑。

已知应用

著名的加密猫合约中可以找到这个模式的实现。第655行,有一个名为tokensofowner(address_owner)的方法,它返回属于给定地址的所有猫咪的ID。

另一个例子现在关闭了,基于以太坊的游戏 Slotthereum。在这个合约中,模式以类似于示例的方式使用,来获取所有游戏的ID。


更多Solidity开发模式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值