如何在 Solidity 中對數(shù)組進行去重

一沟绪、引言

Solidity 是一種面向以太坊平臺的智能合約編程語言割卖,具有類似 JavaScript 和 C++ 的語法結構疙驾。它是專門為在區(qū)塊鏈上編寫自執(zhí)行合約而設計的杠河,支持復雜的業(yè)務邏輯和去中心化應用(dApps)的開發(fā)氢架。隨著區(qū)塊鏈技術的快速發(fā)展傻咖,Solidity 已成為構建去中心化金融(DeFi)、NFT 市場以及其他區(qū)塊鏈應用的首選語言(其實主要是以太坊用戶多罷了)岖研。

在區(qū)塊鏈開發(fā)中卿操,處理數(shù)據(jù)的效率至關重要,特別是在智能合約中孙援,數(shù)組的高效操作往往決定了合約的性能和 gas 成本害淤。由于以太坊網(wǎng)絡上的每一筆交易都會產(chǎn)生費用,減少不必要的計算和存儲操作變得尤為關鍵拓售。對數(shù)組進行去重就是這樣一種常見的數(shù)據(jù)操作需求:我們可能需要從一個用戶列表中移除重復地址窥摄,或從一個交易列表中提取唯一的交易 ID。這些操作不僅涉及數(shù)據(jù)的正確性础淤,還直接影響到合約的執(zhí)行成本崭放。

那么哨苛,在 Solidity 中,如何高效地對數(shù)組進行去重币砂?這是一個值得深入探討的話題建峭。本文將介紹幾種常見的去重方法,并分析它們的優(yōu)缺點决摧,幫助你在實際開發(fā)中選擇最合適的策略亿蒸。

二磷雇、Solidity 中的數(shù)組操作基礎

在 Solidity 中漾岳,數(shù)組是最常用的數(shù)據(jù)結構之一,允許開發(fā)者存儲和操作一系列相同類型的元素坠敷。根據(jù)數(shù)組的長度是否固定拘鞋,Solidity 中的數(shù)組可以分為靜態(tài)數(shù)組動態(tài)數(shù)組砚蓬。

2.1 Solidity 中數(shù)組的基本使用方法

在 Solidity 中,定義和使用數(shù)組的方法非常直觀盆色。數(shù)組類型由元素類型和方括號組成灰蛙,例如 uint256[] 表示一個動態(tài)數(shù)組,uint256[5] 表示一個包含 5 個 uint256 元素的靜態(tài)數(shù)組隔躲。

以下是一些基本的數(shù)組操作示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ArrayExample {
    // 動態(tài)數(shù)組
    uint256[] public dynamicArray;

    // 靜態(tài)數(shù)組
    uint256[5] public staticArray;

    // 添加元素到動態(tài)數(shù)組
    function addElement(uint256 element) public {
        dynamicArray.push(element);
    }

    // 修改靜態(tài)數(shù)組中的元素
    function setElement(uint256 index, uint256 element) public {
        require(index < staticArray.length, "Index out of bounds");
        staticArray[index] = element;
    }

    // 獲取動態(tài)數(shù)組的長度
    function getDynamicArrayLength() public view returns (uint256) {
        return dynamicArray.length;
    }
}

2.2 動態(tài)數(shù)組與靜態(tài)數(shù)組的區(qū)別

  1. 靜態(tài)數(shù)組:靜態(tài)數(shù)組的長度在編譯時已確定摩梧,并且在合約生命周期中無法更改。使用靜態(tài)數(shù)組的優(yōu)點是它們的存儲和操作成本相對較低宣旱,因為它們不需要動態(tài)調(diào)整大小仅父。靜態(tài)數(shù)組常用于合約中需要處理固定數(shù)量數(shù)據(jù)的場景,例如固定數(shù)量的參與者或預定義的常量值浑吟。

    // 定義一個包含 3 個元素的靜態(tài)數(shù)組
    uint256[3] public staticArray = [1, 2, 3];
    
  2. 動態(tài)數(shù)組:動態(tài)數(shù)組的長度在合約的生命周期內(nèi)是可變的笙纤,開發(fā)者可以使用 push 方法向數(shù)組中添加元素。雖然動態(tài)數(shù)組提供了靈活性组力,但它們也帶來了更高的 gas 成本省容,尤其是在添加和刪除元素時。動態(tài)數(shù)組適用于需要處理可變數(shù)量數(shù)據(jù)的場景燎字,例如用戶地址列表或交易記錄等腥椒。

    // 定義一個動態(tài)數(shù)組
    uint256[] public dynamicArray;
    
    // 向動態(tài)數(shù)組中添加元素
    dynamicArray.push(4);
    

