映射(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就夠了寥茫!