學習來源https://cryptozombies.io/zh/lesson/2
第1章: 映射(Mapping)和地址(Address)
我們通過給數據庫中的殭屍指定“主人”, 來支持“多玩家”模式。
如此一來,我們需要引入2個新的數據類型:mapping(映射) 和 address(地址)。
Addresses (地址)
以太坊區塊鏈由 _ account _ (賬戶)組成,你可以把它想象成銀行賬戶。一個帳戶的餘額是 以太 (在以太坊區塊鏈上使用的幣種),你可以和其他帳戶之間支付和接受以太幣,就像你的銀行帳戶可以電匯資金到其他銀行帳戶一樣。
每個帳戶都有一個“地址”,你可以把它想象成銀行賬號。這是賬戶唯一的標識符,它看起來長這樣:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
(這是 CryptoZombies 團隊的地址,如果你喜歡 CryptoZombies 的話,請打賞我們一些以太幣!😉)
我們將在後面的課程中介紹地址的細節,現在你只需要瞭解地址屬於特定用戶(或智能合約)的。
所以我們可以指定“地址”作爲殭屍主人的 ID。當用戶通過與我們的應用程序交互來創建新的殭屍時,新殭屍的所有權被設置到調用者的以太坊地址下。
Mapping(映射)
在第1課中,我們看到了 _ 結構體 _ 和 _ 數組 _ 。 映射 是另一種在 Solidity 中存儲有組織數據的方法。
映射是這樣定義的:
//對於金融應用程序,將用戶的餘額保存在一個 uint類型的變量中:
mapping (address => uint) public accountBalance;
//或者可以用來通過userId 存儲/查找的用戶名
mapping (uint => string) userIdToName;
映射本質上是存儲和查找數據所用的鍵-值對。在第一個例子中,鍵是一個 address,值是一個 uint,在第二個例子中,鍵是一個uint,值是一個 string。
第2章: Msg.sender
現在有了一套映射來記錄殭屍的所有權了,我們可以修改 _createZombie 方法來運用它們。
爲了做到這一點,我們要用到 msg.sender。
msg.sender
在 Solidity 中,有一些全局變量可以被所有函數調用。 其中一個就是msg.sender,它指的是當前調用者(或智能合約)的 address。
注意:在 Solidity 中,功能執行始終需要從外部調用者開始。 一個合約只會在區塊鏈上什麼也不做,除非有人調用其中的函數。所以 msg.sender總是存在的。
以下是使用 msg.sender 來更新 mapping 的例子:
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// 更新我們的 `favoriteNumber` 映射來將 `_myNumber`存儲在 `msg.sender`名下
favoriteNumber[msg.sender] = _myNumber;
// 存儲數據至映射的方法和將數據存儲在數組相似
}
function whatIsMyNumber() public view returns (uint) {
// 拿到存儲在調用者地址名下的值
// 若調用者還沒調用 setMyNumber, 則值爲 `0`
return favoriteNumber[msg.sender];
}
在這個小小的例子中,任何人都可以調用 setMyNumber 在我們的合約中存下一個 uint 並且與他們的地址相綁定。 然後,他們調用 whatIsMyNumber 就會返回他們存儲的 uint。
使用 msg.sender 很安全,因爲它具有以太坊區塊鏈的安全保障 —— 除非竊取與以太坊地址相關聯的私鑰,否則是沒有辦法修改其他人的數據的。
第3章: Require
在第一課中,我們成功讓用戶通過調用 createRandomZombie函數 並輸入一個名字來創建新的殭屍。 但是,如果用戶能持續調用這個函數來創建出無限多個殭屍加入他們的軍團,這遊戲就太沒意思了!
於是,我們作出限定:每個玩家只能調用一次這個函數。 這樣一來,新玩家可以在剛開始玩遊戲時通過調用它,爲其軍團創建初始殭屍。
我們怎樣才能限定每個玩家只調用一次這個函數呢?
答案是使用require。 require使得函數在執行過程中,當不滿足某些條件時拋出錯誤,並停止執行:
function sayHiToVitalik(string _name) public returns (string) {
// 比較 _name 是否等於 "Vitalik". 如果不成立,拋出異常並終止程序
// (敲黑板: Solidity 並不支持原生的字符串比較, 我們只能通過比較
// 兩字符串的 keccak256 哈希值來進行判斷)
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 運行如下語句
return "Hi!";
}
如果你這樣調用函數 sayHiToVitalik(“Vitalik”) ,它會返回“Hi!”。而如果調用的時候使用了其他參數,它則會拋出錯誤並停止執行。
因此,在調用一個函數之前,用 require 驗證前置條件是非常有必要的。
注意:在 Solidity 中,關鍵詞放置的順序並不重要
第4章: 繼承(Inheritance)
我們的遊戲代碼越來越長。 當代碼過於冗長的時候,最好將代碼和邏輯分拆到多個不同的合約中,以便於管理。
有個讓 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 (繼承)過來的。 這意味着當你編譯和部署了 BabyDoge,它將可以訪問 catchphrase() 和 anotherCatchphrase()和其他我們在 Doge 中定義的其他公共函數。
這可以用於邏輯繼承(比如表達子類的時候,Cat 是一種 Animal)。 但也可以簡單地將類似的邏輯組合到不同的合約中以組織代碼。
第5章: 引入(Import)
代碼已經夠長了,我們把它分成多個文件以便於管理。 通常情況下,當 Solidity 項目中的代碼太長的時候我們就是這麼做的。
在 Solidity 中,當你有多個文件並且想把一個文件導入另一個文件時,可以使用 import 語句:
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
這樣當我們在合約(contract)目錄下有一個名爲 someothercontract.sol 的文件( ./ 就是同一目錄的意思),它就會被編譯器導入。
第6章: Storage與Memory
在 Solidity 中,有兩個地方可以存儲變量 —— storage 或 memory。
Storage 變量是指永久存儲在區塊鏈中的變量。 Memory 變量則是臨時的,當外部函數對某合約調用完成時,內存型變量即被移除。 你可以把它想象成存儲在你電腦的硬盤或是RAM中數據的關係。
大多數時候你都用不到這些關鍵字,默認情況下 Solidity 會自動處理它們。 狀態變量(在函數之外聲明的變量)默認爲“存儲”形式,並永久寫入區塊鏈;而在函數內部聲明的變量是“內存”型的,它們函數調用結束後消失。
然而也有一些情況下,你需要手動聲明存儲類型,主要用於處理函數內的 _ 結構體 _ 和 _ 數組 _ 時:
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ 看上去很直接,不過 Solidity 將會給出警告
// 告訴你應該明確在這裏定義 `storage` 或者 `memory`。
// 所以你應該明確定義 `storage`:
Sandwich storage mySandwich = sandwiches[_index];
// ...這樣 `mySandwich` 是指向 `sandwiches[_index]`的指針
// 在存儲裏,另外...
mySandwich.status = "Eaten!";
// ...這將永久把 `sandwiches[_index]` 變爲區塊鏈上的存儲
// 如果你只想要一個副本,可以使用`memory`:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// ...這樣 `anotherSandwich` 就僅僅是一個內存裏的副本了
// 另外
anotherSandwich.status = "Eaten!";
// ...將僅僅修改臨時變量,對 `sandwiches[_index + 1]` 沒有任何影響
// 不過你可以這樣做:
sandwiches[_index + 1] = anotherSandwich;
// ...如果你想把副本的改動保存回區塊鏈存儲
}
}
我:這裏是說storage是傳址,而memory是傳值
如果你還沒有完全理解究竟應該使用哪一個,也不用擔心 —— 在本教程中,我們將告訴你何時使用 storage 或是 memory,並且當你不得不使用到這些關鍵字的時候,Solidity 編譯器也發警示提醒你的。
現在,只要知道在某些場合下也需要你顯式地聲明 storage 或 memory就夠了!
第7章: 更多關於函數可見性(internal、external)
我們上一課的代碼有問題!
編譯的時候編譯器就會報錯。
錯誤在於,我們嘗試從 ZombieFeeding 中調用 _createZombie 函數,但 _createZombie 卻是 ZombieFactory 的 private (私有)函數。這意味着任何繼承自 ZombieFactory 的子合約都不能訪問它。
internal 和 external
除 public 和 private 屬性之外,Solidity 還使用了另外兩個描述函數可見性的修飾詞:internal(內部) 和 external(外部)。
internal 和 private 類似,不過, 如果某個合約繼承自其父合約,這個合約即可以訪問父合約中定義的“內部”函數。(嘿,這聽起來正是我們想要的那樣!)。
external 與public 類似,只不過這些函數只能在合約之外調用 - 它們不能被合約內的其他函數調用。 稍後我們將討論什麼時候使用 external 和 public。
我:private<internal<external<public
聲明函數 internal 或 external 類型的語法,與聲明 private 和 public類 型相同:
contract Sandwich {
uint private sandwichesEaten = 0;
function eat() internal {
sandwichesEaten++;
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string) {
baconSandwichesEaten++;
// 因爲eat() 是internal 的,所以我們能在這裏調用
eat();
}
}
第8章: 定義接口Interface
與其他合約的交互
如果我們的合約需要和區塊鏈上的其他的合約會話,則需先定義一個 interface (接口)。
先舉一個簡單的栗子。 假設在區塊鏈上有這麼一個合約:
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
這是個很簡單的合約,您可以用它存儲自己的幸運號碼,並將其與您的以太坊地址關聯。 這樣其他人就可以通過您的地址查找您的幸運號碼了。
現在假設我們有一個外部合約,使用 getNum 函數可讀取其中的數據。
首先,我們定義 LuckyNumber 合約的 interface :
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
請注意,這個過程雖然看起來像在定義一個合約,但其實內裏不同:
首先,我們只聲明瞭要與之交互的函數 —— 在本例中爲 getNum —— 在其中我們沒有使用到任何其他的函數或狀態變量。
其次,我們並沒有使用大括號({ 和 })定義函數體,我們單單用分號(;)結束了函數聲明。這使它看起來像一個合約框架。
編譯器就是靠這些特徵認出它是一個接口的。
在我們的 app 代碼中使用這個接口,合約就知道其他合約的函數是怎樣的,應該如何調用,以及可期待什麼類型的返回值。
在下一課中,我們將真正調用其他合約的函數。目前我們只要聲明一個接口,用於調用 CryptoKitties 合約就行了。
第9章: 使用接口
繼續前面 NumberInterface 的例子,我們既然將接口定義爲:
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
我們可以在合約中這樣使用:
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// ^ 這是FavoriteNumber合約在以太坊上的地址
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 現在變量 `numberContract` 指向另一個合約對象
function someFunction() public {
// 現在我們可以調用在那個合約中聲明的 `getNum`函數:
uint num = numberContract.getNum(msg.sender);
// ...在這兒使用 `num`變量做些什麼
}
}
通過這種方式,只要將您合約的可見性設置爲public(公共)或external(外部),它們就可以與以太坊區塊鏈上的任何其他合約進行交互。
第10章: 處理多返回值
getKitty 是我們所看到的第一個返回多個值的函數。我們來看看是如何處理的:
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// 這樣來做批量賦值:
(a, b, c) = multipleReturns();
}
// 或者如果我們只想返回其中一個變量:
function getLastReturnValue() external {
uint c;
// 可以對其他字段留空:
(,,c) = multipleReturns();
}
第11章: if語句
if語句的語法在 Solidity 中,與在 JavaScript 中差不多:
function eatBLT(string sandwich) public {
// 看清楚了,當我們比較字符串的時候,需要比較他們的 keccak256 哈希碼
if (keccak256(sandwich) == keccak256("BLT")) {
eat();
}
}