表達(dá)式和控制體

原文鏈接
date:20170707

輸入?yún)?shù)和輸出參數(shù)

在javascript中,函數(shù)可以傳遞參數(shù)作為輸入;Solidity于javascript和C不同,可以返回任意數(shù)量的參數(shù)作為輸出。

輸入?yún)?shù)

輸入?yún)?shù)和變量的定義一樣。在表達(dá)式中,未使用到的參數(shù)可以忽略變量名稱。例如,我們合約里的一個(gè)函數(shù),有兩個(gè)整形參數(shù),我們可以寫成這樣:

contract Simple {
    function taker(uint _a, uint _b) {
        // 對參數(shù)_a和_b進(jìn)行計(jì)算
    }
}
輸出參數(shù)

輸出參數(shù)可以在returns之后通過同樣的語法來定義。例如,我們?nèi)绻胍祷貎蓚€(gè)結(jié)果:給定整形參數(shù)的和與乘積,我們可以這么寫:

contract Simple {
    function arithmetics(uint _a, uint _b) returns (uint o_sum, uint o_product) {
        o_sum = _a + _b;
        o_product = _a * _b;
    }
}

輸出參數(shù)的名稱可以省略。輸出值可以直接通過return表達(dá)式指定。return表達(dá)式可以返回多個(gè)值,參看返回多個(gè)值章節(jié)。返回參數(shù)被初始化為0,如果它們沒有被明確指定,就會保持0.
輸入?yún)?shù)和輸出參數(shù)可以在函數(shù)體中使用??梢员恢匦沦x值。(?Input parameters and output parameters can be used as expressions in the function body. There, they are also usable in the left-hand side of assignment.)

控制體

很多javascript的控制體都可以在solidity中使用,除了switchgoto。所以有:if,elsewhile,do,for,break,continue,return,?:,它們的語義和C或者javascript一致。
條件判斷的圓括號不可以省略,但是花括號,如果只有一條語句的時(shí)候,可以省略。
需要注意的是在Solidity中并沒有在C和javascript中的非布爾類型轉(zhuǎn)換為布爾類型。所以if (1) { ... }在Solidity中并不試用。

返回多個(gè)值

當(dāng)函數(shù)有多個(gè)輸出,return (v0,v1, ..., vn)可以返回多個(gè)值。組件數(shù)量和輸出參數(shù)必須一致。

函數(shù)調(diào)用

內(nèi)部函數(shù)調(diào)用

當(dāng)前合約的函數(shù)可以直接調(diào)用(“內(nèi)部調(diào)用”),也可以遞歸調(diào)用。例子如下所示:

ontract C {
    function g(uint a) returns (uint ret) { return f(); }
    function f() returns (uint ret) { return g(7) + f(); }
}

這些函數(shù)調(diào)用會被轉(zhuǎn)換為在EVM(以太坊虛擬機(jī))中的簡單跳轉(zhuǎn)。它的效果是當(dāng)前的內(nèi)存并沒有清除,所以在內(nèi)部傳遞內(nèi)存引用是很高效的。只有同個(gè)合約內(nèi)的函數(shù)可以在內(nèi)部調(diào)用。

外部函數(shù)調(diào)用

表達(dá)式this.g(8)c.g(2)c是一個(gè)合約實(shí)例)都是有效的函數(shù)調(diào)用。但是這個(gè)時(shí)候,這個(gè)調(diào)用就被稱為“外部函數(shù)調(diào)用”。通過一個(gè)消息調(diào)用,并不是簡單的直接的跳轉(zhuǎn)。注意函數(shù)調(diào)用this是不能用在構(gòu)造函數(shù)中的,因?yàn)楫?dāng)前合約并未生成。
其他合約的函數(shù)調(diào)用也是外部調(diào)用。所有的外部函數(shù)調(diào)用,都會把參數(shù)拷貝到內(nèi)存中。
當(dāng)我們調(diào)用其他合約的函數(shù),一定數(shù)量的Wei會隨著調(diào)用發(fā)送出去,gas可以通過.value().gas()指定。

contract InfoFeed {
    function info() payable returns (uint ret) { return 42; }
}

contract Consumer {
    InfoFeed feed;
    function setFeed(address addr) { feed = InfoFeed(addr); }
    function callFeed() { feed.info.value(10).gas(800)(); }
}

