Solidity之合約講解(二) - 寫一個(gè)"僵尸"智能合約

映射(Mapping)和地址(Address)

我們通過(guò)給數(shù)據(jù)庫(kù)中的僵尸指定“主人”囚枪, 來(lái)支持“多玩家”模式。
如此一來(lái)劳淆,我們需要引入2個(gè)新的數(shù)據(jù)類型:mapping(映射)address(地址)眶拉。
Addresses (地址)
以太坊區(qū)塊鏈由 account (賬戶)組成,你可以把它想象成銀行賬戶憔儿。一個(gè)帳戶的余額是 以太幣(在以太坊區(qū)塊鏈上使用的幣種)忆植,你可以和其他帳戶之間支付和接收以太幣,就像你的銀行帳戶可以轉(zhuǎn)賬資金到其他銀行帳戶一樣谒臼。

每個(gè)帳戶都有一個(gè)“地址”朝刊,你可以把它想象成銀行賬號(hào)。這是賬戶唯一的標(biāo)識(shí)符蜈缤,它看起來(lái)長(zhǎng)這樣:0x0cE446255506E92DF41614C46F1d6df9Cc969183(這是原本教程制作者的以太坊賬戶地址拾氓,可以轉(zhuǎn)eth鼓勵(lì)鼓勵(lì)下哈??)
地址的細(xì)節(jié)將在后面的課程中介紹,現(xiàn)在你只需要了解地址屬于特定用戶(或智能合約)的底哥。
所以我們可以指定“地址”作為僵尸主人的 ID咙鞍。當(dāng)用戶通過(guò)與我們的應(yīng)用程序交互來(lái)創(chuàng)建新的僵尸時(shí),新僵尸的所有權(quán)被設(shè)置到調(diào)用者的以太坊地址下趾徽。


Mapping(映射)

映射是除了 結(jié)構(gòu)體 和 數(shù)組 的另一種在 Solidity 中存儲(chǔ)有組織數(shù)據(jù)的方法续滋。
映射是這樣定義的:

//對(duì)于金融應(yīng)用程序,將用戶的余額保存在一個(gè) uint類型的變量中:
mapping (address => uint) public accountBalance;
//或者可以用來(lái)通過(guò)userId 存儲(chǔ)/查找的用戶名
mapping (uint => string) userIdToName;

映射本質(zhì)上是存儲(chǔ)和查找數(shù)據(jù)所用的鍵-值對(duì)孵奶。在第一個(gè)例子中疲酌,鍵是一個(gè) address,值是一個(gè) uint了袁,在第二個(gè)例子中朗恳,鍵是一個(gè)uint,值是一個(gè) string载绿。

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

給僵尸合約創(chuàng)建兩個(gè)映射:

  • zombieToOwner 的映射粥诫。其鍵是一個(gè)uint(我們將根據(jù)它的 id 存儲(chǔ)和查找僵尸),值address崭庸。映射屬性為public怀浆。
  • 一個(gè)名為 ownerZombieCount 的映射劝堪,其中鍵是 address,值是 uint揉稚。

Msg.sender

在 Solidity 中秒啦,有一些全局變量可以被所有函數(shù)調(diào)用。 其中一個(gè)就是 msg.sender搀玖,它指的是當(dāng)前調(diào)用者(或智能合約)的 address余境。
注意:在 Solidity 中,功能執(zhí)行始終需要從外部調(diào)用者開始灌诅。 一個(gè)合約只會(huì)在區(qū)塊鏈上什么也不做芳来,除非有人調(diào)用其中的函數(shù)。所以 msg.sender總是存在的猜拾。
以下是使用 msg.sender 來(lái)更新 mapping 的例子:

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
  // 更新我們的 `favoriteNumber` 映射來(lái)將 `_myNumber`存儲(chǔ)在 `msg.sender`名下
  favoriteNumber[msg.sender] = _myNumber;
  // 存儲(chǔ)數(shù)據(jù)至映射的方法和將數(shù)據(jù)存儲(chǔ)在數(shù)組相似
}

function whatIsMyNumber() public view returns (uint) {
  // 拿到存儲(chǔ)在調(diào)用者地址名下的值
  // 若調(diào)用者還沒(méi)調(diào)用 setMyNumber即舌, 則值為 `0`
  return favoriteNumber[msg.sender];
}

在這個(gè)小小的例子中,任何人都可以調(diào)用 setMyNumber 在我們的合約中存下一個(gè) uint 并且與他們的地址相綁定挎袜。 然后顽聂,他們調(diào)用 whatIsMyNumber 就會(huì)返回他們uint值。
使用 msg.sender 很安全盯仪,因?yàn)樗哂幸蕴粎^(qū)塊鏈的安全保障 —— 除非竊取與以太坊地址相關(guān)聯(lián)的私鑰紊搪,否則是沒(méi)有辦法修改其他人的數(shù)據(jù)的。


Require

