以太坊Solidity開(kāi)發(fā)入門(高級(jí)篇)

上一篇:以太坊Solidity開(kāi)發(fā)入門(進(jìn)階篇)

第1章: 智能協(xié)議的永固性

到現(xiàn)在為止,我們講的 Solidity 和其他語(yǔ)言沒(méi)有質(zhì)的區(qū)別扶平,它長(zhǎng)得也很像 JavaScript.

但是卑雁,在有幾點(diǎn)以太坊上的 DApp 跟普通的應(yīng)用程序有著天壤之別。

第一個(gè)例子,在你把智能協(xié)議傳上以太坊之后,它就變得不可更改, 這種永固性意味著你的代碼永遠(yuǎn)不能被調(diào)整或更新藕溅。

你編譯的程序會(huì)一直,永久的熄守,不可更改的蜈垮,存在以太網(wǎng)上。這就是Solidity代碼的安全性如此重要的一個(gè)原因裕照。如果你的智能協(xié)議有任何漏洞攒发,即使你發(fā)現(xiàn)了也無(wú)法補(bǔ)救。你只能讓你的用戶們放棄這個(gè)智能協(xié)議晋南,然后轉(zhuǎn)移到一個(gè)新的修復(fù)后的合約上惠猿。

但這恰好也是智能合約的一大優(yōu)勢(shì)。 代碼說(shuō)明一切负间。 如果你去讀智能合約的代碼偶妖,并驗(yàn)證它,你會(huì)發(fā)現(xiàn)政溃, 一旦函數(shù)被定義下來(lái)趾访,每一次的運(yùn)行,程序都會(huì)嚴(yán)格遵照函數(shù)中原有的代碼邏輯一絲不茍地執(zhí)行董虱,完全不用擔(dān)心函數(shù)被人篡改而得到意外的結(jié)果扼鞋。

外部依賴關(guān)系

在第2課中,我們將加密小貓(CryptoKitties)合約的地址硬編碼到DApp中去了愤诱。有沒(méi)有想過(guò)普气,如果加密小貓出了點(diǎn)問(wèn)題以舒,比方說(shuō)蔓纠,集體消失了會(huì)怎么樣帅矗? 雖然這種事情幾乎不可能發(fā)生,但是科吭,如果小貓沒(méi)了昏滴,我們的 DApp 也會(huì)隨之失效 -- 因?yàn)槲覀冊(cè)?DApp 的代碼中用“硬編碼”的方式指定了加密小貓的地址,如果這個(gè)根據(jù)地址找不到小貓对人,我們的僵尸也就吃不到小貓了影涉,而按照前面的描述,我們卻沒(méi)法修改合約去應(yīng)付這個(gè)變化规伐!

因此蟹倾,我們不能硬編碼,而要采用“函數(shù)”,以便于 DApp 的關(guān)鍵部分可以以參數(shù)形式修改鲜棠。

比方說(shuō)肌厨,我們不再一開(kāi)始就把獵物地址給寫入代碼,而是寫個(gè)函數(shù) setKittyContractAddress, 運(yùn)行時(shí)再設(shè)定獵物的地址豁陆,這樣我們就可以隨時(shí)去鎖定新的獵物柑爸,也不用擔(dān)心加密小貓集體消失了。

實(shí)戰(zhàn)演習(xí)
請(qǐng)修改第2課的代碼盒音,使得可以通過(guò)程序更改CryptoKitties合約地址表鳍。

  1. 刪除采用硬編碼 方式的 ckAddress 代碼行。

  2. 之前創(chuàng)建 kittyContract 變量的那行代碼祥诽,修改為對(duì) kittyContract 變量的聲明 -- 暫時(shí)不給它指定具體的實(shí)例譬圣。

  3. 創(chuàng)建名為 setKittyContractAddress 的函數(shù), 它帶一個(gè)參數(shù) _addressaddress類型)雄坪, 可見(jiàn)性設(shè)為external厘熟。

  4. 在函數(shù)內(nèi)部,添加一行代碼维哈,將 kittyContract 變量設(shè)置為返回值:KittyInterface(_address)绳姨。

注意:你可能會(huì)注意到這個(gè)功能有個(gè)安全漏洞,別擔(dān)心 - 咱們到下一章里解決它;)

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

  function setKittyContractAddress(address _address) external{
    kittyContract = KittyInterface(_address);
  }

  function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}


第2章: Ownable Contracts

上一章中阔挠,您有沒(méi)有發(fā)現(xiàn)任何安全漏洞呢飘庄?

呀!setKittyContractAddress 可見(jiàn)性居然申明為“外部的”(external)购撼,豈不是任何人都可以調(diào)用它跪削! 也就是說(shuō),任何調(diào)用該函數(shù)的人都可以更改 CryptoKitties 合約的地址份招,使得其他人都沒(méi)法再運(yùn)行我們的程序了切揭。

我們確實(shí)是希望這個(gè)地址能夠在合約中修改狞甚,但我可沒(méi)說(shuō)讓每個(gè)人去改它呀锁摔。

要對(duì)付這樣的情況,通常的做法是指定合約的“所有權(quán)” - 就是說(shuō)哼审,給它指定一個(gè)主人(沒(méi)錯(cuò)谐腰,就是您),只有主人對(duì)它享有特權(quán)涩盾。

OpenZeppelin庫(kù)的Ownable 合約

下面是一個(gè) Ownable 合約的例子: 來(lái)自 OpenZeppelin Solidity 庫(kù)的 Ownable 合約十气。 OpenZeppelin 是主打安保和社區(qū)審查的智能合約庫(kù),您可以在自己的 DApps中引用春霍。等把這一課學(xué)完砸西,您不要催我們發(fā)布下一課,最好利用這個(gè)時(shí)間把 OpenZeppelin 的網(wǎng)站看看,保管您會(huì)學(xué)到很多東西芹枷!

把樓下這個(gè)合約讀讀通衅疙,是不是還有些沒(méi)見(jiàn)過(guò)代碼?別擔(dān)心鸳慈,我們隨后會(huì)解釋饱溢。

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }
}

