Solidity是以太坊的主要程式語言,它是一種靜態型別的 JavaScript-esque 語言,是面向合約的、為實現智慧合約而建立的高階程式語言,設計的目的是能在以太坊虛擬機器(EVM)上執行。
本文基於CryptoZombies,教程地址為:cryptozombies.io/zh/
合約
Solidity 的程式碼都包裹在合約裡面. 一份合約
就是以太應幣應用的基本模組, 所有的變數和函式都屬於一份合約, 它是你所有應用的起點.
一份名為 HelloWorld
的空合約如下:
contract HelloWorld {
}
複製程式碼
hello world
首先看一個簡單的智慧合約。
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData; // 宣告一個型別為 uint (256位無符號整數)的狀態變數,叫做 storedData
function set(uint x) public {
storedData = x; // 狀態變數可以直接訪問,不需要使用 this. 或者 self. 這樣的字首
}
function get() public view returns (uint) {
return storedData;
}
}
複製程式碼
所有的 Solidity 原始碼都必須冠以 “version pragma” — 標明 Solidity 編譯器的版本. 以避免將來新的編譯器可能破壞你的程式碼。
例如: pragma solidity ^0.4.0;
(當前 Solidity 的最新版本是 0.4.0).
關鍵字
pragma
的含義是,一般來說,pragmas(編譯指令)是告知編譯器如何處理原始碼的指令的(例如, pragma once )。
Solidity中合約的含義就是一組程式碼(它的 函式 )和資料(它的 狀態 ),它們位於以太坊區塊鏈的一個特定地址上。
該合約能完成的事情並不多:它能允許任何人在合約中儲存一個單獨的數字,並且這個數字可以被世界上任何人訪問,且沒有可行的辦法阻止你釋出這個數字。當然,任何人都可以再次呼叫 set
,傳入不同的值,覆蓋你的數字,但是這個數字仍會被儲存在區塊鏈的歷史記錄中。
Solidity 語句以分號(;)結尾
狀態變數
狀態變數是被永久地儲存在合約中。也就是說它們被寫入以太幣區塊鏈中,想象成寫入一個資料庫。
contract HelloWorld {
// 這個無符號整數將會永久的被儲存在區塊鏈中
uint myUnsignedInteger = 100;
}
複製程式碼
在上面的例子中,定義 myUnsignedInteger
為 uint
型別,並賦值100。
uint
無符號資料型別, 指其值不能是負數,對於有符號的整數存在名為int
的資料型別。Solidity中,
uint
實際上是uint256
代名詞, 一個256位的無符號整數。
程式有時需要對不同型別的資料進行操作,因為 Solidity 是靜態型別語言,對不同型別的資料進行運算會丟擲異常,比如:
uint8 a = 5;
uint b = 6;
// 將會丟擲錯誤,因為 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
複製程式碼
a * b
返回型別是 uint
, 但是當我們嘗試用 uint8
型別接收時, 就會造成潛在的錯誤。這時,就需要顯式的進行資料型別轉換:
// 我們需要將 b 轉換為 uint8:
uint8 c = a * uint8(b);
複製程式碼
把它的資料型別轉換為
uint8
, 就可以了,編譯器也不會出錯。
Solidity 支援多種資料型別,比如:
- string(字串):字串用於儲存任意長度的 UTF-8 編碼資料
- fixedArray(靜態陣列):固定長度的陣列
- dynamicArray(動態陣列):長度不固定,可以動態新增元素的陣列
- enum(列舉)
- mapping
- 等
數學運算
在 Solidity 中,數學運算很直觀明瞭,與其它程式設計語言相同:
- 加法:
x + y
- 減法:
x - y
, - 乘法:
x * y
- 除法:
x / y
- 取模 / 求餘:
x % y
(例如, 13 % 5 餘 3, 因為13除以5,餘3) - 乘方:
x ** y
結構體
Solidity 提供了 結構體
,用來表示更復雜的資料型別。
struct Person {
uint age;
string name;
}
複製程式碼
結構體允許你生成一個更復雜的資料型別,它有多個屬性。
建立結構體方式為:
// 建立一個新的Person:
Person satoshi = Person(172, "Satoshi");
複製程式碼
陣列
Solidity 提供兩種型別的陣列:靜態陣列
和動態陣列
。
// 固定長度為2的靜態陣列:
uint[2] fixedArray;
// 固定長度為5的string型別的靜態陣列:
string[5] stringArray;
// 動態陣列,長度不固定,可以動態新增元素:
uint[] dynamicArray;
複製程式碼
使用 push 函式向陣列中新增值:
fixedArray.push[123]
fixedArray.push[234]
// fixedArray 值為 [123, 234]
複製程式碼
array.push()
在陣列的 尾部 加入新元素 ,所以元素在陣列中的順序就是新增的順序
array.push()
會返回陣列的長度。
Solidity 陣列支援多種型別,比如結構體:
struct Person {
uint age;
string name;
}
Person[] people; // dynamic Array, we can keep adding to it
複製程式碼
結構體型別的陣列新增值的方式為:
people.push(Person(16, "Vitalik"));
// 也可以使用下面的方式,推薦使用上述一行簡潔的方式
Person satoshi = Person(172, "Satoshi");
people.push(satoshi);
複製程式碼
公共陣列
也可以使用public
定義公共陣列,Solidity 會自動建立getter
方法。語法如下:
struct Person {
uint age;
string name;
}
Person[] public people; // dynamic Array, we can keep adding to it
複製程式碼
公共陣列支援其它的合約讀取資料(但不能寫入資料),所以這在合約中是一個有用的儲存公共資料的模式。(有點像全域性變數,所有合約共享同一個“記憶體空間“,厲害了!)
函式
Solidity 中,函式定義如下:
function eatHamburgers(string _name, uint _amount) {
}
複製程式碼
Solidity
習慣上函式裡的變數都是以(_)開頭 (但不是硬性規定) 以區別全域性變數。
這是一個名為 eatHamburgers
的函式,它接受兩個引數:一個 string
型別的 和 一個 uint
型別的。現在函式內部還是空的。
函式呼叫如下:
eatHamburgers("vitalik", 100);
複製程式碼
私有/公共函式
Solidity 函式分為私有函式和共有函式。
Solidity 定義的函式的屬性預設為
公共
。 這就意味著任何一方 (或其它合約) 都可以呼叫你合約裡的函式。
顯然,不是什麼時候都需要這樣,而且這樣的合約易於受到攻擊。所以將自己的函式定義為私有
是一個好的程式設計習慣,只有當你需要外部世界呼叫它時才將它設定為公共
。
可以把所有的函式都顯式的宣告
public
和private
來規避這個問題。
定義私有函式比較簡單,只需要在函式引數後新增 private
關鍵字即可。示例如下:
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
複製程式碼
這意味著只有我們合約中的其它函式才能夠呼叫這個函式,給 numbers
陣列新增新成員。
和函式的引數類似,私有函式的名字用(
_
)起始。
注意:
在智慧合約中你所用的一切都是公開可見的,即便是區域性變數和被標記成private
的狀態變數也是如此。
返回值
和其它語言一樣,Solidity 函式也有返回值,示例如下:
string greeting = "What`s up dog";
function sayHello() public returns (string) {
return greeting;
}
複製程式碼
返回值使用 returns
關鍵字標註。(已經是非常奇怪的寫法了。。)
修飾符
view
constant
是view
的別名
string greeting = "What`s up dog";
function sayHello() public returns (string) {
return greeting;
}
複製程式碼
像 sayHello
函式這種實際上沒有改變合約中資料內容的情況,可以把函式定義為view
,這意味著此函式只讀不修改資料。可以使用以下宣告方式:
function sayHello() public view returns (string) {}
複製程式碼
可以將函式宣告為 view
型別,這種情況下要保證不修改狀態。
下面的語句被認為是修改狀態:
- 修改狀態變數。
- 產生事件。
- 建立其它合約。
- 使用
selfdestruct
。 - 通過呼叫傳送以太幣。
- 呼叫任何沒有標記為
view
或者pure
的函式。 - 使用低階呼叫。
- 使用包含特定操作碼的內聯彙編。
pure
pure 比 view 更輕量,使用這個修飾符修飾的函式甚至都不會讀取合約中的資料,例如:
function _multiply(uint a, uint b) private pure returns (uint) { return a * b; }
複製程式碼
這個函式沒有讀取應用裡的狀態,它的返回值只和它輸入的引數相關。
Solidity 編輯器會給出提示,提醒你使用 pure/view修飾符。
函式可以宣告為 pure
,在這種情況下,承諾不讀取或修改狀態。
除了上面解釋的狀態修改語句列表之外,以下被認為是從狀態中讀取:
- 讀取狀態變數。
- 訪問
this.balance
或者<address>.balance
。 - 訪問
block
,tx
,msg
中任意成員 (除msg.sig
和msg.data
之外)。 - 呼叫任何未標記為
pure
的函式。 - 使用包含某些操作碼的內聯彙編。
payable
payable 關鍵字用來說明,這個函式可以接受以太幣,如果沒有這個關鍵字,函式會自動拒絕所有傳送給它的以太幣。
事件
事件 是合約和區塊鏈通訊的一種機制。你的前端應用“監聽”某些事件,並做出反應。例如:
// 這裡建立事件
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
//觸發事件,通知app
IntegersAdded(_x, _y, result);
return result;
}
複製程式碼
使用者介面(當然也包括伺服器應用程式)可以監聽區塊鏈上正在傳送的事件,而不會花費太多成本。一旦它被髮出,監聽該事件的listener都將收到通知。而所有的事件都包含了 from
, to
和 amount
三個引數,可方便追蹤事務。 為了監聽這個事件,你可以使用如下程式碼(javascript 實現):
var abi = /* abi 由編譯器產生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* 地址 */);
var event = clientReceipt.IntegersAdded();
// 監視變化
event.watch(function(error, result){
// 結果包括對 `Deposit` 的呼叫引數在內的各種資訊。
if (!error)
console.log(result);
});
// 或者通過回撥立即開始觀察
var event = clientReceipt.IntegersAdded(function(error, result) {
if (!error)
console.log(result);
});
複製程式碼
程式碼示例
下面是一個完整的程式碼示例:
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; // 定義動態陣列
// 建立私有函式,私有函式命名使用 _ 字首
function _createZombie(string _name, uint _dna) private {
// 函式引數命名 使用 _ 作為字首
// arrays.push() 將元素加入到陣列尾部,並且返回陣列的長度
uint id = zombies.push(Zombie(_name, _dna)) - 1;
// 觸發事件
NewZombie(id, _name, _dna);
}
// view 為函式修飾符,表示此函式不需要更新或建立狀態變數
// pure 表示函式不需要使用狀態變數
function _generateRandomDna(string _str) private view returns (uint) {
// 使用 keccak256 建立一個偽隨機數
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
複製程式碼
Ethereum 內部有一個雜湊函式keccak256,它用了SHA3版本。一個雜湊函式基本上就是把一個字串轉換為一個256位的16進位制數字。
在智慧合約中使用隨機數很難保證節點不作弊, 這是因為智慧合約中的隨機數一般要依賴計算節點的本地時間得到, 而本地時間是可以被惡意節點偽造的,因此這種方法並不安全。 通行的做法是採用 鏈外off-chain 的第三方服務,比如 Oraclize 來獲取隨機數)。
參考連結
- Solidity 文件: https://solidity-cn.readthedocs.io/zh/develop/index.html
- cryptozombie-lessons: https://cryptozombies.io/zh/
最後,感謝女朋友支援和包容,比❤️
也可以在公號輸入以下關鍵字獲取歷史文章:公號&小程式
| 設計模式
| 併發&協程