2.3 數(shù)組操作的 gas 成本及其在智能合約中的重要性

在智能合約中,每次數(shù)組操作都會消耗一定的 gas候衍,這是因為操作涉及對以太坊虛擬機(EVM)中存儲的讀取和寫入笼蛛。尤其是在以太坊主網(wǎng)上,gas 成本直接影響到交易費用蛉鹿,因此對數(shù)組的操作效率顯得尤為重要伐弹。

  • 讀操作:在數(shù)組中讀取數(shù)據(jù)的 gas 成本相對較低,通常只需要訪問存儲器。
  • 寫操作:寫操作的 gas 成本較高惨好,因為它涉及到將數(shù)據(jù)寫入以太坊的持久化存儲煌茴。
  • 動態(tài)調(diào)整大小:對于動態(tài)數(shù)組,每次 push 操作不僅需要寫入新元素日川,還可能涉及數(shù)組大小調(diào)整的操作蔓腐,這會增加額外的 gas 成本。

優(yōu)化數(shù)組操作是 Solidity 開發(fā)中的一個關鍵點龄句。為了減少不必要的 gas 消耗回论,開發(fā)者通常會在合約邏輯中慎重考慮數(shù)組的使用方式和操作方法。例如分歇,盡量避免在循環(huán)中進行多次寫操作傀蓉,或者在不必要的情況下使用動態(tài)數(shù)組。

總之职抡,理解數(shù)組的基本操作及其 gas 成本葬燎,可以幫助開發(fā)者編寫更高效的智能合約,避免不必要的資源浪費缚甩,提升合約的整體性能谱净。

三、Solidity 中的去重挑戰(zhàn)

在智能合約開發(fā)中擅威,Solidity 的局限性往往會影響開發(fā)者實現(xiàn)特定功能的方式壕探。一個顯著的限制是,Solidity 不直接支持像 JavaScript 中的 Set 這樣的動態(tài)數(shù)據(jù)結構郊丛。這使得在 Solidity 中處理集合操作(如去重)變得更加復雜和昂貴李请。

由于 Solidity 的局限性和區(qū)塊鏈環(huán)境的特性,開發(fā)者必須在實現(xiàn)去重的同時厉熟,盡可能減少存儲和計算操作导盅,以節(jié)省 gas 和降低存儲成本。這需要精心設計合約邏輯庆猫,選擇最合適的數(shù)據(jù)結構和算法來優(yōu)化性能和成本。在智能合約開發(fā)中绅络,這種權衡和優(yōu)化是不可避免的月培,也是開發(fā)者需要不斷學習和改進的關鍵點。

3.1 Solidity 的局限性

  1. 缺乏高級數(shù)據(jù)結構:Solidity 目前只支持基本的數(shù)據(jù)結構恩急,如數(shù)組和映射(mapping)杉畜。這些數(shù)據(jù)結構雖然足以滿足許多簡單需求,但在處理更復雜的數(shù)據(jù)操作時衷恭,如自動去重或排序此叠,它們顯得力不從心。與 JavaScript 不同随珠,Solidity 沒有原生的 Set 類型灭袁,這意味著沒有直接的方式來存儲唯一值猬错。
  2. 存儲操作成本高:Solidity 中的任何寫入操作,特別是涉及到永久存儲(storage)時茸歧,都會消耗大量的 gas倦炒。存儲的數(shù)據(jù)越多,操作越復雜软瞎,消耗的 gas 就越高逢唤。因此,構建一個復雜的數(shù)據(jù)結構或進行多次數(shù)據(jù)寫入操作涤浇,會顯著增加合約的部署和執(zhí)行成本鳖藕。
  3. 沒有原生的集合操作:Solidity 缺乏對集合操作的原生支持。像在 JavaScript 中使用 Setadd 方法自動去重只锭,或使用 has 方法快速查找元素著恩,這些在 Solidity 中都需要手動實現(xiàn)。通常纹烹,這需要編寫額外的邏輯和循環(huán)页滚,進一步增加了合約的復雜性和執(zhí)行成本。