下面有沒(méi)有您沒(méi)學(xué)過(guò)的東東?

  • 構(gòu)造函數(shù):function Ownable()是一個(gè) constructor (構(gòu)造函數(shù))走芋,構(gòu)造函數(shù)不是必須的绩郎,它與合約同名,構(gòu)造函數(shù)一生中唯一的一次執(zhí)行翁逞,就是在合約最初被創(chuàng)建的時(shí)候肋杖。

  • 函數(shù)修飾符:modifier onlyOwner()。 修飾符跟函數(shù)很類似熄攘,不過(guò)是用來(lái)修飾其他已有函數(shù)用的兽愤, 在其他語(yǔ)句執(zhí)行前,為它檢查下先驗(yàn)條件挪圾。 在這個(gè)例子中浅萧,我們就可以寫個(gè)修飾符 onlyOwner 檢查下調(diào)用者,確保只有合約的主人才能運(yùn)行本函數(shù)哲思。我們下一章中會(huì)詳細(xì)講述修飾符洼畅,以及那個(gè)奇怪的_;

  • indexed 關(guān)鍵字:別擔(dān)心棚赔,我們還用不到它帝簇。

所以Ownable 合約基本都會(huì)這么干:

  1. 合約創(chuàng)建,構(gòu)造函數(shù)先行靠益,將其 owner 設(shè)置為msg.sender(其部署者)

  2. 為它加上一個(gè)修飾符 onlyOwner丧肴,它會(huì)限制陌生人的訪問(wèn),將訪問(wèn)某些函數(shù)的權(quán)限鎖定在 owner 上胧后。

  3. 允許將合約所有權(quán)轉(zhuǎn)讓給他人芋浮。

onlyOwner 簡(jiǎn)直人見(jiàn)人愛(ài),大多數(shù)人開(kāi)發(fā)自己的 Solidity DApps壳快,都是從復(fù)制/粘貼 Ownable 開(kāi)始的纸巷,從它再繼承出的子類,并在之上進(jìn)行功能開(kāi)發(fā)眶痰。

既然我們想把 setKittyContractAddress 限制為 onlyOwner 瘤旨,我們也要做同樣的事情。

實(shí)戰(zhàn)演習(xí)

首先竖伯,將 Ownable 合約的代碼復(fù)制一份到新文件 ownable.sol 中存哲。 接下來(lái)因宇,創(chuàng)建一個(gè) ZombieFactory,繼承 Ownable祟偷。

1.在程序中導(dǎo)入 ownable.sol 的內(nèi)容羽嫡。 如果您不記得怎么做了,參考下 zombiefeeding.sol肩袍。

2.修改 ZombieFactory 合約杭棵, 讓它繼承自 Ownable。 如果您不記得怎么做了氛赐,看看 zombiefeeding.sol魂爪。

pragma solidity ^0.4.19;

import "./ownable.sol";

contract ZombieFactory is Ownable{

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

第3章: onlyOwner 函數(shù)修飾符

現(xiàn)在我們有了個(gè)基本版的合約 ZombieFactory 了,它繼承自 Ownable 接口艰管,我們也可以給 ZombieFeeding 加上 onlyOwner 函數(shù)修飾符滓侍。

這就是合約繼承的工作原理。記得:

ZombieFeeding 是個(gè) ZombieFactory
ZombieFactory 是個(gè) Ownable

因此 ZombieFeeding 也是個(gè) Ownable, 并可以通過(guò) Ownable 接口訪問(wèn)父類中的函數(shù)/事件/修飾符牲芋。往后撩笆,ZombieFeeding 的繼承者合約們同樣也可以這么延續(xù)下去。

函數(shù)修飾符

函數(shù)修飾符看起來(lái)跟函數(shù)沒(méi)什么不同缸浦,不過(guò)關(guān)鍵字modifier 告訴編譯器夕冲,這是個(gè)modifier(修飾符),而不是個(gè)function(函數(shù))裂逐。它不能像函數(shù)那樣被直接調(diào)用歹鱼,只能被添加到函數(shù)定義的末尾,用以改變函數(shù)的行為卜高。

咱們仔細(xì)讀讀 onlyOwner:

/**
 * @dev 調(diào)用者不是‘主人’弥姻,就會(huì)拋出異常
 */
modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}
onlyOwner 函數(shù)修飾符是這么用的:

contract MyContract is Ownable {
  event LaughManiacally(string laughter);

  //注意! `onlyOwner`上場(chǎng) :
  function likeABoss() external onlyOwner {
    LaughManiacally("Muahahahaha");
  }
}

注意 likeABoss 函數(shù)上的 onlyOwner 修飾符掺涛。 當(dāng)你調(diào)用 likeABoss 時(shí)庭敦,首先執(zhí)行 onlyOwner 中的代碼, 執(zhí)行到 onlyOwner 中的 _; 語(yǔ)句時(shí)薪缆,程序再返回并執(zhí)行 likeABoss 中的代碼秧廉。

可見(jiàn),盡管函數(shù)修飾符也可以應(yīng)用到各種場(chǎng)合矮燎,但最常見(jiàn)的還是放在函數(shù)執(zhí)行之前添加快速的 require檢查定血。

因?yàn)榻o函數(shù)添加了修飾符 onlyOwner赔癌,使得唯有合約的主人(也就是部署者)才能調(diào)用它诞外。

注意:主人對(duì)合約享有的特權(quán)當(dāng)然是正當(dāng)?shù)模贿^(guò)也可能被惡意使用灾票。比如峡谊,萬(wàn)一,主人添加了個(gè)后門,允許他偷走別人的僵尸呢既们?
所以非常重要的是濒析,部署在以太坊上的 DApp,并不能保證它真正做到去中心啥纸,你需要閱讀并理解它的源代碼号杏,才能防止其中沒(méi)有被部署者惡意植入后門;作為開(kāi)發(fā)人員斯棒,如何做到既要給自己留下修復(fù) bug 的余地盾致,又要盡量地放權(quán)給使用者,以便讓他們放心你荣暮,從而愿意把數(shù)據(jù)放在你的 DApp 中庭惜,這確實(shí)需要個(gè)微妙的平衡。

實(shí)戰(zhàn)演習(xí)

現(xiàn)在我們可以限制第三方對(duì) setKittyContractAddress的訪問(wèn)穗酥,除了我們自己护赊,誰(shuí)都無(wú)法去修改它。

  1. 將 onlyOwner 函數(shù)修飾符添加到 setKittyContractAddress 中砾跃。
pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }

  function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}

第4章: Gas

厲害骏啰!現(xiàn)在我們懂了如何在禁止第三方修改我們的合約的同時(shí),留個(gè)后門給咱們自己去修改抽高。

讓我們來(lái)看另一種使得 Solidity 編程語(yǔ)言與眾不同的特征:

Gas - 驅(qū)動(dòng)以太坊DApps的能源

