OpenZeppelin庫DoubleEndedQueue數(shù)據(jù)結(jié)構(gòu)的分析

顧名思義DoubleEndedQueue是一個隊(duì)列题造,是一個雙端隊(duì)列择份。
雙端隊(duì)列是一種抽象數(shù)據(jù)類型飒炎,它概括了一個隊(duì)列秆吵,其中的元素可以從前(頭) 或后(尾) 添加或刪除岭皂。 因此也經(jīng)常被稱為首尾鏈表牵寺。
用Solidity語言如何來實(shí)現(xiàn)一個雙端隊(duì)列呢叫胁?

分析源碼可知亡问,定義了一個結(jié)構(gòu)體Bytes32Deque枉侧。其中聲明了一個mapping(int128 => byte32)類型的變量_data引瀑;還聲明了兩個int128類型的變量_begin和_end。_begin和_end作為mapping的key使用榨馁。

mapping類型中的byte32可以理解憨栽,因?yàn)橹殿愋偷臄?shù)據(jù)都可以經(jīng)過類型轉(zhuǎn)換得到byte32類型的數(shù)據(jù)。

為什么_begin和_end要定義成int128類型呢翼虫?
// 查詢首尾元素時屑柔,隊(duì)列為空拋出該錯誤
error Empty();
// 通過元素在隊(duì)列中的位置查詢元素時,檢查越界問題
error OutOfBounds();

// _begin隊(duì)列頭珍剑,_end隊(duì)列位掸宛,作為mapping的key使用。
// mapping 存儲數(shù)據(jù)
struct Bytes32Deque {
        int128 _begin;
        int128 _end;
        mapping(int128 => bytes32) _data;
    }

通過_begin和_end的類型可以分析出招拙,對列的最大長度是uint256唧瘾。
因?yàn)開begin到_end的長度就是兩個int128,所以最大長度則是uint256别凤。
這里要注意int128包含正數(shù)和負(fù)數(shù)饰序,也就意味著mapping的key可以為負(fù)數(shù)和正數(shù)。

雙端隊(duì)列的特性是支持頭尾插入规哪、刪除求豫。下面我們分析下如何實(shí)現(xiàn)頭尾插入和刪除。

頭尾插入元素

/**  隊(duì)列尾插入元素
     * @dev Inserts an item at the end of the queue.
     */
    function pushBack(Bytes32Deque storage deque, bytes32 value) internal {
        // 獲取當(dāng)前隊(duì)列尾的位置诉稍,再直接存儲值
        int128 backIndex = deque._end;
        deque._data[backIndex] = value;
        unchecked {
            // 加1蝠嘉,更新隊(duì)列尾的位置
            deque._end = backIndex + 1;
        }
    }

    /** 隊(duì)列頭插入元素
     * @dev Inserts an item at the beginning of the queue.
     */
    function pushFront(Bytes32Deque storage deque, bytes32 value) internal {
        int128 frontIndex;
        unchecked {
            // 頭插入,找到當(dāng)前頭的位置杯巨,在減去 1
            frontIndex = deque._begin - 1; 
        }
        // 存儲值
        deque._data[frontIndex] = value;
        // 更新隊(duì)列頭的位置
        deque._begin = frontIndex;
    }

pushBack函數(shù)是從隊(duì)列末尾插入元素蚤告。先獲取當(dāng)前隊(duì)列尾的位置,再直接存儲值舔箭,最后加1更新_end罩缴。
pushFront函數(shù)是從隊(duì)列頭部插入元素蚊逢。先找到當(dāng)前頭的位置,減去 1箫章,再存儲值烙荷,最后更新_begin。

由于_begin和_end都是int128類型檬寂,所以默認(rèn)值都是0终抽。pushBack函數(shù)在進(jìn)行尾插入是從0開始,依次遞增桶至,占用正數(shù)段昼伴;而pushFront頭插入是從-1開始,依次遞減镣屹,占用負(fù)數(shù)段圃郊。

頭尾刪除元素

 /**   檢查隊(duì)列是否為空,只對首尾位置進(jìn)行判斷女蜈,并未操作mapping
     * @dev Returns true if the queue is empty.
     */
    function empty(Bytes32Deque storage deque) internal view returns (bool) {
        return deque._end <= deque._begin;
    }

