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

OpenZeppelin庫是基于Solidity 0.8.0版本進(jìn)行分析。(https://docs.openzeppelin.com/contracts/4.x/)
首先EnumerableSet是一種數(shù)據(jù)結(jié)構(gòu)菱父,用來存儲(chǔ)數(shù)據(jù)摊溶,方便增刪改查的操作沪猴。

在Solidity語言中璃吧,key-value形式的mapping數(shù)據(jù)類型也支持增刪改查嫡纠,但是它的刪除只會(huì)將value設(shè)置成類型對(duì)應(yīng)的默認(rèn)值递沪。所以,要實(shí)現(xiàn)存儲(chǔ)的值真正意義上的刪除沫勿,需要借助數(shù)組來實(shí)現(xiàn)挨约。使用數(shù)組還可以獲取數(shù)組長(zhǎng)度(_length函數(shù)),檢查元素是否存在(_contains函數(shù))产雹,便于查找(_at函數(shù))诫惭,這些mapping都無法單獨(dú)實(shí)現(xiàn)。

于是蔓挖,其中就定義了一個(gè)結(jié)構(gòu)體Set夕土。

struct Set {
        // Storage of set values
        bytes32[] _values;
        // Position of the value in the `values` array, plus 1 because index 0
        // means a value is not in the set.
        mapping(bytes32 => uint256) _indexes;
    }

_values變量是一個(gè)字節(jié)數(shù)組用來存儲(chǔ)值,_indexes是一個(gè)mapping類型瘟判,用來記錄值在數(shù)組中的位置怨绣。
_indexes變量的key,bytes32是我們需要存儲(chǔ)的值拷获;_indexes變量的value篮撑,uint256就是value在數(shù)組中的索引位置。

add函數(shù)

// 存儲(chǔ)值value在數(shù)組中的位置是否為0刀诬,為0表示數(shù)組中不存在咽扇,否則數(shù)組中存在該value
function _contains(Set storage set, bytes32 value) private view returns (bool) {
        return set._indexes[value] != 0;
}
// 新增 
function _add(Set storage set, bytes32 value) private returns (bool) {
        if (!_contains(set, value)) {
            set._values.push(value);
            // The value is stored at length-1, but we add 1 to all indexes
            // and use 0 as a sentinel value
            set._indexes[value] = set._values.length;
            return true;
        } else {
            return false;
        }
    }

在_add函數(shù)中邪财,傳遞byte32類型的value參數(shù)陕壹。先檢查數(shù)組是否存儲(chǔ)過該值,已經(jīng)存儲(chǔ)返回false树埠,否則進(jìn)行存儲(chǔ)糠馆,返回true。
這里存儲(chǔ)使用了數(shù)組的push函數(shù)怎憋,同時(shí)在mapping類型的_indexes中記錄當(dāng)前數(shù)組的索引位置又碌,這里給的是數(shù)組的長(zhǎng)度。存儲(chǔ)一個(gè)值绊袋,索引位置就是1毕匀,每新增一個(gè)值,索引位置會(huì)依次累加癌别。這樣就實(shí)現(xiàn)了新增功能皂岔。

remove

function _remove(Set storage set, bytes32 value) private returns (bool) {
        // We read and store the value's index to prevent multiple reads from the same storage slot
        uint256 valueIndex = set._indexes[value];

        if (valueIndex != 0) {
            // Equivalent to contains(set, value)
            // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
            // the array, and then remove the last element (sometimes called as 'swap and pop').
            // This modifies the order of the array, as noted in {at}.

            uint256 toDeleteIndex = valueIndex - 1;
            uint256 lastIndex = set._values.length - 1;

            if (lastIndex != toDeleteIndex) {
                bytes32 lastValue = set._values[lastIndex];

                // Move the last value to the index where the value to delete is
                set._values[toDeleteIndex] = lastValue;
                // Update the index for the moved value
                set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex
            }

            // Delete the slot where the moved value was stored
            set._values.pop();

            // Delete the index for the deleted slot
            delete set._indexes[value];

            return true;
        } else {
            return false;
        }
    }

_remove刪除函數(shù)的邏輯是,先找出要?jiǎng)h除value的索引位置展姐,這里的if判斷同樣是檢查數(shù)組中是否存在該值躁垛。

uint256 toDeleteIndex = valueIndex - 1;
uint256 lastIndex = set._values.length - 1;

toDeleteIndex和lastIndex的值為什么要減去1剖毯?

前面新增函數(shù)提到過,指定索引位置時(shí)數(shù)組的長(zhǎng)度教馆,由于數(shù)組的索引下標(biāo)是從0開始逊谋,所以這里需要減去1。

 bytes32 lastValue = set._values[lastIndex];
 // Move the last value to the index where the value to delete is
 set._values[toDeleteIndex] = lastValue;
 // Update the index for the moved value
 set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex

// 開始刪除
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the index for the deleted slot
delete set._indexes[value];

