美文网首页
OpenZeppelin库DoubleEndedQueue数据结

OpenZeppelin库DoubleEndedQueue数据结

作者: 渚清与沙白 | 来源:发表于2023-08-21 15:28 被阅读0次

顾名思义DoubleEndedQueue是一个队列,是一个双端队列。
双端队列是一种抽象数据类型,它概括了一个队列,其中的元素可以从前(头) 或后(尾) 添加或删除。 因此也经常被称为首尾链表。
用Solidity语言如何来实现一个双端队列呢?

分析源码可知,定义了一个结构体Bytes32Deque。其中声明了一个mapping(int128 => byte32)类型的变量_data;还声明了两个int128类型的变量_begin和_end。_begin和_end作为mapping的key使用。

mapping类型中的byte32可以理解,因为值类型的数据都可以经过类型转换得到byte32类型的数据。

为什么_begin和_end要定义成int128类型呢?
// 查询首尾元素时,队列为空抛出该错误
error Empty();
// 通过元素在队列中的位置查询元素时,检查越界问题
error OutOfBounds();

// _begin队列头,_end队列位,作为mapping的key使用。
// mapping 存储数据
struct Bytes32Deque {
        int128 _begin;
        int128 _end;
        mapping(int128 => bytes32) _data;
    }

通过_begin和_end的类型可以分析出,对列的最大长度是uint256。
因为_begin到_end的长度就是两个int128,所以最大长度则是uint256。
这里要注意int128包含正数和负数,也就意味着mapping的key可以为负数和正数。

双端队列的特性是支持头尾插入、删除。下面我们分析下如何实现头尾插入和删除。

头尾插入元素

/**  队列尾插入元素
     * @dev Inserts an item at the end of the queue.
     */
    function pushBack(Bytes32Deque storage deque, bytes32 value) internal {
        // 获取当前队列尾的位置,再直接存储值
        int128 backIndex = deque._end;
        deque._data[backIndex] = value;
        unchecked {
            // 加1,更新队列尾的位置
            deque._end = backIndex + 1;
        }
    }

    /** 队列头插入元素
     * @dev Inserts an item at the beginning of the queue.
     */
    function pushFront(Bytes32Deque storage deque, bytes32 value) internal {
        int128 frontIndex;
        unchecked {
            // 头插入,找到当前头的位置,在减去 1
            frontIndex = deque._begin - 1; 
        }
        // 存储值
        deque._data[frontIndex] = value;
        // 更新队列头的位置
        deque._begin = frontIndex;
    }

pushBack函数是从队列末尾插入元素。先获取当前队列尾的位置,再直接存储值,最后加1更新_end。
pushFront函数是从队列头部插入元素。先找到当前头的位置,减去 1,再存储值,最后更新_begin。

由于_begin和_end都是int128类型,所以默认值都是0。pushBack函数在进行尾插入是从0开始,依次递增,占用正数段;而pushFront头插入是从-1开始,依次递减,占用负数段。

头尾删除元素

 /**   检查队列是否为空,只对首尾位置进行判断,并未操作mapping
     * @dev Returns true if the queue is empty.
     */
    function empty(Bytes32Deque storage deque) internal view returns (bool) {
        return deque._end <= deque._begin;
    }

/**   队列尾删除元素
     * @dev Removes the item at the end of the queue and returns it.
     *
     * Reverts with `Empty` if the queue is empty.
     */
    function popBack(Bytes32Deque storage deque) internal returns (bytes32 value) {
        if (empty(deque)) revert Empty();
        int128 backIndex;
        unchecked {
            // 这里减1,因为 每次队列尾插入后,会先加1再更新_end的值,意味着在有效范围内,队列最后一个位置没有元素。
            // 这里需要先减去1,获取到最后一个元素的位置
            backIndex = deque._end - 1;
        }
        // 从mapping中取值,并返回
        value = deque._data[backIndex];
        // 删除mapping中的该值
        delete deque._data[backIndex];
        // 更新末尾位置
        deque._end = backIndex;
    }

 /**   队列头删除元素
     * @dev Removes the item at the beginning of the queue and returns it.
     *
     * Reverts with `Empty` if the queue is empty.
     */
    function popFront(Bytes32Deque storage deque) internal returns (bytes32 value) {
        if (empty(deque)) revert Empty();
        // 队列头插入时,更新begin时,存在元素,这里不需要加减 1
        int128 frontIndex = deque._begin;
        // 从mapping获取元素,并返回
        value = deque._data[frontIndex];
        // 删除mapping中该元素
        delete deque._data[frontIndex];
        unchecked {
            // 更新头部的位置,因为删除了队列的第一个元素,又因头插入占用的是负数段,所以头部位置需要加 1。
            deque._begin = frontIndex + 1;
        }
    }

