Solidity是以太坊的主要編程語言,它是一種靜態類型的 JavaScript-esque 語言,是面向合約的、爲實現智能合約而建立的高級編程語言,設計的目的是能在以太坊虛擬機(EVM)上運行。javascript
本文基於CryptoZombies,教程地址爲:https://cryptozombies.io/zh/lesson/2
html
以太坊區塊鏈由 account (帳戶)組成,你能夠把它想象成銀行帳戶。一個賬戶的餘額是以太 (在以太坊區塊鏈上使用的幣種),你能夠和其餘賬戶之間支付和接受以太幣,就像你的銀行賬戶能夠電匯資金到其餘銀行賬戶同樣。java
每一個賬戶都有一個「地址」,你能夠把它想象成銀行帳號。這是帳戶惟一的標識符,它看起來長這樣:python
0x0cE446255506E92DF41614C46F1d6df9Cc969183
複製代碼
這是 CryptoZombies 團隊的地址,爲了表示支持CryptoZombies,能夠讚揚一些以太幣!git
address
:地址類型存儲一個 20 字節的值(以太坊地址的大小)。 地址類型也有成員變量,並做爲全部合約的基礎。web
address
類型是一個160位的值,且不容許任何算數操做。這種類型適合存儲合約地址或外部人員的密鑰對。編程
Mappings 和哈希表相似,它會執行虛擬初始化,以使全部可能存在的鍵都映射到一個字節表示爲全零的值。小程序
映射是這樣定義的:設計模式
//對於金融應用程序,將用戶的餘額保存在一個 uint類型的變量中:
mapping (address => uint) public accountBalance;
//或者能夠用來經過userId 存儲/查找的用戶名
mapping (uint => string) userIdToName;
複製代碼
映射本質上是存儲和查找數據所用的鍵-值對。在第一個例子中,鍵是一個 address,值是一個 uint,在第二個例子中,鍵是一個uint,值是一個 string。api
映射類型在聲明時的形式爲 mapping(_KeyType => _ValueType)。 其中 _KeyType 能夠是除了映射、變長數組、合約、枚舉以及結構體之外的幾乎全部類型。 _ValueType 能夠是包括映射類型在內的任何類型。
對映射的取值操做以下:
userIdToName[12]
// 若是鍵12 不在 映射中,獲得的結果是0
複製代碼
映射中,實際上並不存儲 key,而是存儲它的 keccak256 哈希值,從而便於查詢實際的值。因此映射是沒有長度的,也沒有 key 的集合或 value 的集合的概念。,你不能像操做
python
字典那應該獲取到當前 Mappings 的全部鍵或者值。
在 Solidity 中,在全局命名空間中已經存在了(預設了)一些特殊的變量和函數,他們主要用來提供關於區塊鏈的信息或一些通用的工具函數。
msg.sender指的是當前調用者(或智能合約)的 address。
注意:在 Solidity 中,功能執行始終須要從外部調用者開始。 一個合約只會在區塊鏈上什麼也不作,除非有人調用其中的函數。因此對於每個外部函數調用,包括 msg.sender 和 msg.value 在內全部 msg 成員的值都會變化。這裏包括對庫函數的調用。
如下是使用 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 很安全,由於它具備以太坊區塊鏈的安全保障 —— 除非竊取與以太坊地址相關聯的私鑰,不然是沒有辦法修改其餘人的數據的。
如下是其它的一些特殊變量。
Solidity 使用狀態恢復異常來處理錯誤。這種異常將撤消對當前調用(及其全部子調用)中的狀態所作的全部更改,而且還向調用者標記錯誤。
函數 assert
和 require
可用於檢查條件並在條件不知足時拋出異常。
這裏主要介紹 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 中,關鍵詞放置的順序並不重要
// 如下兩個語句等效
require(keccak256(_name) == keccak256("Vitalik"));
require(keccak256("Vitalik") == keccak256(_name));
複製代碼
除 public 和 private 屬性以外,Solidity 還使用了另外兩個描述函數可見性的修飾詞:internal(內部) 和 external(外部)。
internal
和 private
相似,不過,若是某個合約繼承自其父合約,這個合約便可以訪問父合約中定義的「內部(internal)」函數
。
external
與public
相似,只不過external
函數只能在合約以外調用 - 它們不能被合約內的其餘函數調用。
聲明函數 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();
}
}
複製代碼
Solidity 有兩種函數調用(內部調用不會產生實際的 EVM 調用或稱爲消息調用
,而外部調用則會產生一個 EVM 調用), 函數和狀態變量有四種可見性類型。 函數能夠指定爲 external ,public ,internal 或者 private,默認狀況下函數類型爲 public。 對於狀態變量,不能設置爲 external ,默認是 internal 。
external :
外部函數做爲合約接口的一部分,意味着咱們能夠從其餘合約和交易中調用。 一個外部函數 f 不能從內部調用(即 f 不起做用,但 this.f() 能夠)。 當收到大量數據的時候,外部函數有時候會更有效率。
public :
public 函數是合約接口的一部分,能夠在內部或經過消息調用。對於公共狀態變量, 會自動生成一個 getter 函數。
internal :
這些函數和狀態變量只能是內部訪問(即從當前合約內部或從它派生的合約訪問),不使用 this 調用。
private :
private 函數和狀態變量僅在當前定義它們的合約中使用,而且不能被派生合約使用。
合約中的全部內容對外部觀察者都是可見的。設置一些 private 類型只能阻止其餘合約訪問和修改這些信息, 可是對於區塊鏈外的整個世界它仍然是可見的。
可見性標識符的定義位置,對於狀態變量來講是在類型後面,對於函數是在參數列表和返回關鍵字中間。
pragma solidity ^0.4.16;
contract C {
// 對於函數是在參數列表和返回關鍵字中間。
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data; // 對於狀態變量來講是在類型後面
}
複製代碼
和 python 相似,Solidity 函數支持多值返回,好比:
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();
}
複製代碼
這裏留空字段使用
,
的方式太不直觀了,還不如 python/go 使用下劃線_
代替無用字段。
在 Solidity 中,有兩個地方能夠存儲變量 —— storage 或 memory。
Storage 變量是指永久存儲在區塊鏈中的變量。 Memory 變量則是臨時的,當外部函數對某合約調用完成時,內存型變量即被移除。 你能夠把它想象成存儲在你電腦的硬盤或是RAM中數據的關係。
storage 和 memory 放到狀態變量名前邊,在類型後邊,格式以下:
變量類型 <storage|memory> 變量名
大多數時候都用不到這些關鍵字,默認狀況下 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,而且當你不得不使用到這些關鍵字的時候,Solidity 編譯器也發警示提醒你的。
如今,只要知道在某些場合下也須要你顯式地聲明 storage 或 memory就夠了!
Solidity 的繼承和 Python 的繼承類似,支持多重繼承。 看下面這個例子:
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
// 能夠多重繼承。請注意,Doge 也是 BabyDoge 的基類,
// 但只有一個 Doge 實例(就像 C++ 中的虛擬繼承)。
contract BlackBabyDoge is Doge, BabyDoge {
function color() public returns (string) {
return "Black";
}
}
複製代碼
BabyDoge
從 Doge
那裏 inherits(繼承)
過來。 這意味着當編譯和部署了 BabyDoge
,它將能夠訪問 catchphrase() 和 anotherCatchphrase()和其餘咱們在 Doge 中定義的其餘公共函數(private 函數不可訪問)。
Solidity使用 is 從另外一個合約派生。派生合約能夠訪問全部非私有成員,包括內部函數和狀態變量,但沒法經過 this
來外部訪問。
派生合約須要提供基類構造函數須要的全部參數。這能夠經過兩種方式來完成:
pragma solidity ^0.4.0;
contract Base {
uint x;
// 這是註冊 Base 和設置名稱的構造函數。
function Base(uint _x) public { x = _x; }
}
contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) public {
}
}
contract Derived1 is Base {
function Derived1(uint _y) Base(_y * _y) public {
}
}
複製代碼
一種方法直接在繼承列表中調用基類構造函數(is Base(7)
)。 另外一種方法是像 修飾器 modifier
使用方法同樣, 做爲派生合約構造函數定義頭的一部分,(Base(_y * _y)
)。 若是構造函數參數是常量而且定義或描述了合約的行爲,使用第一種方法比較方便。 若是基類構造函數的參數依賴於派生合約,那麼必須使用第二種方法。 若是像這個簡單的例子同樣,兩個地方都用到了,優先使用 修飾器modifier 風格的參數。
合約函數能夠缺乏實現,以下例所示(請注意函數聲明頭由 ; 結尾):
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32); } 複製代碼
這些合約沒法成功編譯(即便它們除了未實現的函數還包含其餘已經實現了的函數),但他們能夠用做基類合約:
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32); } contract Cat is Feline {
function utterance() public returns (bytes32) { return "miaow"; }
}
複製代碼
若是合約繼承自抽象合約,而且沒有經過重寫來實現全部未實現的函數,那麼它自己就是抽象的。
接口相似於抽象合約,可是它們不能實現任何函數。還有進一步的限制:
首先,看一下一個interface的例子:
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
(外部),它們就能夠與以太坊區塊鏈上的任何其餘合約進行交互。
若是一個合約須要和區塊鏈上的其餘的合約會話,則需先定義一個 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); } 複製代碼
使用這個接口,合約就知道其餘合約的函數是怎樣的,應該如何調用,以及可期待什麼類型的返回值。
下面是一個示例代碼,會用到上邊的知識點:
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;
// 建立一個叫作 zombieToOwner 的映射。其鍵是一個uint,值爲 address。映射屬性爲public
mapping (uint => address) public zombieToOwner;
// 建立一個名爲 ownerZombieCount 的映射,其中鍵是 address,值是 uint
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 來確保這個函數只有在每一個用戶第一次調用它的時候執行,用以建立初始殭屍
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
// CryptoKitties 合約提供了getKitty 函數,它返回全部的加密貓的數據,包括它的「基因」(殭屍遊戲要用它生成新的殭屍)。
// 一個獲取 kitty 的接口
contract KittyInterface {
// 在interface裏定義了 getKitty 函數 在 returns 語句以後用分號
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 ); } //ZombieFeeding繼承自 `ZombieFactory 合約 contract ZombieFeeding is ZombieFactory {
// CryptoKitties 合約的地址
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
// 建立一個名爲 kittyContract 的 KittyInterface,並用 ckAddress 爲它初始化
KittyInterface kittyContract = KittyInterface(ckAddress);
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) public {
// 確保對本身殭屍的全部權
require(msg.sender == zombieToOwner[_zombieId]);
// 聲明一個名爲 myZombie 數據類型爲Zombie的 storage 類型本地變量
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
// Add an if statement here
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");
}
}
複製代碼
這段代碼看起來內容有點多,能夠拆分一下,把
ZombieFactory
代碼提取到一個新的文件zombiefactory.sol
,如今就可使用 import 語句來導入另外一個文件的代碼。
在 Solidity 中,當你有多個文件而且想把一個文件導入另外一個文件時,可使用 import 語句:
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
複製代碼
這樣當咱們在合約(contract)目錄下有一個名爲 someothercontract.sol 的文件( ./ 就是同一目錄的意思),它就會被編譯器導入。
這一點和 go 相似,在同一目錄下文件中的內容能夠直接使用,而不用使用 xxx.name 的形式。
編譯和部署 ZombieFeeding,就能夠將這個合約部署到以太坊了。最終完成的這個合約繼承自 ZombieFactory,所以它能夠訪問本身和父輩合約中的全部 public 方法。
下面是一個與ZombieFeeding合約進行交互的例子, 這個例子使用了 JavaScript 和 web3.js:
var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)
// 假設咱們有咱們的殭屍ID和要攻擊的貓咪ID
let zombieId = 1;
let kittyId = 1;
// 要拿到貓咪的DNA,咱們須要調用它的API。這些數據保存在它們的服務器上而不是區塊鏈上。
// 若是一切都在區塊鏈上,咱們就不用擔憂它們的服務器掛了,或者它們修改了API,
// 或者由於不喜歡咱們的殭屍遊戲而封殺了咱們
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
let imgUrl = data.image_url
// 一些顯示圖片的代碼
})
// 當用戶點擊一隻貓咪的時候:
$(".kittyImage").click(function(e) {
// 調用咱們合約的 `feedOnKitty` 函數
ZombieFeeding.feedOnKitty(zombieId, kittyId)
})
// 偵聽來自咱們合約的新殭屍事件好來處理
ZombieFactory.NewZombie(function(error, result) {
if (error) return
// 這個函數用來顯示殭屍:
generateZombie(result.zombieId, result.name, result.dna)
})
複製代碼
最後,感謝女友支持和包容,比❤️
也能夠在公號輸入如下關鍵字獲取歷史文章:公號&小程序
| 設計模式
| 併發&協程