在 Solidity 中器一,你的用戶想要每次執(zhí)行你的 DApp 都需要支付一定的 gas,gas 可以用以太幣購(gòu)買厨内,因此祈秕,用戶每次跑 DApp 都得花費(fèi)以太幣。

一個(gè) DApp 收取多少 gas 取決于功能邏輯的復(fù)雜程度雏胃。每個(gè)操作背后请毛,都在計(jì)算完成這個(gè)操作所需要的計(jì)算資源,(比如瞭亮,存儲(chǔ)數(shù)據(jù)就比做個(gè)加法運(yùn)算貴得多)方仿, 一次操作所需要花費(fèi)的 ##gas## 等于這個(gè)操作背后的所有運(yùn)算花銷的總和。

由于運(yùn)行你的程序需要花費(fèi)用戶的真金白銀统翩,在以太坊中代碼的編程語(yǔ)言仙蚜,比其他任何編程語(yǔ)言都更強(qiáng)調(diào)優(yōu)化。同樣的功能厂汗,使用笨拙的代碼開(kāi)發(fā)的程序委粉,比起經(jīng)過(guò)精巧優(yōu)化的代碼來(lái),運(yùn)行花費(fèi)更高娶桦,這顯然會(huì)給成千上萬(wàn)的用戶帶來(lái)大量不必要的開(kāi)銷贾节。

為什么要用 gas 來(lái)驅(qū)動(dòng)汁汗?

以太坊就像一個(gè)巨大、緩慢栗涂、但非常安全的電腦知牌。當(dāng)你運(yùn)行一個(gè)程序的時(shí)候,網(wǎng)絡(luò)上的每一個(gè)節(jié)點(diǎn)都在進(jìn)行相同的運(yùn)算斤程,以驗(yàn)證它的輸出 —— 這就是所謂的”去中心化“ 由于數(shù)以千計(jì)的節(jié)點(diǎn)同時(shí)在驗(yàn)證著每個(gè)功能的運(yùn)行角寸,這可以確保它的數(shù)據(jù)不會(huì)被被監(jiān)控,或者被刻意修改忿墅。

可能會(huì)有用戶用無(wú)限循環(huán)堵塞網(wǎng)絡(luò)袭厂,抑或用密集運(yùn)算來(lái)占用大量的網(wǎng)絡(luò)資源,為了防止這種事情的發(fā)生球匕,以太坊的創(chuàng)建者為以太坊上的資源制定了價(jià)格纹磺,想要在以太坊上運(yùn)算或者存儲(chǔ),你需要先付費(fèi)亮曹。

注意:如果你使用側(cè)鏈橄杨,倒是不一定需要付費(fèi),比如咱們?cè)?Loom Network 上構(gòu)建的 CryptoZombies 就免費(fèi)照卦。你不會(huì)想要在以太坊主網(wǎng)上玩兒“魔獸世界”吧式矫? - 所需要的 gas 可能會(huì)買到你破產(chǎn)。但是你可以找個(gè)算法理念不同的側(cè)鏈來(lái)玩它役耕。我們將在以后的課程中咱們會(huì)討論到采转,什么樣的 DApp 應(yīng)該部署在太坊主鏈上,什么又最好放在側(cè)鏈瞬痘。

省 gas 的招數(shù):結(jié)構(gòu)封裝 (Struct packing)

在第1課中故慈,我們提到除了基本版的 uint 外,還有其他變種 uintuint8框全,uint16察绷,uint32等。

通常情況下我們不會(huì)考慮使用 uint 變種津辩,因?yàn)闊o(wú)論如何定義 uint的大小拆撼,Solidity 為它保留256位的存儲(chǔ)空間。例如喘沿,使用 uint8 而不是uintuint256)不會(huì)為你節(jié)省任何 gas闸度。

除非,把 uint 綁定到 struct 里面蚜印。

如果一個(gè) struct 中有多個(gè) uint莺禁,則盡可能使用較小的 uint, Solidity 會(huì)將這些 uint 打包在一起,從而占用較少的存儲(chǔ)空間晒哄。例如:

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// 因?yàn)槭褂昧私Y(jié)構(gòu)打包睁宰,`mini` 比 `normal` 占用的空間更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);

所以,當(dāng) uint 定義在一個(gè) struct 中的時(shí)候寝凌,盡量使用最小的整數(shù)子類型以節(jié)約空間柒傻。 并且把同樣類型的變量放一起(即在 struct 中將把變量按照類型依次放置),這樣 Solidity 可以將存儲(chǔ)空間最小化较木。例如红符,有兩個(gè) struct:

uint c; uint32 a; uint32 b; 和 uint32 a; uint c; uint32 b;

前者比后者需要的gas更少,因?yàn)榍罢甙?code>uint32放一起了伐债。

實(shí)戰(zhàn)演習(xí)

在本課中预侯,咱們給僵尸添2個(gè)新功能:le??velreadyTime - 后者是用來(lái)實(shí)現(xiàn)一個(gè)“冷卻定時(shí)器”,以限制僵尸獵食的頻率峰锁。

讓我們回到 zombiefactory.sol萎馅。

  1. 為 Zombie 結(jié)構(gòu)體 添加兩個(gè)屬性:level(uint32)和readyTime(uint32)。因?yàn)橄M愋蛿?shù)據(jù)打成一個(gè)包虹蒋,所以把它們放在結(jié)構(gòu)體的末尾糜芳。

32位足以保存僵尸的級(jí)別和時(shí)間戳了,這樣比起使用普通的uint(256位)魄衅,可以更緊密地封裝數(shù)據(jù)峭竣,從而為我們省點(diǎn) gas。

pragma solidity ^0.4.19;

import "./ownable.sol";