這里是將要?jiǎng)h除的value和數(shù)組最后一個(gè)元素交換位置土铺,使用數(shù)組的pop函數(shù)胶滋,移除最后一個(gè)元素,這樣就實(shí)現(xiàn)了需要?jiǎng)h除指定的value悲敷。最后镀钓,再使用delete指令把mapping中記錄的索引位置刪除,會(huì)自動(dòng)設(shè)置為0镀迂,所以前面的_contains函數(shù)要判斷不等于0丁溅。

通過 _add 函數(shù)可以看出,EnumerableSet的value具有唯一性探遵,不可重復(fù)窟赏。
通過 _remove 函數(shù)可以看出,EnumerableSet無法保證數(shù)據(jù)存儲(chǔ)的順序性箱季。

at函數(shù) 查找

function _at(Set storage set, uint256 index) private view returns (bytes32) {
        return set._values[index];
}

查找就很簡(jiǎn)單了涯穷,直接通過數(shù)組的 [ ] 方式來訪問。

length長(zhǎng)度

  function _length(Set storage set) private view returns (uint256) {
        return set._values.length;
  }

獲取EnumerableSet的長(zhǎng)度藏雏,其實(shí)就是數(shù)組的長(zhǎng)度拷况。
以上函數(shù)都是private修飾,只允許在EnumerableSet內(nèi)訪問掘殴。所以赚瘦,我們?cè)谑褂玫臅r(shí)候并不能訪問這些函數(shù),所以定義了三種類型的結(jié)構(gòu)體奏寨,Bytes32Set起意、AddressSet和UintSet共開發(fā)者使用;可以看出分別支持bytes32病瞳、address和uint三種基本類型揽咕。

// bytes32
struct Bytes32Set {
    Set _inner;
}
// address
struct AddressSet {
    Set _inner;
}
// uint
struct UintSet {
    Set _inner;
}

同時(shí)分別對(duì)這三種結(jié)構(gòu)體做了add、remove套菜、at亲善、contains、length逗柴、values函數(shù)的重載蛹头。這些函數(shù)都是用了internal修飾,而不是private。

Bytes32Set

function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _add(set._inner, value);
}
function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _remove(set._inner, value);
}
function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
        return _contains(set._inner, value);
}
function length(Bytes32Set storage set) internal view returns (uint256) {
        return _length(set._inner);
}
function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
        return _at(set._inner, index);
}
function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
        bytes32[] memory store = _values(set._inner);
        bytes32[] memory result;

        /// @solidity memory-safe-assembly
        assembly {
            result := store
        }

        return result;
}

add掘而、remove函數(shù)傳遞的參數(shù)是bytes32類型挟冠,Bytes32Set類型的變量set調(diào)用了_inner屬性;即set._inner袍睡。

set._inner其實(shí)就是一個(gè)Set結(jié)構(gòu)知染,傳遞到_add和_remove函數(shù)中,并且_add和_remove函數(shù)接收的value參數(shù)正好是byte32類型斑胜。所以控淡,Bytes32Set類型在函數(shù)調(diào)用處理value時(shí)不需要進(jìn)行類型轉(zhuǎn)換。

然而止潘,AddressSet和UintSet則不一樣掺炭,他們需要進(jìn)行類型轉(zhuǎn)換,將address和uint基本類型轉(zhuǎn)換成bytes32類型才能調(diào)用_add和_remove函數(shù)凭戴。

AddressSet

function add(AddressSet storage set, address value) internal returns (bool) {
        return _add(set._inner, bytes32(uint256(uint160(value))));
}
function remove(AddressSet storage set, address value) internal returns (bool) {
        return _remove(set._inner, bytes32(uint256(uint160(value))));
}
 function at(AddressSet storage set, uint256 index) internal view returns (address) {
        return address(uint160(uint256(_at(set._inner, index))));
 }

這里重點(diǎn)關(guān)注下類型轉(zhuǎn)換涧狮,將address類型轉(zhuǎn)換成byte32類型、將byte32類型轉(zhuǎn)換成address類型么夫。

address -> bytes32

add和remove函數(shù)在調(diào)用_add和_remove函數(shù)時(shí)者冤,需要傳遞bytes32類型,所以這里需要對(duì)address類型進(jìn)行轉(zhuǎn)換档痪。

  1. uint160(value):由于address類型的長(zhǎng)度是20個(gè)字節(jié)涉枫,也就是160位,所以先轉(zhuǎn)成了uint160腐螟。
  2. uint256(uint160(value)):由于bytes32是32字節(jié)愿汰,256位,所以用uint256做了一次轉(zhuǎn)換乐纸。
  3. bytes32(uint256(uint160(value))):最后將uint256類型轉(zhuǎn)換成bytes32類型衬廷。
byte32 ->address