3.2 在 Solidity 中實現(xiàn)去重的難度

在 Solidity 中去重的主要難點在于如何在保證數(shù)據(jù)唯一性的同時控制 gas 成本铺呵。以下是實現(xiàn)去重的一些挑戰(zhàn):

  1. 高昂的 gas 成本:為了實現(xiàn)去重裹驰,開發(fā)者需要遍歷數(shù)組中的所有元素,并且通常需要在遍歷過程中檢查每個元素是否已經(jīng)存在片挂。使用映射(mapping)或集合(set)來跟蹤已經(jīng)見過的元素是一種常見的解決方案幻林,但每次訪問和修改映射都會消耗 gas,尤其是在數(shù)據(jù)量較大時音念,成本可能會非常高沪饺。
  2. 存儲空間的浪費:為實現(xiàn)去重,我們需要額外的數(shù)據(jù)結構來跟蹤已經(jīng)處理的元素闷愤。例如整葡,使用映射來記錄一個元素是否已出現(xiàn)過,雖然這種方式可以使查找操作的時間復雜度為 O(1)讥脐,但是映射本身需要額外的存儲空間遭居,這會增加合約的總體存儲成本。更糟的是旬渠,存儲在區(qū)塊鏈上的數(shù)據(jù)是永久存在的俱萍,這意味著這些額外的存儲消耗將會是長期的。
  3. Gas Limit 約束:以太坊網(wǎng)絡對單個交易執(zhí)行的 gas 數(shù)量有上限(即 gas limit)告丢。如果一個合約函數(shù)在執(zhí)行時消耗的 gas 超過了這個限制枪蘑,交易將被回滾,合約不會執(zhí)行成功。去重操作的復雜性可能導致 gas 消耗迅速增加岳颇,特別是在處理大型數(shù)組或在復雜邏輯中嵌套多次去重操作時照捡。

四、方法一:使用集合(或映射)進行去重

下面是一個使用 openzepplin 的 EnumerableSet 庫來快速去重空投地址的智能合約示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// version >= v3.3.0
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

contract AirdropUniqueAddresses {
    // Using the EnumerableSet library for AddressSet
    using EnumerableSet for EnumerableSet.AddressSet;

    // Declare a state variable of type EnumerableSet.AddressSet to store unique addresses
    EnumerableSet.AddressSet private seen;

    // Function to add an address to the set if it is not already present
    function addAirdropAddress(address _address) public returns (bool) {
        // Add the address to the set
        // Returns true if the address was not already in the set, false otherwise
        return seen.add(_address);
    }

    // Function to check if an address is already in the set
    function isAirdropAddressSeen(address _address) public view returns (bool) {
        // Check if the address is in the set
        return seen.contains(_address);
    }

    // Function to get the total number of unique addresses in the set
    function getTotalUniqueAddresses() public view returns (uint256) {
        // Return the number of unique addresses in the set
        return seen.length();
    }

    // Function to retrieve an address by index in the set
    function getAirdropAddressAtIndex(uint256 index) public view returns (address) {
        // Ensure the index is within the bounds of the set
        require(index < seen.length(), "Index out of bounds");
        // Retrieve the address at the specified index
        return seen.at(index);
    }

    // Function to remove an address from the set
    function removeAirdropAddress(address _address) public returns (bool) {
        // Remove the address from the set
        // Returns true if the address was in the set and removed, false otherwise
        return seen.remove(_address);
    }
}
  • 優(yōu)點
    • 高效性:映射在 Solidity 中提供了 O(1) 的查找時間赦役。
    • 易于實現(xiàn):邏輯簡單麻敌,易于理解。
  • 缺點
    • 需要額外的存儲空間掂摔,可能會增加 gas 成本术羔。
    • 不能動態(tài)創(chuàng)建映射,需要預先定義數(shù)據(jù)結構:類似這種代碼在編譯中會報錯Uninitialized mapping. Mappings cannot be created dynamically, you have to assign them from a state variable.