contract ZombieFactory is Ownable {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
        uint32 level;
        uint32 readyTime;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

第5章: 時(shí)間單位

level 屬性表示僵尸的級(jí)別晃虫。以后皆撩,在我們創(chuàng)建的戰(zhàn)斗系統(tǒng)中,打勝仗的僵尸會(huì)逐漸升級(jí)并獲得更多的能力哲银。

readyTime 稍微復(fù)雜點(diǎn)扛吞。我們希望增加一個(gè)“冷卻周期”,表示僵尸在兩次獵食或攻擊之之間必須等待的時(shí)間荆责。如果沒(méi)有它喻粹,僵尸每天可能會(huì)攻擊和繁殖1,000次,這樣游戲就太簡(jiǎn)單了草巡。

為了記錄僵尸在下一次進(jìn)擊前需要等待的時(shí)間守呜,我們使用了 Solidity 的時(shí)間單位。

時(shí)間單位
Solidity 使用自己的本地時(shí)間單位山憨。

變量 now 將返回當(dāng)前的unix時(shí)間戳(自1970年1月1日以來(lái)經(jīng)過(guò)的秒數(shù))查乒。我寫這句話時(shí) unix 時(shí)間是1515527488`。

注意:Unix時(shí)間傳統(tǒng)用一個(gè)32位的整數(shù)進(jìn)行存儲(chǔ)郁竟。這會(huì)導(dǎo)致“2038年”問(wèn)題玛迄,當(dāng)這個(gè)32位的unix時(shí)間戳不夠用,產(chǎn)生溢出棚亩,使用這個(gè)時(shí)間的遺留系統(tǒng)就麻煩了蓖议。所以虏杰,如果我們想讓我們的 DApp 跑夠20年,我們可以使用64位整數(shù)表示時(shí)間勒虾,但為此我們的用戶又得支付更多的 gas纺阔。真是個(gè)兩難的設(shè)計(jì)啊修然!

Solidity 還包含秒(seconds)笛钝,分鐘(minutes)小時(shí)(hours)愕宋,天(days)玻靡,周(weeks)年(years) 等時(shí)間單位。它們都會(huì)轉(zhuǎn)換成對(duì)應(yīng)的秒數(shù)放入 uint 中中贝。所以 1分鐘 就是 60囤捻,1小時(shí)3600(60秒×60分鐘),1天86400(24小時(shí)×60分鐘×60秒)邻寿,以此類推最蕾。

下面是一些使用時(shí)間單位的實(shí)用案例:

uint lastUpdated;

// 將‘上次更新時(shí)間’ 設(shè)置為 ‘現(xiàn)在’
function updateTimestamp() public {
  lastUpdated = now;
}

// 如果到上次`updateTimestamp` 超過(guò)5分鐘,返回 'true'
// 不到5分鐘返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

有了這些工具老厌,我們可以為僵尸設(shè)定”冷靜時(shí)間“功能

實(shí)戰(zhàn)演習(xí)

現(xiàn)在咱們給DApp添加一個(gè)“冷卻周期”的設(shè)定瘟则,讓僵尸兩次攻擊或捕獵之間必須等待 1天。

  1. 聲明一個(gè)名為 cooldownTime 的uint枝秤,并將其設(shè)置為 1 days醋拧。(沒(méi)錯(cuò),”1 days“使用了復(fù)數(shù)淀弹, 否則通不過(guò)編譯器)

  2. 因?yàn)樵谏弦徽轮形覀兘o Zombie 結(jié)構(gòu)體中添加 level 和 readyTime 兩個(gè)參數(shù)丹壕,所以現(xiàn)在創(chuàng)建一個(gè)新的 Zombie 結(jié)構(gòu)體時(shí),需要修改 _createZombie()薇溃,在其中把新舊參數(shù)都初始化一下菌赖。
    修改 zombies.push 那一行, 添加加2個(gè)參數(shù):1(表示當(dāng)前的 level )和uint32(now + cooldownTime 現(xiàn)在+冷靜時(shí)間)(表示下次允許攻擊的時(shí)間 readyTime)沐序。

注意:必須使用 uint32(...) 進(jìn)行強(qiáng)制類型轉(zhuǎn)換琉用,因?yàn)?now 返回類型 uint256。所以我們需要明確將它轉(zhuǎn)換成一個(gè) uint32 類型的變量策幼。

now + cooldownTime 將等于當(dāng)前的unix時(shí)間戳(以秒為單位)加上”1天“里的秒數(shù) - 這將等于從現(xiàn)在起1天后的unix時(shí)間戳邑时。然后我們就比較,看看這個(gè)僵尸的 readyTime是否大于 now特姐,以決定再次啟用僵尸的時(shí)機(jī)有沒(méi)有到來(lái)晶丘。

下一章中,我們將討論如何通過(guò) readyTime 來(lái)規(guī)范僵尸的行為。

pragma solidity ^0.4.19;

import "./ownable.sol";

contract ZombieFactory is Ownable {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    uint cooldownTime = 1 days;

    struct Zombie {
        string name;
        uint dna;
        uint32 level;
        uint32 readyTime;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) internal {
    
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

第6章: 僵尸冷卻

現(xiàn)在浅浮,Zombie 結(jié)構(gòu)體中定義好了一個(gè) readyTime 屬性沫浆,讓我們跳到 zombiefeeding.sol, 去實(shí)現(xiàn)一個(gè)”冷卻周期定時(shí)器“滚秩。

按照以下步驟修改 feedAndMultiply

  1. ”捕獵“行為會(huì)觸發(fā)僵尸的”冷卻周期“

  2. 僵尸在這段”冷卻周期“結(jié)束前不可再捕獵小貓

這將限制僵尸专执,防止其無(wú)限制地捕獵小貓或者整天不停地繁殖。將來(lái)叔遂,當(dāng)我們?cè)黾討?zhàn)斗功能時(shí)他炊,我們同樣用”冷卻周期“限制僵尸之間打斗的頻率争剿。

首先已艰,我們要定義一些輔助函數(shù),設(shè)置并檢查僵尸的 readyTime蚕苇。

將結(jié)構(gòu)體作為參數(shù)傳入

由于結(jié)構(gòu)體的存儲(chǔ)指針可以以參數(shù)的方式傳遞給一個(gè) privateinternal 的函數(shù)哩掺,因此結(jié)構(gòu)體可以在多個(gè)函數(shù)之間相互傳遞。

遵循這樣的語(yǔ)法:

function _doStuff(Zombie storage _zombie) internal {
  // do stuff with _zombie
}

這樣我們可以將某僵尸的引用直接傳遞給一個(gè)函數(shù)涩笤,而不用是通過(guò)參數(shù)傳入僵尸ID后嚼吞,函數(shù)再依據(jù)ID去查找。

實(shí)戰(zhàn)演習(xí)

  1. 先定義一個(gè) _triggerCooldown 函數(shù)蹬碧。它要求一個(gè)參數(shù)舱禽,_zombie,表示一某個(gè)僵尸的存儲(chǔ)指針恩沽。這個(gè)函數(shù)可見(jiàn)性設(shè)置為 internal誊稚。

  2. 在函數(shù)中,把 _zombie.readyTime 設(shè)置為 uint32(now + cooldownTime)罗心。

  3. 接下來(lái)里伯,創(chuàng)建一個(gè)名為 _isReady 的函數(shù)。這個(gè)函數(shù)的參數(shù)也是名為 _zombieZombie storage渤闷。這個(gè)功能只具有 internal 可見(jiàn)性疾瓮,并返回一個(gè) bool 值。

  4. 函數(shù)計(jì)算返回(_zombie.readyTime <= now)飒箭,值為 truefalse狼电。這個(gè)功能的目的是判斷下次允許獵食的時(shí)間是否已經(jīng)到了。

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }


  function _triggerCooldown(Zombie storage _zombie) internal {
    _zombie.readyTime = uint32(now + cooldownTime);
  }

  function _isReady (Zombie storage _zombie) internal view returns (bool) {
    return (_zombie.readyTime <= now);
  }

  function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }
}

第7章: 公有函數(shù)和安全性

現(xiàn)在來(lái)修改 feedAndMultiply 弦蹂,實(shí)現(xiàn)冷卻周期漫萄。

回顧一下這個(gè)函數(shù),前一課上我們將其可見(jiàn)性設(shè)置為public盈匾。你必須仔細(xì)地檢查所有聲明為 publicexternal的函數(shù)腾务,一個(gè)個(gè)排除用戶濫用它們的可能,謹(jǐn)防安全漏洞削饵。請(qǐng)記住岩瘦,如果這些函數(shù)沒(méi)有類似 onlyOwner 這樣的函數(shù)修飾符未巫,用戶能利用各種可能的參數(shù)去調(diào)用它們。

檢查完這個(gè)函數(shù)启昧,用戶就可以直接調(diào)用這個(gè)它叙凡,并傳入他們所希望的 _targetDnaspecies 。打個(gè)游戲還得遵循這么多的規(guī)則密末,還能不能愉快地玩耍拔找!

仔細(xì)觀察严里,這個(gè)函數(shù)只需被 feedOnKitty() 調(diào)用新啼,因此,想要防止漏洞刹碾,最簡(jiǎn)單的方法就是設(shè)其可見(jiàn)性為 internal燥撞。

實(shí)戰(zhàn)演習(xí)

  1. 目前函數(shù) feedAndMultiply 可見(jiàn)性為 public。我們將其改為 internal 以保障合約安全迷帜。因?yàn)槲覀儾幌M脩粽{(diào)用它的時(shí)候塞進(jìn)一堆亂七八糟的 DNA物舒。

  2. feedAndMultiply 過(guò)程需要參考 cooldownTime。首先戏锹,在找到 myZombie 之后冠胯,添加一個(gè) require 語(yǔ)句來(lái)檢查 _isReady() 并將 myZombie 傳遞給它。這樣用戶必須等到僵尸的 冷卻周期 結(jié)束后才能執(zhí)行 feedAndMultiply 功能锦针。

  3. 在函數(shù)結(jié)束時(shí)荠察,調(diào)用 _triggerCooldown(myZombie),標(biāo)明捕獵行為觸發(fā)了僵尸新的冷卻周期伞插。

pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }

  function _triggerCooldown(Zombie storage _zombie) internal {
    _zombie.readyTime = uint32(now + cooldownTime);
  }

  function _isReady(Zombie storage _zombie) internal view returns (bool) {
      return (_zombie.readyTime <= now);
  }

  // 1. 使這個(gè)函數(shù)的可見(jiàn)性為 internal
  function feedAndMultiply(uint _zombieId, uint _targetDna, string species) internal {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    // 2. 在這里為 `_isReady` 增加一個(gè)檢查
    require(_isReady(myZombie));
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
    // 3. 調(diào)用 `triggerCooldown`
    _triggerCooldown(myZombie);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}

第8章: 進(jìn)一步了解函數(shù)修飾符

相當(dāng)不錯(cuò)割粮!我們的僵尸現(xiàn)在有了“冷卻定時(shí)器”功能。

接下來(lái)媚污,我們將添加一些輔助方法舀瓢。我們?yōu)槟鷦?chuàng)建了一個(gè)名為 zombiehelper.sol 的新文件,并且將 zombiefeeding.sol 導(dǎo)入其中耗美,這讓我們的代碼更整潔京髓。

我們打算讓僵尸在達(dá)到一定水平后,獲得特殊能力商架。但是達(dá)到這個(gè)小目標(biāo)堰怨,我們還需要學(xué)一學(xué)什么是“函數(shù)修飾符”。

帶參數(shù)的函數(shù)修飾符

之前我們已經(jīng)讀過(guò)一個(gè)簡(jiǎn)單的函數(shù)修飾符了:onlyOwner蛇摸。函數(shù)修飾符也可以帶參數(shù)备图。例如:

// 存儲(chǔ)用戶年齡的映射
mapping (uint => uint) public age;

// 限定用戶年齡的修飾符
modifier olderThan(uint _age, uint _userId) {
  require(age[_userId] >= _age);
  _;
}

// 必須年滿16周歲才允許開(kāi)車 (至少在美國(guó)是這樣的).
// 我們可以用如下參數(shù)調(diào)用`olderThan` 修飾符:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 其余的程序邏輯
}

看到了吧, olderThan 修飾符可以像函數(shù)一樣接收參數(shù),是“宿主”函數(shù) driveCar 把參數(shù)傳遞給它的修飾符的揽涮。

來(lái)抠藕,我們自己生產(chǎn)一個(gè)修飾符,通過(guò)傳入的level參數(shù)來(lái)限制僵尸使用某些特殊功能蒋困。

實(shí)戰(zhàn)演習(xí)

ZombieHelper 中盾似,創(chuàng)建一個(gè)名為 aboveLevelmodifier,它接收2個(gè)參數(shù)雪标, _level (uint類型) 以及 _zombieId (uint類型)零院。
運(yùn)用函數(shù)邏輯確保僵尸 zombies[_zombieId].level 大于或等于 _level
記住村刨,修飾符的最后一行為 _;告抄,表示修飾符調(diào)用結(jié)束后返回,并執(zhí)行調(diào)用函數(shù)余下的部分烹困。


pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  // 在這里開(kāi)始
  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }
}


第9章: 僵尸修飾符

現(xiàn)在讓我們?cè)O(shè)計(jì)一些使用 aboveLevel 修飾符的函數(shù)玄妈。

作為游戲乾吻,您得有一些措施激勵(lì)玩家們?nèi)ド?jí)他們的僵尸:

  • 2級(jí)以上的僵尸髓梅,玩家可給他們改名。
  • 20級(jí)以上的僵尸绎签,玩家能給他們定制的 DNA枯饿。
    是實(shí)現(xiàn)這些功能的時(shí)候了。以下是上一課的示例代碼诡必,供參考:
// 存儲(chǔ)用戶年齡的映射
mapping (uint => uint) public age;

// 限定用戶年齡的修飾符
modifier olderThan(uint _age, uint _userId) {
  require (age[_userId] >= _age);
  _;
}

// 必須年滿16周歲才允許開(kāi)車 (至少在美國(guó)是這樣的).
// 我們可以用如下參數(shù)調(diào)用`olderThan` 修飾符:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 其余的程序邏輯
}

實(shí)戰(zhàn)演習(xí)

  1. 創(chuàng)建一個(gè)名為 changeName 的函數(shù)奢方。它接收2個(gè)參數(shù):_zombieIduint類型)以及 _newNamestring類型),可見(jiàn)性為 external爸舒。它帶有一個(gè) aboveLevel 修飾符蟋字,調(diào)用的時(shí)候通過(guò) _level 參數(shù)傳入2, 當(dāng)然扭勉,別忘了同時(shí)傳 _zombieId 參數(shù)鹊奖。

  2. 在這個(gè)函數(shù)中,首先我們用 require 語(yǔ)句涂炎,驗(yàn)證 msg.sender 是否就是 zombieToOwner [_zombieId]忠聚。

  3. 然后函數(shù)將 zombies[_zombieId] .name 設(shè)置為 _newName

  4. changeName 下創(chuàng)建另一個(gè)名為 changeDna 的函數(shù)唱捣。它的定義和內(nèi)容幾乎和 changeName 相同两蟀,不過(guò)它第二個(gè)參數(shù)是 _newDnauint類型),在修飾符 aboveLevel_level 參數(shù)中傳遞 20 ≌痃裕現(xiàn)在赂毯,他可以把僵尸的 dna 設(shè)置為 _newDna 了。

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  // 在這里開(kāi)始
  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }
}

第10章: 利用 'View' 函數(shù)節(jié)省 Gas

酷炫!現(xiàn)在高級(jí)別僵尸可以擁有特殊技能了党涕,這一定會(huì)鼓動(dòng)我們的玩家去打怪升級(jí)的活烙。你喜歡的話,回頭我們還能添加更多的特殊技能遣鼓。

現(xiàn)在需要添加的一個(gè)功能是:我們的 DApp 需要一個(gè)方法來(lái)查看某玩家的整個(gè)僵尸軍團(tuán) - 我們稱之為 getZombiesByOwner合呐。

實(shí)現(xiàn)這個(gè)功能只需從區(qū)塊鏈中讀取數(shù)據(jù)犀盟,所以它可以是一個(gè) view 函數(shù)。這讓我們不得不回顧一下“gas優(yōu)化”這個(gè)重要話題。

“view” 函數(shù)不花 “gas”

當(dāng)玩家從外部調(diào)用一個(gè)view函數(shù)撩轰,是不需要支付一分 gas 的。

這是因?yàn)?view 函數(shù)不會(huì)真正改變區(qū)塊鏈上的任何數(shù)據(jù) - 它們只是讀取脯颜。因此用 view 標(biāo)記一個(gè)函數(shù)悠汽,意味著告訴 web3.js,運(yùn)行這個(gè)函數(shù)只需要查詢你的本地以太坊節(jié)點(diǎn)缸棵,而不需要在區(qū)塊鏈上創(chuàng)建一個(gè)事務(wù)(事務(wù)需要運(yùn)行在每個(gè)節(jié)點(diǎn)上舟茶,因此花費(fèi) gas)。

稍后我們將介紹如何在自己的節(jié)點(diǎn)上設(shè)置 web3.js堵第。但現(xiàn)在吧凉,你關(guān)鍵是要記住,在所能只讀的函數(shù)上標(biāo)記上表示“只讀”的“external view 聲明踏志,就能為你的玩家減少在 DApp 中 gas 用量阀捅。

注意:如果一個(gè) view 函數(shù)在另一個(gè)函數(shù)的內(nèi)部被調(diào)用,而調(diào)用函數(shù)與 view 函數(shù)的不屬于同一個(gè)合約针余,也會(huì)產(chǎn)生調(diào)用成本饲鄙。這是因?yàn)槿绻髡{(diào)函數(shù)在以太坊創(chuàng)建了一個(gè)事務(wù),它仍然需要逐個(gè)節(jié)點(diǎn)去驗(yàn)證圆雁。所以標(biāo)記為 view 的函數(shù)只有在外部調(diào)用時(shí)才是免費(fèi)的忍级。

實(shí)戰(zhàn)演習(xí)

我們來(lái)寫一個(gè)”返回某玩家的整個(gè)僵尸軍團(tuán)“的函數(shù)。當(dāng)我們從 web3.js 中調(diào)用它伪朽,即可顯示某一玩家的個(gè)人資料頁(yè)轴咱。

這個(gè)函數(shù)的邏輯有點(diǎn)復(fù)雜,我們需要好幾個(gè)章節(jié)來(lái)描述它的實(shí)現(xiàn)驱负。

  1. 創(chuàng)建一個(gè)名為 getZombiesByOwner 的新函數(shù)嗦玖。它有一個(gè)名為 _owneraddress 類型的參數(shù)。

  2. 將其申明為 external view 函數(shù)跃脊,這樣當(dāng)玩家從 web3.js 中調(diào)用它時(shí)宇挫,不需要花費(fèi)任何 gas。

  3. 函數(shù)需要返回一個(gè)uint []uint數(shù)組)酪术。

先這么聲明著器瘪,我們將在下一章中填充函數(shù)體翠储。

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  // 在這里創(chuàng)建你的函數(shù)
  function getZombiesByOwner(address _owner) external view returns(uint []) {
    
  }
}

第11章: 存儲(chǔ)非常昂貴

Solidity 使用storage(存儲(chǔ))是相當(dāng)昂貴的,”寫入“操作尤其貴橡疼。

這是因?yàn)樵瑹o(wú)論是寫入還是更改一段數(shù)據(jù), 這都將永久性地寫入?yún)^(qū)塊鏈欣除∽∈茫”永久性“啊历帚!需要在全球數(shù)千個(gè)節(jié)點(diǎn)的硬盤上存入這些數(shù)據(jù)滔岳,隨著區(qū)塊鏈的增長(zhǎng),拷貝份數(shù)更多挽牢,存儲(chǔ)量也就越大谱煤。這是需要成本的!

為了降低成本禽拔,不到萬(wàn)不得已刘离,避免將數(shù)據(jù)寫入存儲(chǔ)。這也會(huì)導(dǎo)致效率低下的編程邏輯 - 比如每次調(diào)用一個(gè)函數(shù)睹栖,都需要在 memory(內(nèi)存) 中重建一個(gè)數(shù)組硫惕,而不是簡(jiǎn)單地將上次計(jì)算的數(shù)組給存儲(chǔ)下來(lái)以便快速查找。

在大多數(shù)編程語(yǔ)言中磨淌,遍歷大數(shù)據(jù)集合都是昂貴的疲憋。但是在 Solidity 中凿渊,使用一個(gè)標(biāo)記了external view的函數(shù)梁只,遍歷比 storage 要便宜太多,因?yàn)?view 函數(shù)不會(huì)產(chǎn)生任何花銷埃脏。 (gas可是真金白銀疤侣唷!)彩掐。

我們將在下一章討論for循環(huán)构舟,現(xiàn)在我們來(lái)看一下看如何如何在內(nèi)存中聲明數(shù)組。

在內(nèi)存中聲明數(shù)組
在數(shù)組后面加上 memory關(guān)鍵字堵幽, 表明這個(gè)數(shù)組是僅僅在內(nèi)存中創(chuàng)建狗超,不需要寫入外部存儲(chǔ),并且在函數(shù)調(diào)用結(jié)束時(shí)它就解散了朴下。與在程序結(jié)束時(shí)把數(shù)據(jù)保存進(jìn) storage 的做法相比努咐,內(nèi)存運(yùn)算可以大大節(jié)省gas開(kāi)銷 -- 把這數(shù)組放在view里用,完全不用花錢殴胧。

以下是申明一個(gè)內(nèi)存數(shù)組的例子:

function getArray() external pure returns(uint[]) {
  // 初始化一個(gè)長(zhǎng)度為3的內(nèi)存數(shù)組
  uint[] memory values = new uint[](3);
  // 賦值
  values.push(1);
  values.push(2);
  values.push(3);
  // 返回?cái)?shù)組
  return values;
}

這個(gè)小例子展示了一些語(yǔ)法規(guī)則渗稍,下一章中佩迟,我們將通過(guò)一個(gè)實(shí)際用例,展示它和 for 循環(huán)結(jié)合的做法竿屹。

注意:內(nèi)存數(shù)組 必須 用長(zhǎng)度參數(shù)(在本例中為3)創(chuàng)建报强。目前不支持 array.push()之類的方法調(diào)整數(shù)組大小,在未來(lái)的版本可能會(huì)支持長(zhǎng)度修改拱燃。

實(shí)戰(zhàn)演習(xí)

我們要要?jiǎng)?chuàng)建一個(gè)名為 getZombiesByOwner 的函數(shù)秉溉,它以uint []數(shù)組的形式返回某一用戶所擁有的所有僵尸。

  1. 聲明一個(gè)名為result的`uint [] memory' (內(nèi)存變量數(shù)組)

  2. 將其設(shè)置為一個(gè)新的 uint 類型數(shù)組碗誉。數(shù)組的長(zhǎng)度為該 _owner 所擁有的僵尸數(shù)量坚嗜,這可通過(guò)調(diào)用 ownerZombieCount [_ owner] 來(lái)獲取。

函數(shù)結(jié)束诗充,返回 result 苍蔬。目前它只是個(gè)空數(shù)列,我們到下一章去實(shí)現(xiàn)它蝴蜓。

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  function getZombiesByOwner(address _owner) external view returns(uint[]) {
    // 在這里開(kāi)始
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    return result;
  }

}