在(一)最后全景,我們成功讓用戶通過(guò)調(diào)用 createRandomZombie函數(shù) 并輸入一個(gè)名字來(lái)創(chuàng)建新的僵尸耀石。 但是,如果用戶能持續(xù)調(diào)用這個(gè)函數(shù)來(lái)創(chuàng)建出無(wú)限多個(gè)僵尸加入他們的軍團(tuán)爸黄,這游戲就太沒(méi)意思了滞伟!
于是,我們作出限定:每個(gè)玩家只能調(diào)用一次這個(gè)函數(shù)炕贵。 這樣一來(lái)梆奈,新玩家可以在剛開始玩游戲時(shí)通過(guò)調(diào)用它,為其軍團(tuán)創(chuàng)建初始僵尸鲁驶。

我們?cè)鯓硬拍芟薅總€(gè)玩家只調(diào)用一次這個(gè)函數(shù)呢鉴裹?

答案是使用require。 require使得函數(shù)在執(zhí)行過(guò)程中钥弯,當(dāng)不滿足某些條件時(shí)拋出錯(cuò)誤,并停止執(zhí)行:

function sayHiToVitalik(string _name) public returns (string) {
  // 比較 _name 是否等于 "Vitalik". 如果不成立督禽,拋出異常并終止程序
  // (敲黑板: Solidity 并不支持原生的字符串比較, 我們只能通過(guò)比較
  // 兩字符串的 keccak256 哈希值來(lái)進(jìn)行判斷)
  require(keccak256(_name) == keccak256("Vitalik"));
  // 如果返回 true, 運(yùn)行如下語(yǔ)句
  return "Hi!";
}

如果你這樣調(diào)用函數(shù) sayHiToVitalik(“Vitalik”) ,它會(huì)返回“Hi脆霎!”。而如果調(diào)用的時(shí)候使用了其他參數(shù)狈惫,它則會(huì)拋出錯(cuò)誤并停止執(zhí)行睛蛛。
值得注意的是其中Solidity 并不支持原生的字符串比較
因此鹦马,在函數(shù)里,用 require 驗(yàn)證前置條件是非常有必要的忆肾。

在我們的僵尸游戲中荸频,我們不希望用戶通過(guò)反復(fù)調(diào)用 createRandomZombie 來(lái)給他們的軍隊(duì)創(chuàng)建無(wú)限多個(gè)僵尸 —— 這將使得游戲非常無(wú)聊。
所以我們使用 require 來(lái)確保這個(gè)函數(shù)只有在每個(gè)用戶第一次調(diào)用它的時(shí)候執(zhí)行客冈,用以創(chuàng)建初始僵尸旭从。
在 createRandomZombie 的前面放置 require 語(yǔ)句。 使得函數(shù)先檢查 ownerZombieCount [msg.sender] 的值為 0 场仲,不然就拋出一個(gè)錯(cuò)誤和悦。

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

繼承(Inheritance)

我們的游戲代碼越來(lái)越長(zhǎng)。 當(dāng)代碼過(guò)于冗長(zhǎng)的時(shí)候渠缕,最好將代碼和邏輯分拆到多個(gè)不同的合約中鸽素,以便于管理。
有個(gè)讓 Solidity 的代碼易于管理的功能亦鳞,就是合約 inheritance (繼承):

contract Doge {
  function catchphrase() public returns (string) {
    return "So Wow CryptoDoge";
  }
}

contract BabyDoge is Doge {
  function anotherCatchphrase() public returns (string) {
    return "Such Moon BabyDoge";
  }
}

由于 BabyDoge 是從 Doge 那里 inherits (繼承)過(guò)來(lái)的馍忽。 這意味著當(dāng)你編譯和部署了 BabyDoge,它將可以訪問(wèn) catchphrase() 和 anotherCatchphrase()和其他我們?cè)?Doge 中定義的其他公共函數(shù)燕差。
這可以用于邏輯繼承(比如表達(dá)子類的時(shí)候舵匾,Cat 是一種 Animal)。 但也可以簡(jiǎn)單地將類似的邏輯組合到不同的合約中以組織代碼谁不。

在接下來(lái)的章節(jié)中坐梯,我們將要為僵尸實(shí)現(xiàn)各種功能,讓它可以“獵食”和“繁殖”刹帕。 通過(guò)將這些運(yùn)算放到父類 ZombieFactory 中吵血,使得所有 ZombieFactory 的繼承合約都可以使用這些方法。

現(xiàn)在我們?cè)?ZombieFactory 下創(chuàng)建一個(gè)叫 ZombieFeeding 的合約偷溺,它是繼承自 ZombieFactory 合約的蹋辅,代碼如下

pragma solidity ^0.4.19;

contract ZombieFactory {

    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) private {
        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);
        _createZombie(_name, randDna);
    }

}

// Start here
contract ZombieFeeding is ZombieFactory{

}

引入(Import)

兩個(gè)contracts可以放在不同的sol文件中。當(dāng)代碼夠長(zhǎng)的時(shí)候挫掏,我們把它分成多個(gè)文件以便于管理侦另。
在 Solidity 中,當(dāng)你有多個(gè)文件并且想把一個(gè)文件導(dǎo)入另一個(gè)文件時(shí)尉共,可以使用 import 語(yǔ)句:

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