popBack函数是从队列末尾删除元素。先校验空队列,再进行删除,最后更新末尾位置。可以看到backIndex的 值是由_end减去1得来,这里减1,因为每次队列尾插入后,会先加1再更新_end的值,意味着在有效范围内,队列最后一个位置没有元素。

popFront函数是从队列头部删除元素。同样是先校验空队列,在获取frontIndex的值时,没有进行减1操作。最后在更新_begin时,做了加1操作,因为删除了队列的第一个元素,又因头插入占用的是负数段,所以头部位置需要加 1。例如,队列头部_begin的值是 -10,删除一个元素后,_begin的值应更新为 -9。

查询首尾元素

/**
     * @dev Returns the item at the beginning of the queue.
     *
     * Reverts with `Empty` if the queue is empty.
     */
    function front(Bytes32Deque storage deque) internal view returns (bytes32 value) {
        if (empty(deque)) revert Empty();
        int128 frontIndex = deque._begin;
        return deque._data[frontIndex];
    }

    /**
     * @dev Returns the item at the end of the queue.
     *
     * Reverts with `Empty` if the queue is empty.
     */
    function back(Bytes32Deque storage deque) internal view returns (bytes32 value) {
        if (empty(deque)) revert Empty();
        int128 backIndex;
        unchecked {
            backIndex = deque._end - 1;
        }
        return deque._data[backIndex];
    }

front函数是获取队列头第一个元素。
back函数是获取队列最后一个元素。backIndex = deque._end - 1; 这里还是那个原理,队列尾插入后,更新_end时做了加1操作。

根据队列位置查询元素

/**
     * @dev Return the item at a position in the queue given by `index`, with the first item at 0 and last item at
     * `length(deque) - 1`.
     *
     * Reverts with `OutOfBounds` if the index is out of bounds.
     */
    function at(Bytes32Deque storage deque, uint256 index) internal view returns (bytes32 value) {
        // int256(deque._begin) is a safe upcast
        // 入参类型是 uint256,必须先转换成 int256
       // _begin要与index求和,所以_begin的类型是int128,也必须向上转换成 int256
      // idx是 _begin到_end之间的值
        int128 idx = SafeCast.toInt128(int256(deque._begin) + SafeCast.toInt256(index));
        if (idx >= deque._end) revert OutOfBounds();
        return deque._data[idx];
    }

    // SafeCast.sol
    function toInt128(int256 value) internal pure returns (int128 downcasted) {
        downcasted = int128(value);
        require(downcasted == value, "SafeCast: value doesn't fit in 128 bits");
    }

    function toInt256(uint256 value) internal pure returns (int256) {
        // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive
        require(value <= uint256(type(int256).max), "SafeCast: value doesn't fit in an int256");
        return int256(value);
    }

at函数不太好理解,里面用到了一个类型转换工具SafeCast。我们先看at函数的传参,传递的是uint256,这里为什么需要传uint256类型呢?
前面我们已经提到过,队列的最大长度是uint256,这里的index取值范围就是 0 到 type(uint256).max。
index表示,要查询队列中哪一个位置的元素。
index = 0,要查询队列第一个元素
index = 1,要查询队列第二个元素
index = 2,要查询队列第三个元素
.....
.....
所以,index只能是uint类型,不可能是int类型。队列最大长度是两个int128,所以参数只能是uint256。
在计算idx时,指定的类型是int128,idx也就是_begin到_end的取值范围。最后做了一个越界的验证。