第12章: For 循環(huán)

在之前的章節(jié)中碟绑,我們提到過(guò),函數(shù)中使用的數(shù)組是運(yùn)行時(shí)在內(nèi)存中通過(guò) for 循環(huán)實(shí)時(shí)構(gòu)建茎匠,而不是預(yù)先建立在存儲(chǔ)中的格仲。

為什么要這樣做呢?

為了實(shí)現(xiàn) getZombiesByOwner 函數(shù)诵冒,一種“無(wú)腦式”的解決方案是在 ZombieFactory 中存入”主人“和”僵尸軍團(tuán)“的映射凯肋。

mapping (address => uint[]) public ownerToZombies
然后我們每次創(chuàng)建新僵尸時(shí),執(zhí)行 ownerToZombies [owner] .push(zombieId) 將其添加到主人的僵尸數(shù)組中汽馋。而 getZombiesByOwner 函數(shù)也非常簡(jiǎn)單:

function getZombiesByOwner(address _owner) external view returns (uint[]) {
  return ownerToZombies[_owner];
}

這個(gè)做法有問(wèn)題

做法倒是簡(jiǎn)單侮东。可是如果我們需要一個(gè)函數(shù)來(lái)把一頭僵尸轉(zhuǎn)移到另一個(gè)主人名下(我們一定會(huì)在后面的課程中實(shí)現(xiàn)的)豹芯,又會(huì)發(fā)生什么悄雅?