/**   隊(duì)列尾刪除元素
     * @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持舆,因?yàn)?每次隊(duì)列尾插入后,會先加1再更新_end的值伪窖,意味著在有效范圍內(nèi)逸寓,隊(duì)列最后一個位置沒有元素。
            // 這里需要先減去1覆山,獲取到最后一個元素的位置
            backIndex = deque._end - 1;
        }
        // 從mapping中取值竹伸,并返回
        value = deque._data[backIndex];
        // 刪除mapping中的該值
        delete deque._data[backIndex];
        // 更新末尾位置
        deque._end = backIndex;
    }

 /**   隊(duì)列頭刪除元素
     * @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();
        // 隊(duì)列頭插入時,更新begin時簇宽,存在元素勋篓,這里不需要加減 1
        int128 frontIndex = deque._begin;
        // 從mapping獲取元素,并返回
        value = deque._data[frontIndex];
        // 刪除mapping中該元素
        delete deque._data[frontIndex];
        unchecked {
            // 更新頭部的位置晦毙,因?yàn)閯h除了隊(duì)列的第一個元素生巡,又因頭插入占用的是負(fù)數(shù)段耙蔑,所以頭部位置需要加 1见妒。
            deque._begin = frontIndex + 1;
        }
    }

popBack函數(shù)是從隊(duì)列末尾刪除元素。先校驗(yàn)空隊(duì)列甸陌,再進(jìn)行刪除须揣,最后更新末尾位置∏恚可以看到backIndex的 值是由_end減去1得來耻卡,這里減1,因?yàn)槊看侮?duì)列尾插入后牲尺,會先加1再更新_end的值卵酪,意味著在有效范圍內(nèi)幌蚊,隊(duì)列最后一個位置沒有元素。

popFront函數(shù)是從隊(duì)列頭部刪除元素溃卡。同樣是先校驗(yàn)空隊(duì)列溢豆,在獲取frontIndex的值時,沒有進(jìn)行減1操作瘸羡。最后在更新_begin時漩仙,做了加1操作,因?yàn)閯h除了隊(duì)列的第一個元素犹赖,又因頭插入占用的是負(fù)數(shù)段队他,所以頭部位置需要加 1。例如峻村,隊(duì)列頭部_begin的值是 -10麸折,刪除一個元素后,_begin的值應(yīng)更新為 -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函數(shù)是獲取隊(duì)列頭第一個元素磕谅。
back函數(shù)是獲取隊(duì)列最后一個元素。backIndex = deque._end - 1; 這里還是那個原理雾棺,隊(duì)列尾插入后膊夹,更新_end時做了加1操作。

根據(jù)隊(duì)列位置查詢元素

/**
     * @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
        // 入?yún)㈩愋褪?uint256捌浩,必須先轉(zhuǎn)換成 int256
       // _begin要與index求和放刨,所以_begin的類型是int128,也必須向上轉(zhuǎn)換成 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函數(shù)不太好理解尸饺,里面用到了一個類型轉(zhuǎn)換工具SafeCast进统。我們先看at函數(shù)的傳參,傳遞的是uint256浪听,這里為什么需要傳uint256類型呢螟碎?
前面我們已經(jīng)提到過,隊(duì)列的最大長度是uint256迹栓,這里的index取值范圍就是 0 到 type(uint256).max掉分。
index表示,要查詢隊(duì)列中哪一個位置的元素克伊。
index = 0酥郭,要查詢隊(duì)列第一個元素
index = 1,要查詢隊(duì)列第二個元素
index = 2愿吹,要查詢隊(duì)列第三個元素
.....
.....
所以不从,index只能是uint類型,不可能是int類型犁跪。隊(duì)列最大長度是兩個int128椿息,所以參數(shù)只能是uint256歹袁。
在計(jì)算idx時,指定的類型是int128寝优,idx也就是_begin到_end的取值范圍宇攻。最后做了一個越界的驗(yàn)證。

SafeCast.toInt128(int256(deque._begin) + SafeCast.toInt256(index)); 將 _begin 由 int128 轉(zhuǎn)換成 int256倡勇,將 index 由 uint256 轉(zhuǎn)換成 int256逞刷;二者求和,最后再將和由 int256 轉(zhuǎn)換成 int128 類型妻熊。

這里為什么要進(jìn)行這么多的類型轉(zhuǎn)換夸浅?
原因還得從函數(shù)的入?yún)㈩愋秃妥罱K需要的類型是什么說起,入?yún)⑹?uint256扔役,最終需要的是 int128帆喇。所以,入?yún)ndex必須要進(jìn)行類型轉(zhuǎn)換將uint類型轉(zhuǎn)換成int類型亿胸,所以就有了SafeCast.toInt256(index)坯钦。

_begin本身就是int128類型,為什么也需要進(jìn)行轉(zhuǎn)換呢?
因?yàn)橐v_begin 與 index求和侈玄,類型必須要統(tǒng)一婉刀。index的類型現(xiàn)在是 int256 ,所以 _begin也必須由 int128 類型向上轉(zhuǎn)換成 int256 類型序仙。

最后SafeCast.toInt128就是再將和的類型轉(zhuǎn)換成需要的int128類型突颊。

為什么要對_begin和index求和?
因?yàn)開begin是隊(duì)列的開始位置潘悼。例如要_begin = -2律秃,index傳入的值是1,查詢隊(duì)列第一個位置的元素治唤,mapping的key也就是 -1棒动,_begin 與 index 之和就是 -1。

清空隊(duì)列

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

清空隊(duì)列很簡單宾添,將開始位置和結(jié)束位置設(shè)置為默認(rèn)值0船惨,并沒有對mapping內(nèi)部的元素進(jìn)行刪除操作。

隊(duì)列長度

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));
        }
    }

隊(duì)列長度就是_end減去_begin辞槐。這里類型轉(zhuǎn)換是因?yàn)殚L度是uint256類型掷漱,_end和_begin都是int128類型粘室,所以需要進(jìn)行類型轉(zhuǎn)換榄檬。

總結(jié)

  1. 只能從頭或者尾依次添加或刪除元素,無法從中間某個位置刪除或添加衔统。
  2. 可以方便快速地獲取頭尾位置的元素鹿榜。
  3. 遵循FIFO原則海雪。
  4. 使用時應(yīng)使用storage,不能使用memery舱殿。

思考:

  1. 前面我們分析_begin的值是負(fù)數(shù)段奥裸,那隊(duì)列的頭部位置_begin的值可以是大于0的數(shù)嗎?
    創(chuàng)建Bytes32Deque對象時沪袭,設(shè)置_begin的默認(rèn)值大于0湾宙。
  2. 清空隊(duì)列為什么不刪除元素?
    gas費(fèi)用問題冈绊。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侠鳄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子死宣,更是在濱河造成了極大的恐慌伟恶,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅该,死亡現(xiàn)場離奇詭異博秫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)眶掌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門挡育,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人朴爬,你說我怎么就攤上這事静盅。” “怎么了寝殴?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵蒿叠,是天一觀的道長。 經(jīng)常有香客問我蚣常,道長市咽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任抵蚊,我火速辦了婚禮施绎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贞绳。我一直安慰自己谷醉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布冈闭。 她就那樣靜靜地躺著俱尼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪萎攒。 梳的紋絲不亂的頭發(fā)上遇八,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天矛绘,我揣著相機(jī)與錄音,去河邊找鬼刃永。 笑死货矮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的斯够。 我是一名探鬼主播囚玫,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼读规!你這毒婦竟也來了劫灶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掖桦,失蹤者是張志新(化名)和其女友劉穎本昏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枪汪,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涌穆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了雀久。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宿稀。...
    茶點(diǎn)故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赖捌,靈堂內(nèi)的尸體忽然破棺而出祝沸,到底是詐尸還是另有隱情,我是刑警寧澤越庇,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布罩锐,位于F島的核電站,受9級特大地震影響卤唉,放射性物質(zhì)發(fā)生泄漏涩惑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一桑驱、第九天 我趴在偏房一處隱蔽的房頂上張望竭恬。 院中可真熱鬧,春花似錦熬的、人聲如沸痊硕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岔绸。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亭螟,已是汗流浹背挡鞍。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工骑歹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留预烙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓道媚,卻偏偏與公主長得像扁掸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子最域,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容