觀察at函數(shù)的返回值 return address(uint160(uint256(_at(set._inner, index)))); 把bytes32轉(zhuǎn)換成address。

  1. _at(set._inner, index) 返回值是bytes32類型锯仪,256位泵督,所以需要uint256進(jìn)行轉(zhuǎn)換。
  2. uint256(_at(set._inner, index)) address類型20字節(jié)庶喜,160位,需要uint160進(jìn)行轉(zhuǎn)換救鲤。
  3. uint160(uint256(_at(set._inner, index))) uint160直接轉(zhuǎn)換成address類型即可久窟。
  4. address(uint160(uint256(_at(set._inner, index))))。

UintSet

function add(UintSet storage set, uint256 value) internal returns (bool) {
       return _add(set._inner, bytes32(value));
}
function remove(UintSet storage set, uint256 value) internal returns (bool) {
       return _remove(set._inner, bytes32(value));
}
function at(UintSet storage set, uint256 index) internal view returns (uint256) {
       return uint256(_at(set._inner, index));
}

UintSet同AddressSet一樣本缠,需要進(jìn)行類型轉(zhuǎn)換斥扛,不過轉(zhuǎn)換步驟要簡(jiǎn)單很多。

uint256 -> bytes32

add和remove函數(shù)接收參數(shù)是uint256類型,uint256和bytes32類型都是256位稀颁,所以直接轉(zhuǎn)換就行芬失,bytes32(value)

bytes32-> uint256

這里同理,直接轉(zhuǎn)換即可匾灶。

總結(jié)

  1. EnumerableSet具有元素?zé)o序性棱烂、唯一性。
  2. 類型不同時(shí)阶女,需要進(jìn)行類型轉(zhuǎn)換颊糜。
  3. 查找元素時(shí),沒有使用for循環(huán)秃踩,效率高衬鱼。

思考

  1. 為什么不實(shí)現(xiàn)一個(gè)修改的函數(shù)?
 // Move the last value to the index where the value to delete is
 set._values[toDeleteIndex] = lastValue;

從_remove函數(shù)的實(shí)現(xiàn)來看憔杨,要實(shí)現(xiàn)修改其實(shí)也是可以的鸟赫,通過傳參value查找mapping中記錄的索引位置,再通過數(shù)組訪問索引位置消别,重新賦值即可惯疙。
為什么源碼庫沒有實(shí)現(xiàn)這個(gè)修改操作呢?

  1. Solidity中有很多基礎(chǔ)數(shù)據(jù)類型妖啥,uint8霉颠、uint120、int16荆虱、bytes20等等蒿偎,而EnumerableSet僅僅支持bytes32、address和uint256三種基礎(chǔ)數(shù)據(jù)類型怀读。這些數(shù)據(jù)類型應(yīng)該如何使用EnumerableSet的一些函數(shù)呢诉位?
    也許通過類型轉(zhuǎn)換,轉(zhuǎn)換成EnumerableSet需要的類型就能滿足要求了菜枷。
  2. 從EnumerableSet名字可以看出苍糠,為什么是Set?是否與Java中Set相似啤誊?
    也許是因?yàn)閿?shù)據(jù)存儲(chǔ)的無序性岳瞭,不能保證數(shù)據(jù)存儲(chǔ)順序的一致性,所以命名為EnumerableSet蚊锹。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瞳筏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子牡昆,更是在濱河造成了極大的恐慌姚炕,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異柱宦,居然都是意外死亡些椒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門掸刊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來免糕,“玉大人,你說我怎么就攤上這事痒给∷的” “怎么了?”我有些...
    開封第一講書人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵苍柏,是天一觀的道長(zhǎng)尼斧。 經(jīng)常有香客問我,道長(zhǎng)试吁,這世上最難降的妖魔是什么棺棵? 我笑而不...
    開封第一講書人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮熄捍,結(jié)果婚禮上烛恤,老公的妹妹穿的比我還像新娘。我一直安慰自己余耽,他們只是感情好缚柏,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碟贾,像睡著了一般币喧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上袱耽,一...
    開封第一講書人閱讀 49,792評(píng)論 1 290
  • 那天杀餐,我揣著相機(jī)與錄音,去河邊找鬼朱巨。 笑死史翘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的冀续。 我是一名探鬼主播琼讽,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼沥阳!你這毒婦竟也來了跨琳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤桐罕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溅潜,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年薪伏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滚澜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塘淑。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡槐沼,死狀恐怖兼吓,靈堂內(nèi)的尸體忽然破棺而出视搏,到底是詐尸還是另有隱情棚愤,我是刑警寧澤宛畦,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布养距,位于F島的核電站棍厌,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏毕荐。R本人自食惡果不足惜畸陡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望庸毫。 院中可真熱鬧载佳,春花似錦、人聲如沸刁俭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽如孝。三九已至宪哩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間第晰,已是汗流浹背锁孟。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茁瘦,地道東北人品抽。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像甜熔,于是被迫代替她去往敵國(guó)和親圆恤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

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