這個(gè)“換主”函數(shù)要做到:

1.將僵尸push到新主人的 ownerToZombies 數(shù)組中, 2.從舊主的 ownerToZombies 數(shù)組中移除僵尸铁蹈, 3.將舊主僵尸數(shù)組中“換主僵尸”之后的的每頭僵尸都往前挪一位宽闲,把挪走“換主僵尸”后留下的“空槽”填上, 4.將數(shù)組長(zhǎng)度減1握牧。

但是第三步實(shí)在是太貴了容诬!因?yàn)槊颗矂?dòng)一頭僵尸,我們都要執(zhí)行一次寫操作沿腰。如果一個(gè)主人有20頭僵尸览徒,而第一頭被挪走了,那為了保持?jǐn)?shù)組的順序矫俺,我們得做19個(gè)寫操作吱殉。

由于寫入存儲(chǔ)是 Solidity 中最費(fèi) gas 的操作之一掸冤,使得換主函數(shù)的每次調(diào)用都非常昂貴。更糟糕的是友雳,每次調(diào)用的時(shí)候花費(fèi)的 gas 都不同稿湿!具體還取決于用戶在原主軍團(tuán)中的僵尸頭數(shù),以及移走的僵尸所在的位置押赊。以至于用戶都不知道應(yīng)該支付多少 gas饺藤。