這樣當(dāng)我們?cè)诤霞s(contract)目錄下有一個(gè)名為 someothercontract.sol 的文件( ./ 就是同一目錄的意思)榔昔,它就會(huì)被編譯器導(dǎo)入历涝。


Storage與Memory

在 Solidity 中,有兩個(gè)地方可以存儲(chǔ)變量 —— storage 或 memory。

Storage 變量是指永久存儲(chǔ)在區(qū)塊鏈中的變量褪秀。 Memory 變量則是臨時(shí)的状婶,當(dāng)外部函數(shù)對(duì)某合約調(diào)用完成時(shí)趁窃,memory型變量即被移除。 你可以把它想象成存儲(chǔ)在你電腦的硬盤或是RAM中數(shù)據(jù)的關(guān)系旋廷。

大多數(shù)時(shí)候你都用不到這些關(guān)鍵字,默認(rèn)情況下 Solidity 會(huì)自動(dòng)處理它們礼搁。 狀態(tài)變量(在函數(shù)之外聲明的變量)默認(rèn)為“存儲(chǔ)”形式饶碘,并永久寫入?yún)^(qū)塊鏈;而在函數(shù)內(nèi)部聲明的變量是“內(nèi)存”型的馒吴,它們函數(shù)調(diào)用結(jié)束后消失扎运。

然而也有一些情況下,你需要手動(dòng)聲明存儲(chǔ)類型募书,主要用于處理函數(shù)內(nèi)的 結(jié)構(gòu)體 和 數(shù)組 時(shí):

contract SandwichFactory {
  struct Sandwich {
    string name;
    string status;
  }

  Sandwich[] sandwiches;

  function eatSandwich(uint _index) public {
    // Sandwich mySandwich = sandwiches[_index];

    // ^ 看上去很直接绪囱,不過(guò) Solidity 將會(huì)給出警告
    // 告訴你應(yīng)該明確在這里定義 `storage` 或者 `memory`。

    // 所以你應(yīng)該明確定義 `storage`:
    Sandwich storage mySandwich = sandwiches[_index];
    // ...這樣 `mySandwich` 是指向 `sandwiches[_index]`的指針
    // 在存儲(chǔ)里莹捡,另外...
    mySandwich.status = "Eaten!";
    // ...這將永久把 `sandwiches[_index]` 變?yōu)閰^(qū)塊鏈上的存儲(chǔ)

    // 如果你只想要一個(gè)副本鬼吵,可以使用`memory`:
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // ...這樣 `anotherSandwich` 就僅僅是一個(gè)內(nèi)存里的副本了
    // 另外
    anotherSandwich.status = "Eaten!";
    // ...將僅僅修改臨時(shí)變量,對(duì) `sandwiches[_index + 1]` 沒(méi)有任何影響
    // 不過(guò)你可以這樣做:
    sandwiches[_index + 1] = anotherSandwich;
    // ...如果你想把副本的改動(dòng)保存回區(qū)塊鏈存儲(chǔ)
  }
}

如果你還沒(méi)有完全理解究竟應(yīng)該使用哪一個(gè)篮赢,也不用擔(dān)心 —— 在本教程中齿椅,我們將告訴你何時(shí)使用 storage 或是 memory,并且當(dāng)你不得不使用到這些關(guān)鍵字的時(shí)候启泣,Solidity 編譯器也發(fā)警示提醒你的涣脚。

現(xiàn)在,只要知道在某些場(chǎng)合下也需要你顯式地聲明 storage 或 memory就夠了寥茫!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末遣蚀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子纱耻,更是在濱河造成了極大的恐慌芭梯,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弄喘,死亡現(xiàn)場(chǎng)離奇詭異玖喘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蘑志,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門累奈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人急但,你說(shuō)我怎么就攤上這事澎媒。” “怎么了羊始?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵旱幼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我突委,道長(zhǎng)柏卤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任匀油,我火速辦了婚禮缘缚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘敌蚜。我一直安慰自己桥滨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布弛车。 她就那樣靜靜地躺著齐媒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纷跛。 梳的紋絲不亂的頭發(fā)上喻括,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音贫奠,去河邊找鬼唬血。 笑死,一個(gè)胖子當(dāng)著我的面吹牛唤崭,可吹牛的內(nèi)容都是我干的拷恨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谢肾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼腕侄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起芦疏,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤冕杠,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后眯分,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拌汇,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年弊决,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了噪舀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡飘诗,死狀恐怖与倡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昆稿,我是刑警寧澤纺座,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站溉潭,受9級(jí)特大地震影響净响,放射性物質(zhì)發(fā)生泄漏少欺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一馋贤、第九天 我趴在偏房一處隱蔽的房頂上張望赞别。 院中可真熱鬧,春花似錦配乓、人聲如沸仿滔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)崎页。三九已至,卻和暖如春腰埂,著一層夾襖步出監(jiān)牢的瞬間飒焦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工盐固, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荒给,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓刁卜,卻偏偏與公主長(zhǎng)得像志电,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蛔趴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354