info函數(shù)必須使用payable標(biāo)識符,否則,.value()就無法使用。
需要注意的是在表達(dá)式InfoFeed(addr)中,有一個(gè)顯式轉(zhuǎn)換:我們知道所給定地址的合約類型是InfoFeed,并且不執(zhí)行構(gòu)造函數(shù)。顯式轉(zhuǎn)換必須特別小心。不要在不確定合約類型的時(shí)候調(diào)用合約里的函數(shù)。
我們可以直接使用function setFeed(InfoFeed _feed){feed = _feed;}。表達(dá)式feed.info.value(10).gas(800)只是設(shè)置了值和gas的數(shù)目。最后的圓括號才是真正的函數(shù)調(diào)用。
如果所調(diào)用的合約并不存在(賬戶并不包含代碼),或者被調(diào)用函數(shù)本身拋出異常,或者gas不足,那函數(shù)調(diào)用會發(fā)生異常。

警告:任何與其他合約的交互都會有潛在的危險(xiǎn),尤其是我們對所調(diào)用合約的代碼不了解的時(shí)候。當(dāng)前合約把控制權(quán)交給所調(diào)用的合約,那么擁有控制權(quán)的合約可以做很多事情。即使該合約繼承自所知的一個(gè)合約,繼承合約只是需要實(shí)現(xiàn)正確的接口。接口到底怎么實(shí)現(xiàn),完全是任意的,所以會有危險(xiǎn)。另外,所調(diào)用合約可能調(diào)用其他合約,甚至還會調(diào)用本合約。這就意味著,被調(diào)用合約可以反過來改變本合約的狀態(tài)變量。所以在狀態(tài)變量改變完成之后再調(diào)用外部函數(shù),合約魯棒性就會更好。(?Write your functions in a way that, for example, calls to external functions happen after any changes to state variables in your contract so your contract is not vulnerable to a reentrancy exploit.)

具名函數(shù)和匿名函數(shù)(Named Calls and Anonymous Function)
參數(shù)

函數(shù)的參數(shù)可以起名,如果參數(shù)用{}包裹,可以是任何順序。例子如下:

pragma solidity ^0.4.0;

contract C {
    function f(uint key, uint value) { ... }

    function g() {
        // 名稱參數(shù)
        f({value: 2, key: 3});
    }
}
省略參數(shù)名稱

沒有使用到的參數(shù)(尤其是返回值參數(shù))可以直接省略名稱。這些名稱在堆棧中還是會體現(xiàn)出來,但是不可訪問。

pragma solidity ^0.4.0;

contract C {
    // 省略參數(shù)名稱
    function func(uint k, uint) returns(uint) {
        return k;
    }
}

通過new關(guān)鍵字生成合約

合約可以通過new關(guān)鍵字來創(chuàng)建新的合約。我們必須深入了解被創(chuàng)建的合約,遞歸創(chuàng)建依賴是不允許的。

pragma solidity ^0.4.0;

contract D {
    uint x;
    function D(uint a) payable {
        x = a;
    }
}


contract C {
    D d = new D(4); // 可以看成C合約構(gòu)造函數(shù)的一部分

    function createD(uint arg) {
        D newD = new D(arg);
    }

    function createAndEndowD(uint arg, uint amount) {
        // 在創(chuàng)建的時(shí)候發(fā)送以太幣
        D newD = (new D).value(amount)(arg);
    }
}

在例子中可以看出來,在創(chuàng)建的時(shí)候使用.value()來發(fā)送以太幣,但是不能限制gas的數(shù)量。如果創(chuàng)建失?。ㄓ捎诙褩R绯觯囝~不足或者其他問題),會拋出異常。

表達(dá)式執(zhí)行順序

表達(dá)式的執(zhí)行順序并沒有被指定(一個(gè)表達(dá)式中的節(jié)點(diǎn)的子節(jié)點(diǎn)的順序也是沒有指定的,但是會在父節(jié)點(diǎn)執(zhí)行之前執(zhí)行)。它只保證表達(dá)式按照順序執(zhí)行,并且可以實(shí)現(xiàn)布爾表達(dá)式的短路邏輯。參看操作符優(yōu)先級章節(jié),獲取更多信息。

賦值

解構(gòu)賦值和返回多個(gè)值