注意:當(dāng)然,我們也可以把數(shù)組中最后一個(gè)僵尸往前挪來(lái)填補(bǔ)空槽流礁,并將數(shù)組長(zhǎng)度減少一涕俗。但這樣每做一筆交易,都會(huì)改變僵尸軍團(tuán)的秩序神帅。

由于從外部調(diào)用一個(gè) view 函數(shù)是免費(fèi)的再姑,我們也可以在 getZombiesByOwner 函數(shù)中用一個(gè)for循環(huán)遍歷整個(gè)僵尸數(shù)組,把屬于某個(gè)主人的僵尸挑出來(lái)構(gòu)建出僵尸數(shù)組找御。那么我們的 transfer 函數(shù)將會(huì)便宜得多元镀,因?yàn)槲覀儾恍枰矂?dòng)存儲(chǔ)里的僵尸數(shù)組重新排序,總體上這個(gè)方法會(huì)更便宜霎桅,雖然有點(diǎn)反直覺(jué)栖疑。

使用 for 循環(huán)

for循環(huán)的語(yǔ)法在 Solidity 和 JavaScript 中類似。

來(lái)看一個(gè)創(chuàng)建偶數(shù)數(shù)組的例子:

function getEvens() pure external returns(uint[]) {
  uint[] memory evens = new uint[](5);
  // 在新數(shù)組中記錄序列號(hào)
  uint counter = 0;
  // 在循環(huán)從1迭代到10:
  for (uint i = 1; i <= 10; i++) {
    // 如果 `i` 是偶數(shù)...
    if (i % 2 == 0) {
      // 把它加入偶數(shù)數(shù)組
      evens[counter] = i;
      //索引加一滔驶, 指向下一個(gè)空的‘even’
      counter++;
    }
  }
  return evens;
}

