solidity教程(一)基礎講解
title: solidity教程(一)基礎 tags: solidity,區塊鏈
第1章: 課程概述
第一課你將創造一個"殭屍工廠", 用它建立一支殭屍部隊。
- 我們的工廠會把我們部隊中所有的殭屍儲存到資料庫中
- 工廠會有一個函式能產生新的殭屍
- 每個殭屍會有一個隨機的獨一無二的面孔 在後面的課程裡,我們會增加功能。比如,讓殭屍能攻擊人類或其它殭屍! 但是在實現這些好玩的功能之前,我們先要實現建立殭屍這樣的基本功能。
殭屍DNA如何運作 殭屍的面孔取決於它的DNA。它的DNA很簡單,由一個16位的整陣列成:
8356281049284737
如同真正的DNA, 這個數字的不同部分會對應不同的特點。 前2位代表頭型,緊接著的2位代表眼睛,等等。
注: 本教程我們儘量簡化。我們的殭屍只有7種頭型(雖然2位數字允許100種可能性)。以後我們會加入更多的頭型, 如果我們想讓殭屍有更多造型。
例如,前兩位數字是 83, 計算殭屍的頭型,我們做83 % 7 + 1 = 7 運算, 此殭屍將被賦予第七類頭型。
第2章: 合約
將Solidity我們從最基本的合約開始入手,Solidity 的程式碼都包裹在contract(合約)裡面. 一份合約就是以太應幣應用的基本模組, 所有的變數和函式都屬於一份合約, 它是你所有應用的起點.它相當於JAVA或者C++中的類。第一個Solidity專案,當然是最經典的HelloWord。
一份名為 HelloWorld 的空合約如下:
contract HelloWorld {
}
注:我們的程式碼都是用remix 執行
版本指令 所有的 Solidity 原始碼都必須宣告版本,就如go語言必要package “xx” 指定包一樣。 例如: pragma solidity ^0.4.19; (當前 Solidity 的最新版本是 0.4.19). 綜上所述, 下面就是一個最基本的合約:
pragma solidity ^0.4.19;
contract HelloWorld {
}
實戰演習
為了建立我們的殭屍部隊, 讓我們先建立一個基礎合約,稱為 ZombieFactory。 1.指定版本為0.4.19 2.建立一個空合約 ZombieFactory。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
}
第3章: 狀態變數
我們已經實現了一個空合約, 下面學習 Solidity 中如何使用變數。 狀態變數是被永久地儲存在合約中。也就是說它們被寫入以太幣區塊鏈中. 想象成寫入一個數據庫。
Solidity的變數型別:
- 值型別 :這類變數在賦值或傳參時,總是進行值拷貝。
- 布林型別(Booleans)
- 整型(Integers)
- 定長浮點型(Fixed Point Numbers)
- 定長位元組陣列(Fixed-size byte arrays)
- 有理數和整型常量(Rational and Integer Literals)
- 字串常量(String literals)
- 十六進位制常量(Hexadecimal literals)
- 列舉(Enums)
- 函式型別(Function Types)
- 地址型別(Address)
- 地址常量(Address Literals)
- 函式型別及地址型別(Address)
- 引用型別 :這裡變數在賦值或傳參時,傳遞的是地址。
- 陣列(Arrays)
- 結構體(Structs)
雖然列舉了很多,但是本文涉及計部分簡單型別。所以有不懂的小夥伴們不要害怕,以後會慢慢深入講解的。
例子:
contract Example {
// 這個無符號整數將會永久的被儲存在區塊鏈中
string name ="Jay";
uint age = 25;
}
在上面的例子中,定義name為string型別,並賦值為“Jay”和age 為 uint 型別,並賦值25。
字串型別:string string字串用於儲存任意長度的 UTF-8 編碼資料。 如: string greeting = “Hello world!”。 無符號整數: uint uint 無符號資料型別, 指其值不能是負數,對於有符號的整數存在名為 int 的資料型別。
注: Solidity中, uint 實際上是 uint256代名詞, 一個256位的無符號整數。你也可以定義位數少的uints — uint8, uint16, uint32 等,但一般來講我們都使用簡單的 uint。如果使用到其他的我後面會講。 儲存字元我們也可以選擇使用bytes型別,這個型別我們以後會講。
實戰演習
我們的殭屍DNA將由一個十六位數字組成。
- 定義 dnaDigits 為 uint 資料型別, 並賦值 16。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
//插入程式碼
uint dnaDigits = 16;
}
注意: 變數直接宣告在合約中,是不可直接變更的。
第4章: 數學運算
在 Solidity 中,數學運算很直觀明瞭,與其它程式設計語言相同:
- 加法: x + y
- 減法: x - y,
- 乘法: x * y
- 除法: x / y
- 取模 / 求餘: x % y (例如, 13 % 5 餘 3, 因為13除以5,餘3)
- 次方:x ** y (x 的 y次方 // 例如: 5 ** 2 = 25)
uint x = 5 ** 2; // equal to 5^2 = 25
實戰演習
為了保證我們的殭屍的DNA只含有16個字元,我們先造一個uint資料,讓它等於10^16。這樣一來以後我們可以用模運算子 % 把一個整數變成16位。
- 建立一個uint型別的變數,名字叫dnaModulus, 令其等於 10 的 dnaDigits 次方.
答案: pragma solidity ^0.4.19;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
}
第5章: 結構體
有時我們需要更復雜的資料型別,比如說殭屍名字、DNA等屬性,有沒有辦法把它們集合到一起,變為一個數據型別呢?Solidity 提供的結構體可以幫我們實現: struct Person { uint age; string name; } 結構體允許你生成一個更復雜的資料型別,它有多個屬性。顯然它跟符號我們建立勇士的要求。
實戰演習
在我們的程式中,我們將建立一些殭屍!每個殭屍將擁有多個屬性,所以這是一個展示結構體的完美例子。
1.建立一個struct 命名為 Zombie.
2.我們的 Zombie 結構體有兩個屬性: name (型別為 string), 和 dna (型別為 uint)
pragma solidity ^0.4.19;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
//插入結構體
struct Zombie {
string name;
uint dna;
}
}
第6章: 陣列
在前面我們說了是要建立一支殭屍隊伍,顯然我們之前沒有達到我們都目標。如果你想建立一個集合,可以用 陣列(array)這樣的資料型別. Solidity 支援兩種陣列: 靜態陣列和動態陣列:
// 固定長度為2的靜態陣列: uint[2] fixedArray; // 固定長度為5的string型別的靜態陣列: string[5] stringArray; // 動態陣列,長度不固定,可以動態新增元素: uint[] dynamicArray; 你也可以建立一個 結構體型別的陣列 例如,上一章提到的 Person:
Person[] people; // dynamic Array, we can keep adding to it 記住:狀態變數被永久儲存在區塊鏈中。所以在你的合約中建立動態陣列來儲存成結構的資料是非常有意義的。
公共陣列 你可以定義 public 陣列, Solidity 會自動建立 getter 方法. 語法如下:
Person[] public people;
其它的合約可以從這個陣列讀取資料(但不能寫入資料),所以這在合約中是一個有用的儲存公共資料的模式。
實戰演習
為了把一個殭屍部隊儲存在我們的APP裡,並且能夠讓其它APP看到這些殭屍,我們需要一個公共陣列。
- 建立一個數據型別為 Zombie 的結構體陣列,用 public 修飾,命名為:zombies.
答案: 注意: 為了文章的簡潔性,我用“…” 代表前面出現過的程式碼。
pragma solidity ^0.4.19;
contract ZombieFactory {
...
struct Zombie {
string name;
uint dna;
}
//這裡開始
Zombie[] public zombies;
}
第7章: 定義函式
我們每天需要吃飯、睡覺等等一系列活動,而這一系列的活動在程式中叫做方法,或者函式,下面我們定一個吃漢堡的方法(函式)。 在 Solidity 中函式定義的句法如下: function eatHamburgers(string _name, uint _amount) {
} 這是一個名為 eatHamburgers 的函式,它接受兩個引數:一個 string型別的 和 一個 uint型別的。_name 代表著漢堡的品種,_amount代表我們吃的數量。現在函式內部還是空的。
注:: 習慣上函式裡的變數都是以“_”開頭 (但不是硬性規定) 以區別全域性變數。我們整個教程都會沿用這個習慣。
我們的函式是用如下: eatHamburgers(“vitalik”, 100);
實戰演習
在我們的應用裡,我們要能建立一些殭屍,讓我們寫一個函式做這件事吧!
建立一個函式 createZombie。 它有兩個引數: _name (型別為string), 和 _dna (型別為uint)。 暫時讓函式空著——我們在後面會增加內容。
pragma solidity ^0.4.19;
contract ZombieFactory {
...
Zombie[] public zombies;
// 這裡開始
function createZombie(string _name, uint _dna) {
}
}
第8章: 使用結構體和陣列
建立新的結構體 還記得上個例子中的 Person 結構嗎?
struct Person {
uint age;
string name;
}
Person[] public people;
現在我們學習建立新的 Person 結構,然後把它加入到名為 people 的陣列中.
// 建立一個新的Person:
Person satoshi = Person(172, "Satoshi");
// 將新建立的satoshi新增進people陣列:
people.push(satoshi);
你也可以兩步並一步,用一行程式碼更簡潔:
people.push(Person(16, "Vitalik"));
注:array.push() 在陣列的 尾部 加入新元素 ,所以元素在陣列中的順序就是我們新增的順序, 如:
uint[] numbers;
numbers.push(5);
numbers.push(10);
numbers.push(15);
// numbers is now equal to [5, 10, 15]
實戰演習
讓我們建立名為createZombie的函式來做點兒什麼吧。
- 在函式體裡新建立一個 Zombie, 然後把它加入 zombies 陣列中。 新建立的殭屍的 name 和 dna,來自於函式createZombie的引數。
- 讓我們用一行程式碼簡潔地完成它。 答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
Zombie[] public zombies;
function createZombie(string _name, uint _dna) {
// 這裡開始
zombies.push(Zombie(_name, _dna));
}
}
第9章: 私有 / 公共函式
Solidity 定義的函式的屬性預設為公共(public)。 這就意味著任何一方 (或其它合約) 都可以呼叫你合約裡的函式。 顯然,不是什麼時候都需要這樣,而且這樣的合約易於受到攻擊。 所以將自己的函式定義為私有(private)是一個好的程式設計習慣,只有當你需要外部世界呼叫它時才將它設定為公共。
如何定義一個私有的函式呢?
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
這意味著只有我們合約中的其它函式才能夠呼叫這個函式,給 numbers 陣列新增新成員。
可以看到,在函式名字後面使用關鍵字 private 即可。和函式的引數類似,私有函式的名字用(_)起始。
實戰演習
我們合約的函式 createZombie 的預設屬性是公共的,這意味著任何一方都可以呼叫它去建立一個殭屍。 咱們來把它變成私有吧!
- 變 createZombie 為私有函式,不要忘記遵守命名的規矩哦!
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
}
第10章: 函式的更多屬性
本章中我們將學習函式的返回值和修飾符。
返回值 returns 要想函式返回一個數值,按如下定義:
string greeting = "What's up dog";
function sayHello() public returns (string) {
return greeting;
}
本例 執行sayHello函式 將返回"What’s up dog" 。 Solidity中,函式的定義,可包含返回值的資料型別(如本例中 string)。
函式的修飾符 view、pure 上面的函式實際上沒有改變 Solidity 裡的狀態,即,它沒有改變任何值或者寫任何東西。
這種情況下我們可以把函式定義為 view, 意味著它只能讀取資料不能更改資料:
function sayHello() public view returns (string) { Solidity 還支援 pure 函式, 表明這個函式甚至都不訪問應用裡的資料,例如:
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
這個函式甚至都不讀取應用裡的狀態 — 它的返回值完全取決於它的輸入引數,在這種情況下我們把函式定義為 pure.
注:可能很難記住何時把函式標記為 pure/view。 幸運的是, Solidity 編輯器會給出提示,提醒你使用這些修飾符。
實戰演習
我們想建立一個幫助函式,它根據一個字串隨機生成一個DNA資料。
-
建立一個 private 函式,命名為 _generateRandomDna。它只接收一個輸入變數 _str (型別 string), 返回一個 uint 型別的數值。
-
此函式只讀取我們合約中的一些變數,所以標記為view。
-
函式內部暫時留空,以後我們再新增程式碼。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
// 這裡開始
function _generateRandomDna(string _str) private view returns (uint) {
}
}
第11章: Keccak256 和 型別轉換
如何讓 _generateRandomDna 函式返回一個隨機的 uint?
Ethereum 內部有一個雜湊函式keccak256,它用了SHA3版本。一個雜湊函式基本上就是把一個字串轉換為一個256位的16進位制數字。字串的一個微小變化會引起雜湊資料極大變化。
這在 Ethereum 中有很多應用,但是現在我們只是用它造一個偽隨機數。
例子:
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5 keccak256(“aaaab”); //b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9 keccak256(“aaaac”); 顯而易見,輸入字串只改變了一個字母,輸出就已經天壤之別了。
注: 在區塊鏈中安全地產生一個隨機數是一個很難的問題, 本例的方法不安全,但是在我們的Zombie DNA演算法裡不是那麼重要,已經很好地滿足我們的需要了。
型別轉換 有時你需要變換資料型別。例如:
uint8 a = 5;
uint b = 6;
// 將會丟擲錯誤,因為 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我們需要將 b 轉換為 uint8:
uint8 c = a * uint8(b);
上面, a * b 返回型別是 uint, 但是當我們嘗試用 uint8 型別接收時, 就會造成潛在的錯誤。如果把它的資料型別轉換為 uint8, 就可以了,編譯器也不會出錯。
實戰演習
給 _generateRandomDna 函式新增程式碼! 它應該完成如下功能:
-
第一行程式碼取 _str 的 keccak256 雜湊值生成一個偽隨機十六進位制數,型別轉換為 uint, 最後儲存在型別為 uint 名為 rand 的變數中。
-
我們只想讓我們的DNA的長度為16位 (還記得 dnaModulus?)。所以第二行程式碼應該 return 上面計算的數值對 dnaModulus 求餘數(%)。 答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
}
第12章: 放在一起
我們就快完成我們的隨機殭屍製造器了,來寫一個公共的函式把所有的部件連線起來。
寫一個公共函式,它有一個引數,用來接收殭屍的名字,之後用它生成殭屍的DNA。
實戰演習
-
建立一個 public 函式,命名為createRandomZombie. 它將被傳入一個變數 _name (資料型別是 string)。 (注: 定義公共函式 public 和定義一個私有 private 函式的做法一樣)。
-
函式的第一行應該呼叫 _generateRandomDna 函式,傳入 _name 引數, 結果儲存在一個型別為 uint 的變數裡,命名為 randDna。
-
第二行呼叫 _createZombie 函式, 傳入引數: _name 和 randDna。
-
整個函式應該是4行程式碼 (包括函式的結束符號 } )。
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
// 事件
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
第13章: 事件
我們的合約幾乎就要完成了!讓我們加上一個事件.
事件 :是合約和區塊鏈通訊的一種機制。你的前端應用“監聽”某些事件,並做出反應。
例子:
// 這裡建立事件
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;
}
你的 app 前端可以監聽這個事件。JavaScript 實現如下:
YourContract.IntegersAdded(function(error, result) {
// 幹些事
}
實戰演習
我們想每當一個殭屍創造出來時,我們的前端都能監聽到這個事件,並將它顯示出來。
- 定義一個 事件 叫做 NewZombie。 它有3個引數: zombieId (uint), name (string), 和 dna (uint)。
2.修改 _createZombie 函式使得當新殭屍造出來並加入zombies陣列後,生成事件NewZombie。
3.需要定義殭屍id。 array.push() 返回陣列的長度型別是uint - 因為陣列的第一個元素的索引是 0, array.push() - 1 將是我們加入的殭屍的索引。 zombies.push() - 1 就是 id,資料型別是 uint。在下一行中你可以把它用到 NewZombie 事件中。 答案:
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 {
//
uint id = zombies.push(Zombie(_name, _dna)) - 1;
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 {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
第14章: Web3.js
我們的 Solidity 合約完工了! 現在我們要寫一段 JavaScript 前端程式碼來呼叫這個合約。
以太坊有一個 JavaScript 庫,名為Web3.js。
在後面的課程裡,我們會進一步地教你如何安裝一個合約,如何設定Web3.js。 但是現在我們通過一段程式碼來了解 Web3.js 是如何和我們釋出的合約互動的吧。
如果下面的程式碼你不能全都理解,不用擔心。
// 下面是呼叫合約的方式:
var abi = /* abi是由編譯器生成的 */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* 釋出之後在以太坊上生成的合約地址 */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` 能訪問公共的函式以及事件
// 某個監聽文字輸入的監聽器:
$("#ourButton").click(function(e) {
var name = $("#nameInput").val()
//呼叫合約的 `createRandomZombie` 函式:
ZombieFactory.createRandomZombie(name)
})
// 監聽 `NewZombie` 事件, 並且更新UI
var event = ZombieFactory.NewZombie(function(error, result) {
if (error) return
generateZombie(result.zombieId, result.name, result.dna)
})
// 獲取 Zombie 的 dna, 更新影象
function generateZombie(id, name, dna) {
let dnaStr = String(dna)
// 如果dna少於16位,在它前面用0補上
while (dnaStr.length < 16)
dnaStr = "0" + dnaStr
let zombieDetails = {
// 前兩位數構成頭部.我們可能有7種頭部, 所以 % 7
// 得到的數在0-6,再加上1,數的範圍變成1-7
// 通過這樣計算:
headChoice: dnaStr.substring(0, 2) % 7 + 1,
// 我們得到的圖片名稱從head1.png 到 head7.png
// 接下來的兩位數構成眼睛, 眼睛變化就對11取模:
eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
// 再接下來的兩位數構成衣服,衣服變化就對6取模:
shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
//最後6位控制顏色. 用css選擇器: hue-rotate來更新
// 360度:
skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
zombieName: name,
zombieDescription: "A Level 1 CryptoZombie",
}
return zombieDetails
}
我們的 JavaScript 所做的就是獲取由zombieDetails 產生的資料, 並且利用瀏覽器裡的 JavaScript 神奇功能 (我們用 Vue.js),置換出影象以及使用CSS過濾器。在後面的課程中,你可以看到全部的程式碼。