以太坊Solidity編程:智能合約實現之函數與合約
函數及基本控制結構
函數類型
- 函數也是一個類型,且屬於值類型
- 可以將一個函數賦值給一個變量賦值給一個變量,一個函數類型的變量
- 還可以將一個函數作爲參數進行傳遞
- 也可以在函數調用中返回一個函數
- 函數類型有兩類,可分爲internal和external
- 內部函數(internal):不能在當前合約的上下文環境以外的地方執行,內部函數只能在當前合約內被使用。如在當前的代碼塊內,包括庫函數,和繼承的函數中。
- 外部函數(External):外部函數由地址和函數方法簽名兩部分組成。可作爲外部函數調用的參數,或者由外部函數調用返回。
函數類型的定義
function (param types) {internal|external} [pure|constant|view|payable][returns (return types)] varName;
- 如果不寫類型,默認的函數類型是internal的
- 如果函數沒有返回結果,則必須省略returns關鍵字
函數的定義
function f(<parameter types>) {internal|external} [pure|constant|view|payable][returns (<return types>)]{ // function body}
示例:
contract SimpleFunc {
function hello(uint i) {
// todo
}
}
入值和返回值
- 同JavaScript一樣,函數有輸入參數,但與之不同的是,函數可能有任意數量的返回參數
- 入參(Input Parameter)與變量的定義方式一致,稍微不同的是,不會用到的參數可以省略變量名稱
- 出參(Output Parameters)在returns關鍵字後定義,語法類似變量的定義方式
示例: - Solidity內置支持元素(tuple),這種內置結構可以同時返回多個結果
示例
函數控制結構
- 支持if、else、while、do、for、break、continue、return、?:
- 條件判斷中的括號不可省略,但在單行語句中的大括號可以省略
- 無Boolean類型的自動轉換,比如if(1){…}在Solidity中是無效的
- 沒有switch
函數調用
- 內部調用:同一個合約中,函數互相調用。調用在EVM中被翻譯成簡單的跳轉指令
- 外部調用:一個合約調用另外一個合約的函數,或者通過web3調用合約函數。調用通過消息調用完成(bytes24類型,20字節合約地址+4字節的函數方法簽名)。
示例:
命名參數調用和匿名函數參數
- 函數調用的參數,可以通過指定名字的方式調用,但可以以任意的順序,使用方式爲{}包含。參數的類型和數量要與定義一致。
示例:
省略函數參數名稱
- 沒有使用的參數名可以省略
示例:
變量作用範圍
- 一個變量在聲明後都有初始值爲字節表示的全0
- Solidity使用了JavaScript的變量作用範圍的規則
- 函數內定義的變量,在整個函數中均可用,無論它在哪裏定義
示例:
函數可見性
函數的可見性
- 函數類型:internal和external
- 訪問方式:內部訪問和外部訪問
- 處於訪問控制的需要,函數具有“可見性(Visibility)類型”。
- 狀態變量也具有可見性類型
可見性類型
- external:“外部函數”,可以從其他合約或者通過交易來發起調用。合約內不能直接調用。
- public:“公開函數”,可以從合約內調用,或者消息來進行調用
- Internal:“內部函數”,只能在當前合約或繼承的合約裏調用
- private:“私有函數”,僅在當前合約中可以訪問,在繼承的合約內,不可訪問。
可見性圖
默認訪問權限
- 函數默認是public
- 狀態變量默認的可見性是internal
合約聲明示例
狀態變量訪問
- 編譯器爲自動爲所有的public的狀態變量創建訪問函數。
- 生成的函數名爲參數名稱,輸入參數根據變量類型來頂。如uint無須參數,uint[]則需要輸入參數數組下標作爲參數
示例:
函數修飾符
- 修改器(Modifiers)可以用來改變一個函數的行爲
- 一般用於在函數執行前檢查某種前置條件。
- 修改器是一種合約屬性,可被繼承,同時還可派生的合約重寫。
- 函數可以有多個修改器,它們之間以空格隔開,修飾器會依次檢查執行。
示例:
函數屬性
- 根據對狀態變量的修改情況,函數可以擁有pure|constant|view|public四個屬性。
- 屬性定義放置在函數可見性之後
- 不強制使用
- 主要是爲了節省gas
屬性介紹:
- 只有當函數有返回值的情況下,才需要使用pure、view、constant
- prue:可以讀取狀態變量但是不能改。
- view:不能改也不能讀狀態變量,否則編譯通不過
- constant:view的舊版本,v4.17之後不建議
- 帶關鍵字pure或view,就不能修改狀態變量的值。默認只是向區塊鏈讀取數據,讀取數據不需要花費gas。
狀態變量的屬性: - 狀態變量可以聲明爲constant,同一般語言的常量,目前只支持值類型和String。
- public的狀態變量,其getter函數屬性爲view。
事件
- 事件是使用EVM日誌內置功能的方便工具
- 事件在合約中可被繼承
- 當被調用時,會觸發參數存儲到交易的日誌中
- 可以最多有3個參數被設置爲indexed,來設置是否被索引。
- 設置爲索引後,可以允許通過這個參數來查找日誌,甚至可以按特定的值過濾。
錯誤和異常處理
Solidity的異常處理:
- Solidity使用“狀態恢復”來處理錯誤
- 有某些情況下,異常是自動拋出的
- 拋出異常的效果是當前的執行被終止且被撤銷(值的改變和賬戶餘額的變化都會被回退)
- Solidity暫時沒有捕捉異常的方法(try…catch)
assert/require:
- 函數assert和require來進行條件檢查,如果條件不滿足則拋出異常
- assert函數通常用來測試內部錯誤
- require函數來檢查輸入變量或合約狀態變量是否滿足條件,以及驗證調用外部合約返回值
revert/throw: - revert函數可用於標記錯誤
- throw同revert類似,已不建議使用
三個異常示例(等價的):
常見異常:
- 數組訪問越界,或是負的序號值訪問數組
- 調用require/assert,但參數值爲false
- .transfer()執行失敗
- 如果你的public的函數在沒有payable關鍵字時,卻嘗試在接收ether
- 如果你通過消息調用一個函數,但在調用的過程中,並沒有正確結果(如gas不足等)
合約與繼承
合約
- Solidity中合約類似面嚮對象語言中的類。
- 合約可以繼承。
- 一個合約可以調用另外一個合約。
- 一個合約中可以創建另外一個合約。
- 合約操作另外一個合約,一般都需要直到其代碼。
合約間調用
contract OwnedToken{
// TokenCreator是一個合約類型
// 未初始化前,爲一個引用
TokenCreator creator;
address owner; // 狀態變量
bytes32 name; // 狀態變量
// 構造函數
function OwnedToken(bytes32 _name) public{
owner = msg.sender;
creator = TokenCreator(msg.sender);// 另外一個合約
name = _name;
}
}
合約中創建合約
- 一個合約可以通過new關鍵字來創建一個合約。
- 要創建合約的完整代碼,必須提前知道
contact TokenCreator{
function createToken(bytes32 name) public returns (OwnedToken){
// 創建一個新的合約,name爲構造函數所需變量
OwnedToken tokenAddress = new OwnedToken(name);
return tokenAddress;
}
}
繼承
- Solidity通過複製包括多態的代碼來支持多重繼承。基本的繼承體系與python類似
- 當一個合約從多個其他合約那裏繼承,在區塊鏈上僅會創建一個合約,在父合約裏的代碼會複製來形成繼承合約。
- 派生的合約需要提供所有父合約需要的所有參數。
contract Owned {
function owned() { owner = msg.sender; }
address owner;
}
// 使用`is`來繼承另外一個合約
// 子合約可以使用所有的非私有變量,包括內部函數和狀態變量
contract Mortal is Owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
幾種特殊的合約
- 抽象合約(Abstract Contracts)
- 合約包含抽象函數,也就是沒有函數體的函數
- 這樣的合約不能通過編譯,即使合約內也包含一些正常的函數
- 抽象合約一般可以做爲基合約被繼承
contact Feline{
// 函數沒有函數實現,爲抽象函數,對應的合約爲抽象合約
function utterance() returns (bytes32);
function getContractName() returns (string) {
return "Feline";
}
}
contract Cat is Feline{
// 繼承抽象合約,實現函數功能
function utterance() returns (bytes32) {return "miaow";}
}
- 接口(Interfaces)
- 接口與抽象合約類似,與之不同的是,接口內沒有任何函數是已實現的,同時還有如下限制:
- 不能繼承其他合約,或接口
- 不能定義構造器
- 不能定義變量
- 不能定義結構體
- 不能定義枚舉類
- 接口基本上限制爲合約ABI定義可以表示的內容
// 接口
interface Token {
function transfer(address recipient, uint amount);
}
// 接口可以被
contract MyToken is Token {
function transfer(address recipient, uint amount){
// 函數發現
}
}
- 庫(Libraries)
- 庫與合約類似,但它的目的是在一個指定的地址,且僅部署一次,然後通過EVM的特性來複用代碼。
- 使用庫合約的合約,可以將庫合約視爲隱式的父合約(base contracts),不會顯示的出現在繼承關係中。
- 調用庫函數的方式非常類似,如庫L有函數f(),使用L.f()即可訪問
library Set{
struct Data { mapping(uint => bool) flags; }
// 第一個參數的類型爲“storage reference”, 僅存儲地址,而不是這個庫的特別特徵
// 按照一般語言的慣例,`self`代表第一個參數
function insert(Data storage self, uint value)
public
return (bool)
{
if (self.flags[value])
return false; //already there
self.flags[value] = true;
return true;
}
}
contract C{
Set.Data knownValues;
function register(uint value) public{
// 庫可以直接調用,而無需使用this
requires(Set.insert(knownValues, value));
}
}
實戰:獎品競拍
項目需求
- 大家之前都拿到了不少課程積分,爲活躍課程氛圍,特開展獎品競拍活動:
-
- 大家用課程積分競拍,採用連續競價、明拍、限時模式。
- 獎品爲電商兌換碼,可以兌換不同學習物品
- 一次拍賣完成後,積分概不退還,可開始下一次拍賣。
基本數據結構
- 積分體系:完全使用之前的Mycoin即可,使用繼承
- 拍賣列表:用戶和出價,使用一個mapping即可
- 出價最高用戶/出價:全局狀態變量
- 限時/兌換碼/競拍狀態:全局狀態變量
功能需求:競價
- 連續競價,和之前的出價疊加
- 必須高於當前最高出價纔算出價成功。
- 出價的積分打入合約地址
功能需求:競價完成
- 結束時間已到
- 之前沒有領取過
- 最高出價用戶領取
- 全局狀態變量管理員可以強制終止拍賣
功能需求:重開競價
- 只有管理員可以重開
- 重新設置兌換碼
- 重新設置兌換時間
- 重置競拍狀態
代碼
MyCoin.sol
pragma solidity >=0.6.4;
contract MyCoin{
//數據結構
//1.用戶
address[] userList;
mapping(address=>uint8) userDict;
//2.用戶的幣
mapping(address=>uint) balances;
//3.用戶交易,第一個address爲交易發起方,第二個爲接收方,uint[]爲交易量記錄
mapping(address => mapping(address=>uint[])) public trans;
//用戶初次可領取的數量
uint iniCount = 100;
function Mycoin() public{
//合約本身擁有的幣
balances[address(this)] = 100000;
}
//領取貨幣,只能一次
function getCoin() public returns (bool sufficientAndFirstGet) {
//判斷合約是否錢足夠
if(balances[address(this)]<iniCount) return false;
//判斷是否從合約領取過幣
if(0!=trans[address(this)][msg.sender].length) return false;
//領取幣
balances[address(this)] -= iniCount;
balances[msg.sender] += iniCount;
//記錄交易
trans[address(this)][msg.sender].push(iniCount);
//加入用戶列表
if(0 == userDict[msg.sender]){
userList.push(msg.sender);
userDict[msg.sender] = 1;
}
return true;
}
//發送貨幣
function sendCoin(address receiver, uint amount) public returns(bool sufficient){
//判斷是否還有足夠的幣
if(balances[address(this)]<amount) return false;
//發生交易
balances[msg.sender] -= amount;
balances[receiver] += amount;
//記錄交易
trans[msg.sender][receiver].push(amount);
//加入用戶列表
if (0 == userDict[receiver]) {
userList.push(receiver);
userDict[receiver] = 1;
}
return true;
}
//獲得某個用戶的貨幣量
function getBalances(address addr) public returns(uint){
return balances[addr];
}
//獲得領取的進度
function getPercent() public returns(uint){
uint sum = 0;
for(uint i = 0;i < userList.length; i++){
address userAddress = userList[i]; //用戶的地址
sum = sum + balances[userAddress];
}
return (100*sum)/100000;
}
//獲得幣最多的用戶地址
function getBest() public returns (address){
address maxAdd;
uint maxCoin = 0;
//獲得幣最多的用戶地址
for(uint i = 0; i < userList.length; i++){
address userAddress = userList[i]; //用戶地址
uint userCoin = balances[userAddress]; //用戶積分
if(userCoin > maxCoin){
maxAdd = userAddress;
maxCoin = userCoin;
}
}
return maxAdd;
}
}
MyBid.sol
pragma solidity >=0.6.4;
import "./MyCoin.sol";
contract MyBid is MyCoin{
//數據結構
string private ticket;//電商兌換碼
address owner;//合約創建者
//時間是unix的絕對時間戳
uint public auctionEnd;
// 拍賣的當前狀態
address public highestBidder; //最高出價用戶的地址
uint public highestBid; //最高出價
//出價列表
mapping (address => uint) bids;
// 拍賣結束後設爲 true,將禁止所有的變更
bool ended;
//事件
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
//dev 創建一個簡單的拍賣
//_biddingTime,拍賣時間;_ticket,兌換碼
constructor (uint _biddingTime, string memory _ticket) public{
ticket = _ticket;
auctionEnd = block.timestamp + _biddingTime;
owner = msg.sender;
}
// 競價,value爲加價,和之前的出價疊加
function bid(uint value) public {
// 如果拍賣已結束,撤銷函數的調用。
require(block.timestamp < auctionEnd);
// 如果出價不夠高,不繼續執行
uint actualValue = bids[msg.sender] + value;
require(actualValue > highestBid);
//把積分發送到合約地址,已經檢查額度
sendCoin(address(this),value);
//更新出價和最高出價
bids[msg.sender] = actualValue;
highestBidder = msg.sender;
highestBid = actualValue;
emit HighestBidIncreased(msg.sender, actualValue);
}
// 結束拍賣,並把最高的出價發送給受益人
function auctionEnded() public payable returns (string memory){
// 1. 條件
require(now >= auctionEnd,"Not End!"); // 拍賣尚未結束
require(!ended); // 該函數已被調用
//必須是最高出價用戶調用,或者管理員強制終止
require(msg.sender == highestBidder || msg.sender == owner);
// 2. 生效
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. 返回ticket,只能查看一次
return ticket;
}
// `_;` 表示修飾符,可代表被修飾函數位置
modifier onlyOwner {
require(msg.sender == owner);
_;
}
//開始新的拍賣
function newBid(uint _biddingTime, string memory _ticket) public onlyOwner {
// 1. 條件
require(now >= auctionEnd);
require(ended);
//重置數據
ticket = _ticket;
auctionEnd = now + _biddingTime;
ended = false;
delete highestBidder; //重置最高出價用戶的地址
delete highestBid; //重置最高出價
//delete bids[msg.sender];
}
}
注意:可能由於版本問題存在一些錯誤,如有參考,請及時更正!