這個(gè)函數(shù)將返回一個(gè)形為 [2,4,6,8,10] 的數(shù)組遇革。

實(shí)戰(zhàn)演習(xí)

我們回到 getZombiesByOwner 函數(shù), 通過(guò)一條 for 循環(huán)來(lái)遍歷 DApp 中所有的僵尸揭糕, 將給定的‘用戶id'與每頭僵尸的‘主人’進(jìn)行比較萝快,并在函數(shù)返回之前將它們推送到我們的result 數(shù)組中。

1.聲明一個(gè)變量 counter插佛,屬性為 uint杠巡,設(shè)其值為 0 。我們用這個(gè)變量作為 result 數(shù)組的索引雇寇。

2.聲明一個(gè) for 循環(huán), 從 uint i = 0i <zombies.length蚌铜。它將遍歷數(shù)組中的每一頭僵尸锨侯。

3.在每一輪 for 循環(huán)中,用一個(gè) if 語(yǔ)句來(lái)檢查 zombieToOwner [i] 是否等于 _owner冬殃。這會(huì)比較兩個(gè)地址是否匹配囚痴。

4.在 if 語(yǔ)句中:

  1. 通過(guò)將 result [counter] 設(shè)置為 i,將僵尸ID添加到 result 數(shù)組中审葬。
  2. 將counter加1(參見(jiàn)上面的for循環(huán)示例)深滚。
    就是這樣 - 這個(gè)函數(shù)能返回 _owner 所擁有的僵尸數(shù)組奕谭,不花一分錢 gas。
pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  function getZombiesByOwner(address _owner) external view returns(uint[]) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    // 在這里開(kāi)始
    uint counter = 0;
    for(uint i=0; i<zombies.length; i++){
      if(zombieToOwner[i] == _owner){
        result[counter] = i;
        counter++;
      }
    }
    return result;
  }

}

第13章: 放在一起

恭喜您啊痴荐,居然把第三課也學(xué)完了血柳!

讓我們回顧一下:

  • 添加了一種新方法來(lái)修改CryptoKitties合約
  • 學(xué)會(huì)使用 onlyOwner 進(jìn)行調(diào)用權(quán)限限制
  • 了解了 gas 和 gas 的優(yōu)化
  • 為僵尸添加了 “級(jí)別” 和 “冷卻周期”屬性
  • 當(dāng)僵尸達(dá)到一定級(jí)別時(shí),允許修改僵尸的名字和 DNA
  • 最后生兆,定義了一個(gè)函數(shù)难捌,用以返回某個(gè)玩家的僵尸軍團(tuán)

領(lǐng)獎(jiǎng)時(shí)間

作為完成第三課的獎(jiǎng)勵(lì),您的兩個(gè)僵尸都已經(jīng)升級(jí)了鸦难!

現(xiàn)在 NoName(你在第2課創(chuàng)建的小貓僵尸)已經(jīng)升級(jí)到第2級(jí)根吁,你可以調(diào)用 changeName 給它取個(gè)名字。 終于不再是無(wú)名之輩了合蔽!

去給您的 NoName 取個(gè)名字吧击敌,等你做完下一章,本課程就結(jié)束了拴事。

(示例)[https://cryptozombies.io/zh/lesson/3/chapter/13]

下一篇:以太坊Solidity開(kāi)發(fā)入門(大師篇)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末愚争,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挤聘,更是在濱河造成了極大的恐慌轰枝,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件组去,死亡現(xiàn)場(chǎng)離奇詭異鞍陨,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)从隆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門诚撵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人键闺,你說(shuō)我怎么就攤上這事寿烟。” “怎么了辛燥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵筛武,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我挎塌,道長(zhǎng)徘六,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任榴都,我火速辦了婚禮待锈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嘴高。我一直安慰自己竿音,他們只是感情好和屎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著春瞬,像睡著了一般柴信。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上快鱼,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天颠印,我揣著相機(jī)與錄音,去河邊找鬼抹竹。 笑死线罕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窃判。 我是一名探鬼主播钞楼,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼袄琳!你這毒婦竟也來(lái)了询件?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤唆樊,失蹤者是張志新(化名)和其女友劉穎宛琅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體逗旁,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘿辟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了片效。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片红伦。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖淀衣,靈堂內(nèi)的尸體忽然破棺而出昙读,到底是詐尸還是另有隱情,我是刑警寧澤膨桥,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布蛮浑,位于F島的核電站,受9級(jí)特大地震影響国撵,放射性物質(zhì)發(fā)生泄漏陵吸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一介牙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧澳厢,春花似錦环础、人聲如沸囚似。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)饶唤。三九已至,卻和暖如春贯钩,著一層夾襖步出監(jiān)牢的瞬間募狂,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工角雷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祸穷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓勺三,卻偏偏與公主長(zhǎng)得像雷滚,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吗坚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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