以太坊Soliddity編程:智能合約實現
基本數據類型
1. Solidity類型:
- solidity爲靜態類型語言
- 所有變量均需要預定義
- 和大多數語言一樣,提供幾個基本類型和基本類型組成的複雜類型
- 和現代語言相比,solidity僅覆蓋最常見的類型。
2. Solidityt基本數據類型
- 布爾類型
- 布爾:真或假
- 操作符:!(邏輯非)
- &&(邏輯與)
- || (邏輯或)
- == (相等)
- != (不等)
- 整型
- int / uint : 是有符號和無符號的整數
- uint8到uint256步長8(從8到256位的無符號整數)
- uint和int分別是uint256和int256的別名
- 地址
- 地址:20字節(一個以太坊地址)
- 可表示用戶賬號,合約地址
- 操作符:≤、<、==、!=,≥,>
- 地址成員: 賬戶餘額(balance)
- 地址成員:發送(transfer),從合約發起方向某個地址轉入以太幣(單位是wei)
- 註釋
- 字符串常量
- 字符串常量用雙引號或者單引號括起來,如“abc”,'abc’均可
- 字符串常量可以隱式轉換位bytes
- 字符串常量支持轉義符,如\n,\xNN(16進制)and \nNNNN(Unicode)
補充:
整型操作符:
- 比較: ≤,<,==,!=,≥,>(計量布爾量)
- 位操作符:&,!,^(位異或),~(位取反)
- 算術操作符:+,-,*,/,%(取餘數),**(冪次方)
- 除以0操作,會報異常
枚舉類型: - 用戶自定義類型,基本同C++等語言的定義
- 枚舉類型中的枚舉值到整型可顯示轉換,但不能隱式轉換
- 枚舉類型不能爲空,至少應當包含一個值
基本類型轉換
- 分爲隱式轉換和顯示轉換,如字符串轉爲整型等
- 隱式轉換:如果運算符支持兩邊不同的類型,編譯器會嘗試隱式轉換類型,包括賦值
- 隱式轉換需要能保證不會丟失數據,且語義可通
- 顯示轉換:如果編譯器不允許隱式的自動轉換,但你知道轉換沒問題時,可以進行轉換
- 不正確的轉換會帶來錯誤
array類型
數組
- 數組是可以在編譯時固定大小的,也可以是動態的
- 對於storage存儲數組來說,成員類型可以是任意的(也可以是其他數組,映射或結構)
- 對於memory數組來說,成員類型不能是一個映射,如果是公開可見的函數參數,成員類型是必須是ABI類型的。
數組聲明
- T[k]:元素類型爲T,固定長度爲k的數組
- T[]:動態大小的(變長)的數組
- uint[3][5]:聲明5個uint[3](與大部分語言相同).訪問時,uint[4][0]:第5個數組的第1個元素
- bytes類似於byte[]
數組成員函數
- 成員函數:length,存放元素的數量
- 動態length可變
- 成員函數:push,可在數組的尾部添加一個元素。函數返回新的長度
- Push目前僅storage變長數組可用
數組使用事項
數組作爲返回參數使用
- 暫時還無法在外部函數中使用數組的數組(多維數組)
- 暫時無法從外部函數調用返回的動態內容
- 目前可行的解決辦法是使用較大的靜態尺寸大小的數組
數組創建和初始化
- 可使用new關鍵字創建或初始化memory的變長數組
- 但不能通過.length的長度來修改memory數組大小屬性
- Storage數組可以修改長度
- 目前暫時是這樣規定的
要點:變長數組須先new初始化,分配空間
要點:變長storage數組可以通過push初始化
要點:暫不能通過外部函數返回變長數組
要點:定長的數組不可直接賦值給變長數組
mapping類型
映射定義
- 映射或字典類型,一種鍵值對的映射關係存儲結構
- 定義方式爲mapping(KeyType=>KeyValue)
- 鍵的類型一般爲基本數據類型,暫不支持映射、變長數組、結構體等複雜類型。值的類型無限制
- 映射可以視爲哈希表。所有可能的鍵會被虛擬化的創建,映射到一個類型的默認值
- 映射表中,我們並不存儲鍵的數據,僅存儲它的keccak256哈希值,用來查找值時使用
- 映射並沒有長度,鍵集合(或列表),值集合(或列表)這樣的概念。
映射定義的例子:
映射的賦值:
映射使用要點
- 映射類型,僅能用來定義狀態變量,或者是在內部函數中作爲storage類型的引用
- 可以通過將映射標記爲public,來讓Solidity創建一個訪問器
- 訪問映射,需要提供一個鍵值作爲參數
- 映射暫未提供迭代輸出的方法
struct類型
結構體
- Solidity提供struct來定義自定義類型
- struct可以用於映射和數組中作爲元素
- 其本身也可以包含映射和數組等類型
- 但struct內不能有struct。
結構體示例
結構體初始化
直接賦值(按順序)
命名初始化
忽略mapping
數據的位置和引用類型
值類型:布爾、整型、地址、定長字節數組
- 在傳值時,總是值傳遞,完全拷貝
引用類型:不定長數組、字符串、數組、結構體
- 複雜類型,佔用空間較大,在拷貝時佔用空間較大,一般都通過引用傳遞。
引用傳遞
- 值傳遞,傳遞的是內存的內容
- 引用傳遞,傳遞的是內存的地址
- 引用的改變,修改後會改變內存地址對應儲存的值,也就是變量和其引用會同時改變。
數據的位置
- 複雜/引用類型,如數組(arrays)和結構體(struct)有一個額外的屬性,數據的存儲位置,可選爲memory或storage
- Memory存儲位置爲內存
- Storage保存永久記錄,存儲在鏈上
數據的默認位置
- 基於程序的上下文,大多數時候這樣的選擇是默認的,也通過指定關鍵字storage和memory修改它
- 函數參數,包括返回的參數,默認是memory
- 局部複雜變量默認是storage
- 狀態變量強制爲storage
不同數據位置的賦值
- 在memory和storage之間,完全拷貝
- 任何位置的變量,賦值給狀態變量,完全拷貝
- 狀態變量(storage),賦值storage局部變量,引用傳遞
- memory的引用類型賦值給另一個memory的引用,不會創建另一個拷貝
注意事項
- 不能將memory賦值給局部變量
- memory只能用於函數內部
- 對於值類型,總是會進行拷貝
- storage在區塊鏈中是用key/value的形式存儲,而memory則表現爲字節數組
- 值類型的局部變量是存儲在棧上
- Gas消耗storage>>memory>stack
不同位置複雜類型賦值圖
常規用法
- storage爲合約級變量,在合約創建時就確定了,但內容可以被(交易)改變
- solidity認爲交易就是改變了合約狀態,故合約級變量稱爲“狀態”變量。函數內部只能定義storage的引用
- memory只能用於函數內部,其聲明EVM在運行時創建一塊內存區域給變量使用
特殊變量和函數
地址相關(Address Related)
- <address>.balance(uint256): address的餘額,以wei爲單位
- <address>.transfer(uint256 amount): 從合約(地址)向address發送一定數量的ether,以wei爲單位。
- <address>.send(uint256 amount) returns (bool): 同transfer。不建議。
示例:
合約相關
- this:當前合約的類型,可以顯示的轉換爲Address
- selfdestruct(address recipt):銷燬當前合約,並把它所有資金髮送到給定的地址。
- 如果一個函數需要進行貨幣操作,必須要帶上payable關鍵字。
數學和加密函數
- assert(bool condition):如果條件不滿足,拋出異常
- keccak256(…) returns (bytes32):使用以太坊的(Keccak-256)計算HASH值。常用來做字符串相等判別。
特殊變量及函數
- msg.sender(address) 當前調用發起人的地址
- msg.value(uint) 這個消息所附帶的貨幣量,單位爲wei
- tx.origin(address) 交易的發送者。不建議
- msg.data(bytes) 完整的調用數據(calldata)。
- now(uint) 當前塊的時間戳。
時間單位
- seconds、minutes、hours、days、weeks、years均可做爲後綴,並進行相互轉換,默認是seconds爲單位。
- 後綴不能用於變量
貨幣單位
- 一個字面量的數字,可以使用後綴wei、finney、szabo或ether來在不同面額中轉換
- 不含任何後綴的默認單位是wei
單位換算
貨幣使用示例
實戰:課程積分
項目需求
- 爲了活躍課程的氣氛,促進同學之間的交流,決定建立課程積分體系:
- 每個同學都能領到100個課程積分
- 爲別的同學答疑解惑,可以從對方那裏獲得一定的課程積分
- 課程結束後,積分最高的同學獲得小紅花
典型設計
基於sql數據庫設計實現:
用戶表(User)
- userId: 用戶id
- email: 用戶email
- …
用戶積分表(UserCoin) - userId: 用戶id
- coin:積分數量
- …
用戶積分交易表i(UserTrade) - userId: 發送方用戶id
- toUserId: 接收方用戶id
- coin:發送數量
- …
不足
- 發放積分方式不透明,需要老師的信譽背書
- 積分交易數據不透明,可能出現刷分現象
- 服務器可能掛了…
Dapp設計的優勢
- 去信任。代碼透明,積分體系和發放形式完全公開,無需老師信用背書
- 去中心。數據透明,交易數據公共可讀,無法隨意篡改,公共監督。不用擔心機器崩潰。
- 隱私保護。暫不需要。
Dapp數據結構設計
用狀態變量實現數據結構
用戶表(User)
struct User {
address userId; // 用戶id
}
User[] userList;
用戶積分表(UserCoin)
struct UserCoin {
address userId; //用戶id
uint coin; //積分數量
}
UserCoin[] userCoinList;
用戶積分交易表(UserTrade)
struct UserTrade {
address userId; // 發送方userid
address toUserId; // 接收方userid
uint coin; //發送數量
}
UserTrade[] userTradeList;
Dapp數據結構設計改進原則
- 基本思路同“SQL數據庫映射到KV數據庫”
- 根據查詢/索引需求,在基本數據結構上,建立mapping
- 爲節省存儲,去掉冗餘數據
功能需求1
- 每個同學都能領100個課程積分
- 函數 getCoin()
- 功能:由合約爲調用者發送100個積分
- 查詢需求:只能領一次,需要根據合約和調用者地址,查詢對應的交易記錄。
功能需求2
- 同學之間可以互送積分
- 函數 sendCoin()
- 功能:調用者向一個地址發送積分
- 查詢需求:一個用戶地址擁有的積分
Dapp數據結構的改進
用戶積分交易
struct UserTrade {
address userId; // 發送方userid
address toUserId; // 接收方userid
uint coin; //發送數量
}
UserTrade[] userTradeList;
查詢需求:根據“合約和調用者地址”,查詢交易
mapping (address => mapping(address=>UserTrade[])) trans;
去除冗餘數據
mapping (address => mapping(address=>uint[])) trans;
Dapp數據結構設計結果
用戶
address[] userList;
mapping (address => uint8) userDict;
用戶積分(address爲key)
mapping (address => uint) balances;
用戶積分交易表(address+address爲key)
mapping (address => mapping (address => uint[])) trans;