【區塊鏈】以太坊Solidity編程:智能合約實現之函數與合約

以太坊Solidity編程:智能合約實現之函數與合約

函數及基本控制結構

函數類型

  • 函數也是一個類型,且屬於值類型
  • 可以將一個函數賦值給一個變量賦值給一個變量,一個函數類型的變量
  • 還可以將一個函數作爲參數進行傳遞
  • 也可以在函數調用中返回一個函數
  • 函數類型有兩類,可分爲internal和external
  1. 內部函數(internal):不能在當前合約的上下文環境以外的地方執行,內部函數只能在當前合約內被使用。如在當前的代碼塊內,包括庫函數,和繼承的函數中。
  2. 外部函數(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
    可見性示例
    合約聲明示例
    合約C
    合約D
    合約E

狀態變量訪問

  • 編譯器爲自動爲所有的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)
  1. 合約包含抽象函數,也就是沒有函數體的函數
  2. 這樣的合約不能通過編譯,即使合約內也包含一些正常的函數
  3. 抽象合約一般可以做爲基合約被繼承
contact Feline{
	// 函數沒有函數實現,爲抽象函數,對應的合約爲抽象合約
	function utterance() returns (bytes32);
	 
	function getContractName() returns (string) {
		return "Feline";
	}
}

contract Cat is Feline{
	// 繼承抽象合約,實現函數功能
	function utterance() returns (bytes32) {return "miaow";}
}
  • 接口(Interfaces)
  1. 接口與抽象合約類似,與之不同的是,接口內沒有任何函數是已實現的,同時還有如下限制:
  2. 不能繼承其他合約,或接口
  3. 不能定義構造器
  4. 不能定義變量
  5. 不能定義結構體
  6. 不能定義枚舉類
  7. 接口基本上限制爲合約ABI定義可以表示的內容
// 接口
interface Token {
	function transfer(address recipient, uint amount);
}

// 接口可以被
contract MyToken is Token {
	function transfer(address recipient, uint amount){
		// 函數發現
	}
}
  • 庫(Libraries)
  1. 庫與合約類似,但它的目的是在一個指定的地址,且僅部署一次,然後通過EVM的特性來複用代碼。
  2. 使用庫合約的合約,可以將庫合約視爲隱式的父合約(base contracts),不會顯示的出現在繼承關係中。
  3. 調用庫函數的方式非常類似,如庫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);
	}	
}

實戰:獎品競拍

項目需求

  • 大家之前都拿到了不少課程積分,爲活躍課程氛圍,特開展獎品競拍活動:
    1. 大家用課程積分競拍,採用連續競價、明拍、限時模式。
  • 獎品爲電商兌換碼,可以兌換不同學習物品
  • 一次拍賣完成後,積分概不退還,可開始下一次拍賣。

基本數據結構

  • 積分體系:完全使用之前的Mycoin即可,使用繼承
  • 拍賣列表:用戶和出價,使用一個mapping即可
  • 出價最高用戶/出價:全局狀態變量
  • 限時/兌換碼/競拍狀態:全局狀態變量

功能需求:競價

  1. 連續競價,和之前的出價疊加
  2. 必須高於當前最高出價纔算出價成功。
  3. 出價的積分打入合約地址

功能需求:競價完成

  1. 結束時間已到
  2. 之前沒有領取過
  3. 最高出價用戶領取
  4. 全局狀態變量管理員可以強制終止拍賣

功能需求:重開競價

  1. 只有管理員可以重開
  2. 重新設置兌換碼
  3. 重新設置兌換時間
  4. 重置競拍狀態

代碼

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];
    }
}

注意:可能由於版本問題存在一些錯誤,如有參考,請及時更正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章