區塊鏈入門教程之從比特幣到以太訪再到智能合約從架構概念到應用實戰(DAPP)(四、以太訪、web3、智能合約三者之間的關係及智能合約基本講解)
作者: AlexTan
CSDN: http://blog.csdn.net/alextan_
Github: https://github.com/AlexTan-b-z
e-mail: [email protected]
前言
(ps:這是本博主撰寫的第二部系列作品,第一部是寫的java入門教程,受到了不少讀者的喜歡,如果你也喜歡的話,歡迎關注喲!)
本教程主要面向區塊鏈新手,用通俗易懂的方式講解區塊鏈技術。
這一小節,我們主要講以太訪、智能合約、web3三者之間的關係,以及智能合約(solidity)的基本特性。爲了方便開發,理解這些是非常有必要的。
以太訪、web3、智能合約
什麼是以太訪,到這裏我想大家大概都已經明白了。那什麼是web3(泛指web3.js、web3.py等)呢?前面也提到過,它就相當於是以太訪區塊鏈的接口(api),我們是通過web3,把智能合約部署在區塊鏈上的,我們是通過web3獲取到的區塊鏈相關信息以及進行一些交易。更多細節請參考:web3.js API中文文檔
web3
其實web3無非是封裝的以太訪的JSON-RPC,其JSON-RPC針對不同版本的以太訪客戶端支持以下請求方式:
cpp-ethereum | go-ethereum | py-ethereum | parity |
---|---|---|---|
JSON-RPC 1.0 | ✓ | ||
JSON-RPC 2.0 | ✓ | ✓ | ✓ |
Batch requests | ✓ | ✓ | ✓ |
HTTP | ✓ | ✓ | ✓ |
IPC | ✓ | ✓ | |
WS | ✓ |
更多請參考:
智能合約,Solidity的特性介紹
以太訪中,開發智能合約有很多種語言:
- Solidity:這是一種類似於js的語言,並且這個語言被稱爲開發智能合約的旗艦語言。
- Serpent:這是一種類似於Python的語言。
- LLL:這是一種類似於彙編的低級語言。
- Mutan:這是一種類似於C的語言,目前已被放棄。
而目前官方的最流行的就是solidity。
Solidity的合約其實就類似於面向對象裏所說的類(從某種意義來說,其實就是!),但是和傳統的類又有所不同,其不同點主要在以下幾點(僅對代碼而言):
1. 調用機制不同
2. 成員類型不同
3. 構造函數不能重載
cankao我們將詳細講解以上幾點:
Solidity的調用機制
Solidity的函數調用機制是消息調用的模式,什麼是消息調用呢?
其實可以簡單的把它理解成交易(ps:可以把智能合約的所有的函數調用都理解成交易,這也是爲什麼說智能合約是通過用戶的交易行爲觸發的。),交易即要有發起交易的人,發送目標是什麼,發送的數額是多少等。也就是說,每次的函數調用,也得有這些數據。
我們在合約函數內部可以用msg.sender
獲取調用函數的人(即上面提到的發起交易的人),msg.value
獲取用戶發生的金額(可無),有沒有發生目標根據函數實現的具體功能來確定。
而通過web3調用函數時,也得在函數參數後面加一個:{from:web3.eth.accounts[0], gas:44000000}
類似這樣的一個參數。
調用函數的gas費
我們前面提到,調用合約函數是需要Gas費的,具體的說是,調用涉及寫操作的函數是需要Gas費的,如果只是讀操作,比如說讀狀態變量等,可以通過call()
的方式調,這樣是不需要消耗Gas費的,關於Gas費的計費方式我們前面已經講到過,這裏就不一一闡述了。
成員類型
像java、c++等面向對象的語言一般只有public、protected、private
這三種類型,而solidity裏有:public、private、internal、external
四種類型,其中分別表示的意義是:
- public: 默認是public屬性,即內部和外部都可以訪問
- private:僅當前合約內可以訪問
- internal: 僅當前合約以及所繼承的合約可以訪問
- external: 僅合約外部可以訪問
需要注意的是,不像傳統語言那樣,成員變量的外部調用可以更改變量的值,solidity的成員外部調用是不可以修改變量值的,任何類型都不能修改。
需要注意的另一個點:不同的類型的gas花費是不同的,能少用public儘量少用public,爲什麼呢?下面我們來看實例:
pragma solidity^0.4.18;
contract Test {
uint[10] x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function test(uint[10] a) public returns (uint){
return a[9]*2;
}
function test2(uint[10] a) external returns (uint){
return a[9]*2;
}
function calltest() {
test(x);
}
function calltest2() {
this.test2(x);
//test2(x); //不能在內部調用一個外部函數,會報編譯錯誤。
}
}
打開Remix - Solidity IDE,帖入代碼,創建合約。
然後,我們分別調用test及test2,對比執行花費的gas。
可以看到調用pubic函數花銷更大,這是爲什麼呢?
當使用public 函數時,Solidity會立即複製數組參數數據到內存, 而external函數則是從calldata讀取,而分配內存開銷比直接從calldata讀取要大的多。
那爲什麼public函數要複製數組參數數據到內存呢?是因爲public函數可能會被內部調用,而內部調用數組的參數是當做指向一塊內存的指針。
對於external函數不允許內部調用,它直接從calldata讀取數據,省去了複製的過程。
所以,如果確認一個函數僅僅在外部訪問,請用external。
同樣,我們接着對比calltest()及calltest2(),這裏不截圖了,大家自己運行對比一下,可以發現:calltest2的開銷比calltest的開銷大很多,這是因爲通過this.f()模式調用,會有一個大開銷的CALL調用,並且它傳參的方式也比內部傳遞開銷更大。
因此,極不建議用this.function()
的方式在內部調用external的函數,如果需要內外部都能訪問,還是老實用public吧。
同理,我們把external改成private或者internal,我們會發現external、private會比public的花費更少,因此,當成員變量只需要內部訪問時,儘量用private或者internal。
總之,當我們確定好成員的使用範圍時,用其相應的類型就好,不要一位的使用public。
Solidity函數中view,constant,pure,payable的使用
contant: 函數有返回值,且返回的是狀態變量。
pure:函數有返回值,且返回的不是變量,返回的是一個具體的值。
view:函數有返回值,返回的既不是具體的值也不是狀態變量,例如:局部變量、msg.sender等。
payable: 可以通過這個函數發送以太幣給合約。
Solidity的數據類型
Solidity 類型分爲兩類:值類型(Value Type) 及 引用類型(Reference Types)。
值類型(Value Type)
值類型包含:
- 布爾類型(Booleans)
- 整型(Integers)
- 定長浮點型(Fixed Point Numbers)
- 定長字節數組(Fixed-size byte arrays)
- 有理數和整型常量(Rational and Integer Literals)
- 字符串常量(String literals)
- 十六進制常量(Hexadecimal literals)
- 枚舉(Enums)
- 函數(Function Types)
- 地址(Address)
- 地址常量(Address Literals)
布爾類型(Booleans)
布爾(bool):可能的取值爲常量值true和false。
布爾類型支持的運算符有:
- !邏輯非
- && 邏輯與
- || 邏輯或
- == 等於
- != 不等於
注意:運算符&&和||是短路運算符,如f(x)||g(y),當f(x)爲真時,則不會繼續執行g(y)。
cankao
整型(Integers)
int/uint: 表示有符號和無符號不同位數整數。支持關鍵字uint8 到 uint256 (以8步進),
uint 和 int 默認對應的是 uint256 和 int256。
支持的運算符:
- 比較運算符: <=, < , ==, !=, >=, > (返回布爾值:true 或 false)
- 位操作符: &,|,^(異或),~(位取反)
- 算術操作符:+,-,一元運算-,一元運算+,,/, %(取餘數), **(冪), << (左移位), >>(右移位)
說明:
- 整數除法總是截斷的,但如果運算符是字面量(字面量稍後講),則不會截斷。
- 整數除0會拋異常。
- 移位運算的結果的正負取決於操作符左邊的數。x << y 和 x * 2***y 是相等, x >> y 和 x / 2**y 是相等的。
- 不能進行負移位,即操作符右邊的數不可以爲負數,否則會拋出運行時異常。
注意:Solidity中,右移位是和除等價的,因此右移位一個負數,向下取整時會爲0,而不像其他語言裏爲無限負小數。
定長浮點型(Fixed Point Numbers)
注意:定長浮點型 Solidity(發文時)還不完全支持,它可以用來聲明變量,但不可以用來賦值。
fixed/ufixed: 表示有符號和無符號的固定位浮點數。關鍵字爲ufixedMxN 和 ufixedMxN。
M表示這個類型要佔用的位數,以8步進,可爲8到256位。
N表示小數點的個數,可爲0到80之前
支持的運算符:
- 比較運算符: <=, < , ==, !=, >=, > (返回布爾值:true 或 false)
- 算術操作符:+,-,一元運算-,一元運算+,*,/, %(取餘數)
注意:它和大多數語言的float和double不一樣,M是表示整個數佔用的固定位數,包含整數部分和小數部分。因此用一個小位數(M較小)來表示一個浮點數時,小數部分會幾乎佔用整個空間。
定長字節數組(Fixed-size byte arrays)
關鍵字有:bytes1, bytes2, bytes3, …, bytes32。(以步長1遞增)
byte代表bytes1。
支持的運算符:
- 比較符: <=, <, ==, !=, >=, > (返回bool)
- 位操作符: &, |, ^ (按位異或),~(按位取反), << (左移位), >> (右移位)
- 索引(下標)訪問: 如果x是bytesI,當0 <= k < I ,則x[k]返回第k個字節(只讀)。
移位運算和整數類似,移位運算的結果的正負取決於操作符左邊的數,且不能進行負移位。
成員變量:
.length:表示這個字節數組的長度(只讀)。
cankao
變長(動態分配大小)字節數組(Dynamically-sized byte array)
根據經驗:
bytes用來存儲任意長度的字節數據,string用來存儲任意長度的(UTF-8編碼)的字符串數據。
如果長度可以確定,儘量使用定長的如byte1到byte32中的一個,因爲這樣更省空間。
有理數和整型常量(Rational and Integer Literals)
也有人把Literals翻譯爲字面量
整型常量是有一系列0-9的數字組成,10進製表示,比如:8進制是不存在的,前置0在Solidity中是無效的。
10進制小數常量(Decimal fraction literals)帶了一個., 在.的兩邊至少有一個數字,有效的表示如:1., .1 和 1.3.
科學符號也支持,基數可以是小數,指數必須是整數, 有效的表示如: 2e10, -2e10, 2e-10, 2.5e1。
數字常量表達式本身支持任意精度,也就是可以不會運算溢出,或除法截斷。但當它被轉換成對應的非常量類型,或者將他們與非常量進行運算,則不能保證精度了。
如:(2**800 + 1) - 2**800的結果爲1(uint8整類) ,儘管中間結果已經超過計算機字長。另外:.5 * 8的結果是4,儘管有非整形參與了運算。
只要操作數是整形,整型支持的運算符都適用於整型常量表達式。
如果兩個操作數是小數,則不允許進行位運算,指數也不能是小數。
注意:
Solidity對每一個有理數都有一個數值常量類型。整數常量和有理數常量從屬於數字常量。所有的數字常表達式的結果都屬於數字常量。所以1 + 2和2 + 1都屬於同樣的有理數的數字常量3
警告:
整數常量除法,在早期的版本中是被截斷的,但現在可以被轉爲有理數了,如5/2的值爲 2.5
注意:
數字常量表達式,一旦其中含有常量表達式,它就會被轉爲一個非常量類型。下面代碼中表達式的結果將會被認爲是一個有理數:
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;
cankao上述代碼編譯不能通過,因爲b會被編譯器認爲是小數型。
字符串常量
字符串常量是指由單引號,或雙引號引起來的字符串 (“foo” or ‘bar’)。字符串並不像C語言,包含結束符,”foo”這個字符串大小僅爲三個字節。和整數常量一樣,字符串的長度類型可以是變長的。字符串可以隱式的轉換爲byte1,…byte32 如果適合,也會轉爲bytes或string。
字符串常量支持轉義字符,比如\n,\xNN,\uNNNN。其中\xNN表示16進制值,最終轉換合適的字節。而\uNNNN表示Unicode編碼值,最終會轉換爲UTF8的序列。
十六進制常量(Hexadecimal literals)
十六進制常量,以關鍵字hex打頭,後面緊跟用單或雙引號包裹的字符串,內容是十六進制字符串,如hex”001122ff”。cankao
它的值會用二進制來表示。
十六進制常量和字符串常量類似,也可以轉換爲字節數組。
枚舉(Enums)
在Solidity中,枚舉可以用來自定義類型。它可以顯示的轉換與整數進行轉換,但不能進行隱式轉換。顯示的轉換會在運行時檢查數值範圍,如果不匹配,將會引起異常。枚舉類型應至少有一名成員。下面是一個枚舉的例子:
pragma solidity ^0.4.0;
contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() {
choice = ActionChoices.GoStraight;
}
// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity. The integer type used is just
// large enough to hold all enum values, i.e. if you have more values,
// `uint16` will be used and so on.
function getChoice() returns (ActionChoices) {
return choice;
}
function getDefaultChoice() returns (uint) {
return uint(defaultChoice);
}
}
引用類型
引用類型是一個複雜類型,佔用的空間通常超過256位, 拷貝時開銷很大,因此我們需要考慮將它們存儲在什麼位置,是memory(內存中,數據不是永久存在)還是storage(永久存貯在區塊鏈中)
所有的複雜類型如數組(arrays)和數據結構(struct)有一個額外的屬性:數據的存儲位置(data location)。可爲memory和storage。
根據上下文的不同,大多數時候數據位置有默認值,也通過指定關鍵字storage和memory修改它。
函數參數(包含返回的參數)默認是memory。
局部複雜類型變量(local variables)和 狀態變量(state variables) 默認是storage。
局部變量:局部作用域(越過作用域即不可被訪問,等待被回收)的變量,如函數內的變量。狀態變量:合約內聲明的公有變量
還有一個存儲位置是:calldata,用來存儲函數參數,是隻讀的,不會永久存儲的一個數據位置。外部函數的參數(不包括返回參數)被強制指定爲calldata。效果與memory差不多。
數據位置指定非常重要,因爲他們影響着賦值行爲。
在memory和storage之間或與狀態變量之間相互賦值,總是會創建一個完全獨立的拷貝。
而將一個storage的狀態變量,賦值給一個storage的局部變量,是通過引用傳遞。所以對於局部變量的修改,同時修改關聯的狀態變量。
另一方面,將一個 memory 的引用類型賦值給另一個 memory 的引用,不會創建拷貝(即:memory 之間是引用傳遞)。
- 注意:不能將 memory 賦值給局部變量。
- 對於值類型,總是會進行拷貝。
下面看一段代碼:
pragma solidity ^0.4.0;
contract C {
uint[] x; // x的存儲位置是storage
// memoryArray的存儲位置是 memory
function f(uint[] memoryArray) public {
x = memoryArray; // 從 memory 複製到 storage
var y = x; // storage 引用傳遞局部變量y(y 是一個 storage 引用)
y[7]; // 返回第8個元素
y.length = 2; // x同樣會被修改
delete x; // y同樣會被修改
// 錯誤, 不能將memory賦值給局部變量
// y = memoryArray;
// 錯誤,不能通過引用銷燬storage
// delete y;
g(x); // 引用傳遞, g可以改變x的內容
h(x); // 拷貝到memory, h無法改變x的內容
}
function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}
}
總結
強制的數據位置(Forced data location)
- 外部函數(External function)的參數(不包括返回參數)強制爲:calldata
- 狀態變量(State variables)強制爲: storage
默認數據位置(Default data location)
- 函數參數及返回參數:memory
- 複雜類型的局部變量:storage
深入分析
storage 存儲結構是在合約創建的時候就確定好了的,它取決於合約所聲明狀態變量。但是內容可以被(交易)調用改變。
Solidity 稱這個爲狀態改變,這也是合約級變量稱爲狀態變量的原因。也可以更好的理解爲什麼狀態變量都是storage存儲。
memory 只能用於函數內部,memory 聲明用來告知EVM在運行時創建一塊(固定大小)內存區域給變量使用。
storage 在區塊鏈中是用key/value的形式存儲,而memory則表現爲字節數組
關於棧(stack)
EVM是一個基於棧的語言,棧實際是在內存(memory)的一個數據結構,每個棧元素佔爲256位,棧最大長度爲1024。
值類型的局部變量是存儲在棧上。
不同存儲的消耗(gas消耗)
- storage 會永久保存合約狀態變量,開銷最大
- memory 僅保存臨時變量,函數調用之後釋放,開銷很小
- stack 保存很小的局部變量,幾乎免費使用,但有數量限制。
參考:Solidity官方文檔-類型之data-location
捐助地址:
BTC:
36Q4ivp2bJer9fUQ6uyj4a4yLuTpp28D1T
ETH:
0x786fda245ff497ce18e53618369a3e730a18fc1b
ENS: alextan.eth