Solidity本身支持元組。一個(gè)不同類型的數(shù)組,在編譯的時(shí)候長度一定。這些元組可以用于同時(shí)返回多個(gè)值,也可以用于同時(shí)賦值多個(gè)變量(通常是LValue):

contract C {
    uint[] data;

    function f() returns (uint, bool, uint) {
        return (7, true, 2);
    }

    function g() {
        // 聲明變量和賦值。顯式指明類型是不可以的。
        var (x, b, y) = f();
        // 賦值給定義好的變量
        (x, y) = (2, 7);
        // 變量置換的操作 -- 該方法不適用于非值的storage類型。
        (x, y) = (y, x);
        // 組件可以留空 (變量聲明也可以).
        // 如果元組的以空白結(jié)尾,那么剩下的值就會被拋棄
        (data.length,) = f(); // 設(shè)置長度為7
        // 左側(cè)也同樣可以實(shí)現(xiàn).
        (,data[3]) = f(); // 設(shè)置 data[3] 為 2
        // Components can only be left out at the left-hand-side of assignments, with
        // one exception:
        (x,) = (1,);
        // (1,) 是聲明一元元組的唯一方法, 因?yàn)?(1) 等價(jià)于1.
    }
}
數(shù)組和結(jié)構(gòu)體的難題

對于非值類型的賦值的含義的解釋會稍微麻煩些。賦值給一個(gè)狀態(tài)變量總是生成一個(gè)獨(dú)立的拷貝。換句話說,對本地變量賦值,生成數(shù)據(jù)拷貝的只是針對于初級類型。例如32位大小的靜態(tài)類型。如果結(jié)構(gòu)體或者數(shù)組(包括bytesstring)從狀態(tài)變量賦值給本地變量,本地變量持有源狀態(tài)變量的引用。對本地變量的第二次賦值,不會影響到源狀態(tài)變量,只是修改了引用。對本地變量的成員的修改會影響狀態(tài)。

作用域和聲明

變量被聲明的時(shí)候都會有個(gè)默認(rèn)的初始值,該初始值的byte表示全為0.不管變量的類型是什么,默認(rèn)值都是“零狀態(tài)”。例如,bool的初始值為falseuintint的初始值為0。對于靜態(tài)大小的數(shù)組和從bytes1bytes32。每個(gè)單獨(dú)的元素都被初始化為類型對應(yīng)的默認(rèn)值。最后,對于動態(tài)大小的數(shù)組,bytesstring,默認(rèn)值為空的數(shù)組或者字符串。
在函數(shù)中聲明的變量的作用域是整個(gè)函數(shù),不管它在哪里定義。這是因?yàn)镾olidity的作用域繼承自Javascript的作用域。這和很多語言是相反的,它們的作用域是自它們聲明起到代碼塊結(jié)束。最后,下面的代碼是非法的,會導(dǎo)致編譯器拋出異常,Identifier already decleard:

pragma solidity ^0.4.0;

contract ScopingErrors {
    function scoping() {
        uint i = 0;

        while (i++ < 1) {
            uint same1 = 0;
        }

        while (i++ < 2) {
            uint same1 = 0;// 非法, same1多次定義
        }
    }

    function minimalScoping() {
        {
            uint same2 = 0;
        }

        {
            uint same2 = 0;// 非法,same2多次定義
        }
    }

    function forLoopScoping() {
        for (uint same3 = 0; same3 < 1; same3++) {
        }

        for (uint same3 = 0; same3 < 1; same3++) {// 非法,same3多次定義
        }
    }
}

另外,如果變量定義好之后,就會在函數(shù)開始執(zhí)行之前賦值為默認(rèn)值。所以,以下的代碼是合法的,盡管寫得很爛:

function foo() returns (uint) {
    // baz 隱式初始化為0
    uint bar = 5;
    if (true) {
        bar += baz;
    } else {
        uint baz = 10;// 不會執(zhí)行
    }
    return bar;// 返回 5
}

錯(cuò)誤處理:Asset,Require,Revert和Exceptions