SafeCast.toInt128(int256(deque._begin) + SafeCast.toInt256(index)); 将 _begin 由 int128 转换成 int256,将 index 由 uint256 转换成 int256;二者求和,最后再将和由 int256 转换成 int128 类型。

这里为什么要进行这么多的类型转换?
原因还得从函数的入参类型和最终需要的类型是什么说起,入参是 uint256,最终需要的是 int128。所以,入参index必须要进行类型转换将uint类型转换成int类型,所以就有了SafeCast.toInt256(index)。

_begin本身就是int128类型,为什么也需要进行转换呢?
因为要讲_begin 与 index求和,类型必须要统一。index的类型现在是 int256 ,所以 _begin也必须由 int128 类型向上转换成 int256 类型。

最后SafeCast.toInt128就是再将和的类型转换成需要的int128类型。

为什么要对_begin和index求和?
因为_begin是队列的开始位置。例如要_begin = -2,index传入的值是1,查询队列第一个位置的元素,mapping的key也就是 -1,_begin 与 index 之和就是 -1。

清空队列

function clear(Bytes32Deque storage deque) internal {
        deque._begin = 0;
        deque._end = 0;
    }

清空队列很简单,将开始位置和结束位置设置为默认值0,并没有对mapping内部的元素进行删除操作。

队列长度

function length(Bytes32Deque storage deque) internal view returns (uint256) {
        // The interface preserves the invariant that begin <= end so we assume this will not overflow.
        // We also assume there are at most int256.max items in the queue.
        unchecked {
            return uint256(int256(deque._end) - int256(deque._begin));
        }
    }

队列长度就是_end减去_begin。这里类型转换是因为长度是uint256类型,_end和_begin都是int128类型,所以需要进行类型转换。

总结

  1. 只能从头或者尾依次添加或删除元素,无法从中间某个位置删除或添加。
  2. 可以方便快速地获取头尾位置的元素。
  3. 遵循FIFO原则。
  4. 使用时应使用storage,不能使用memery。

思考:

  1. 前面我们分析_begin的值是负数段,那队列的头部位置_begin的值可以是大于0的数吗?
    创建Bytes32Deque对象时,设置_begin的默认值大于0。
  2. 清空队列为什么不删除元素?
    gas费用问题。

相关文章

  • Solodity知识点集 — Ownable合约(五)

    OpenZeppelin库的Ownable合约 OpenZeppelin 是主打安保和社区审查的智能合约库,您可以...

  • Solidity智能合约:Ownable Contracts

    内容来自:https://cryptozombies.io/ OpenZeppelin库的Ownable 合约 下...

  • REMIX+Openzeppelin建立NFT智能合约

    安装openzeppelin智能合约模版 npm install @openzeppelin/contracts ...

  • MySQL注入总结

    01 MySQL数据库结构及常用函数 一、MySQL数据库结构 MySQL数据库采用库=>表=>列=>数据的存储结...

  • mySql常用语句

    查看有多少库 建库 切换库 查看库中有多少表 建表 查看表结构 插入数据库 查询数据 修改数据 删除数据 修改表结...

  • MySQL基本操作

    mysql 常用命令 数据库操作 连接数据库 退出数据库 查看所有数据库 选择使用的数据库 查看所有的表 查看表结...

  • MySql基础(一)

    文章摘要:1、连接、退出MySql数据库2、查询MySql用户以及localHost3、创建数据库、显示数据库表结...

  • mysql的基本操作

    登录 查看数据库 创建数据库 删除数据库 选择数据库 查看有哪些用户 创建用户 删除用户 为用户分配权限 查看表结...

  • nodejs前后端的数据交互

    数据流向一般是:前端请求 ---->后端数据校验 ---->查询数据库 ---->响应前端 基本的数据库sql(结...

  • SQLAlchemy 关于ORM的理解

    ORM (Object Relational Mapper) 通常, 数据库都是关系型数据库, 表, 就是个二维结...

网友评论

      本文标题:OpenZeppelin库DoubleEndedQueue数据结

      本文链接:https://www.haomeiwen.com/subject/oeosmdtx.html