mapping(uint256 => bool) memory seen;
uint256[] memory input = [1, 2, 2, 3, 4, 4];
for (uint256 i = 0; i < input.length; i++) {
    if (!seen[input[i]]) {
        seen[input[i]] = true;
    }
}

五乙漓、方法二:雙重循環(huán)去重

以下是一個使用雙重循環(huán)來去重空投地址的智能合約示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AirdropUniqueAddresses {
    // Function to get the unique addresses after removing duplicates
    function getUniqueAirdropAddresses(address[] airdropAddresses;) public view returns (address[] memory) {
        // Calculate the length of the airdropAddresses array
        uint256 length = airdropAddresses.length;

        // Create a dynamic array to store unique addresses
        address[] memory uniqueAddresses = new address[](length);
        uint256 uniqueCount = 0;

        // Outer loop to iterate through each address in the airdropAddresses array
        for (uint256 i = 0; i < length; i++) {
            bool isDuplicate = false;

            // Inner loop to check if the address is already in the uniqueAddresses array
            for (uint256 j = 0; j < uniqueCount; j++) {
                if (airdropAddresses[i] == uniqueAddresses[j]) {
                    isDuplicate = true;
                    break;
                }
            }

            // If the address is not a duplicate, add it to the uniqueAddresses array
            if (!isDuplicate) {
                uniqueAddresses[uniqueCount] = airdropAddresses[i];
                uniqueCount++;
            }
        }

        // Create a new array with the size of uniqueCount to store the final unique addresses
        address[] memory finalUniqueAddresses = new address[](uniqueCount);

        // Copy the unique addresses to the final array
        for (uint256 k = 0; k < uniqueCount; k++) {
            finalUniqueAddresses[k] = uniqueAddresses[k];
        }

        return finalUniqueAddresses;
    }
}
  • 優(yōu)點
    • 不需要額外的存儲結構级历,非常節(jié)省空間。
    • 適合小規(guī)模數(shù)據(jù)叭披,簡潔易懂寥殖。
  • 缺點
    • 時間復雜度為 O(n^2),對于大數(shù)據(jù)集不太適用涩蜘。
    • 可能導致高 gas 消耗嚼贡,不建議在生產(chǎn)環(huán)境中使用。

六同诫、參考資料

  1. https://docs.soliditylang.org/zh/v0.8.21/types.html
  2. https://docs.openzeppelin.com/contracts/3.x/api/utils#EnumerableSet
  3. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粤策,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子误窖,更是在濱河造成了極大的恐慌叮盘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霹俺,死亡現(xiàn)場離奇詭異柔吼,居然都是意外死亡,警方通過查閱死者的電腦和手機丙唧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門愈魏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人想际,你說我怎么就攤上這事培漏。” “怎么了沼琉?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵北苟,是天一觀的道長桩匪。 經(jīng)常有香客問我打瘪,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任闺骚,我火速辦了婚禮彩扔,結果婚禮上,老公的妹妹穿的比我還像新娘僻爽。我一直安慰自己虫碉,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布胸梆。 她就那樣靜靜地躺著敦捧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碰镜。 梳的紋絲不亂的頭發(fā)上兢卵,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音绪颖,去河邊找鬼秽荤。 笑死,一個胖子當著我的面吹牛柠横,可吹牛的內(nèi)容都是我干的窃款。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼牍氛,長吁一口氣:“原來是場噩夢啊……” “哼晨继!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起糜俗,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤踱稍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悠抹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珠月,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年楔敌,在試婚紗的時候發(fā)現(xiàn)自己被綠了啤挎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡卵凑,死狀恐怖庆聘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情勺卢,我是刑警寧澤伙判,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站黑忱,受9級特大地震影響宴抚,放射性物質(zhì)發(fā)生泄漏勒魔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一菇曲、第九天 我趴在偏房一處隱蔽的房頂上張望冠绢。 院中可真熱鬧,春花似錦常潮、人聲如沸弟胀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孵户。三九已至,卻和暖如春岔留,著一層夾襖步出監(jiān)牢的瞬間延届,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工贸诚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留方庭,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓酱固,卻偏偏與公主長得像械念,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子运悲,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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