Solidity使用狀態(tài)可回滾異常來處理錯(cuò)誤。這種異??梢曰貪L當(dāng)前調(diào)用(以及子調(diào)用)的所有狀態(tài)變化,并對調(diào)用者標(biāo)記錯(cuò)誤。函數(shù)assertrequire可以被用來檢查條件,如果條件不滿足,將拋出異常。兩者的不同之處在于assert只能用于內(nèi)部錯(cuò)誤,而require用于檢查外部條件(非法的輸入或者外部組件的錯(cuò)誤)。背后的思想是分析工具可以檢查你的合約并且試圖處理那些會引起異常的問題。如果通過了檢查但是還是有錯(cuò)誤,說明你合約的某個(gè)地方有問題。
有另外兩種方法可以觸發(fā)異常:revert函數(shù)可以用于給函數(shù)設(shè)置錯(cuò)誤標(biāo)志并且回滾代碼調(diào)用。在未來,還可以包括調(diào)用revert的時(shí)候產(chǎn)生的錯(cuò)誤細(xì)節(jié)。throw關(guān)鍵字可以用于替代revert()
當(dāng)異常在子函數(shù)調(diào)用的時(shí)候生成,它們會“往上冒泡”(異常會重復(fù)拋出)。但是send和底層函數(shù)call,delegatecallcallcode會在異常的時(shí)候返回false,而不是"往上冒泡"。
截獲異常當(dāng)前還做不到。
在下面的例子中,你可以看到require是如何非常簡便的實(shí)現(xiàn)檢查輸入是否滿足條件以及assert是對內(nèi)部錯(cuò)誤如何使用的。

pragma solidity ^0.4.0;

contract Sharer {
    function sendHalf(address addr) payable returns (uint balance) {
        require(msg.value % 2 == 0); // 只允許偶數(shù)輸入
        uint balanceBeforeTransfer = this.balance;
        addr.transfer(msg.value / 2);
        // 由于交易失敗的時(shí)候會拋出異常,
        // 就不會執(zhí)行到這里, 如果成功了,我們就會少msg.value/2數(shù)目的錢.
        assert(this.balance == balanceBeforeTransfer - msg.value / 2);
        return this.balance;
    }
}

assert類型的異常會在下面的條件下生成:

  1. 數(shù)組越界,索引太大或者是負(fù)數(shù)(例如,x[i]當(dāng)i>x.length或者i<0)
  2. 固定長度的bytesN類型越界訪問。
  3. 被0除或者被0求模。(5 / 0或者23 % 0
  4. 位移時(shí)指定負(fù)數(shù)。
  5. 將太大的數(shù)或者負(fù)數(shù)轉(zhuǎn)換為枚舉類型。
  6. 調(diào)用了初始化為"0狀態(tài)"的函數(shù)變量。
  7. 調(diào)用assert的時(shí)候參數(shù)為false。

require類型的異常會在下面的條件下生成:

  1. 調(diào)用throw
  2. 調(diào)用require,但是參數(shù)執(zhí)行結(jié)果為false
  3. 如果通過消息調(diào)用來調(diào)用函數(shù),但是并沒有正常結(jié)束(例如,gas使用完畢,并沒有符合的函數(shù),或者它自己本身拋出異常),除了使用底層操作call,send,delegatecall或者callcode。底層函數(shù)并不會拋出異常但是會返回false,來指示執(zhí)行失敗。
  4. 如果通過關(guān)鍵字new來創(chuàng)建合約,但是合約沒有正常結(jié)束(參看以上對“不正常結(jié)束”的定義)
  5. 如果外部函數(shù)調(diào)用合約函數(shù),但是合約不包含代碼
  6. 如果通過合約的公共函數(shù)接受以太幣,但是沒有payable標(biāo)識符(包含構(gòu)造函數(shù)和回退函數(shù))。
  7. 通過合約的公有g(shù)etter函數(shù)接收以太幣。
  8. 如果.transfer()失敗了。

在內(nèi)部,Solidity有一個(gè)回滾操作(指令0xfd)來實(shí)現(xiàn)require類型的異常。有一個(gè)非法操作 (指令0xfe)來拋出assert類型的異常。在兩種情況都會導(dǎo)致EVM回滾所有的變化?;貪L的原因是由于并不符合預(yù)期,繼續(xù)執(zhí)行代碼并不安全。因?yàn)槲覀兿氡WC原子操作,最安全的就是直接回滾變化,使得交易沒有發(fā)生過一樣。注意,assert類型的異常會消耗所有的gas,但是自從Metropolis版本起revert類型的異常并不會消耗gas。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容