深入理解Solidity——函式呼叫和賦值
函式呼叫(Function Calls)
內部函式呼叫(Internal Function Calls)
當前合約的函式可以直接內部(Internal)呼叫,也可以遞迴地呼叫,比如這個古怪的例子:
pragma solidity ^0.4.16;
contract C {
function g(uint a) public pure returns (uint ret) { return f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
這些函式呼叫在EMV裡翻譯成簡單的jumps語句。結果是當前記憶體沒有被清除, 即通過記憶體引用的函式是非常高效的。只有同一個合約裡的函式可在內部被呼叫。
外部函式呼叫(External Function Calls)
表示式 this.g(8);
和 c.g(2);
(c是一個合約的例項) 也是有效的函式呼叫,不過被稱為外部函式呼叫, via a message call and not directly via jumps. 請注意建構函式不能這樣被呼叫, 因為這時合約還沒有被建立。
其他合約的函式也是外部呼叫。對於外部呼叫,所有函式引數必須被複制到記憶體中。
當呼叫其他合約的函式時, 傳送的金額(Wei)和gas可以通過.value()
和 .gas()
來設定:
pragma solidity ^0.4.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(address addr) public { feed = InfoFeed(addr); }
function callFeed() public { feed.info.value(10).gas(800)(); }
}
修飾符payable
用在函式info
上,因為如果不這麼做的話,.value()
將不可用。
注意表示式InfoFeed(addr)
InfoFeed
”並且不執行建構函式。 必須非常謹慎地處理顯式型別轉換。永遠不要在不確定型別的情況下呼叫一個合同中的函式。
我們也可以直接使用函式setFeed(InfoFeed _feed) { feed = _feed; }
。 注意feed.info.value(10).gas(800)
只是在本地設定函式呼叫時需要的gas的值和數量,只有最後的括號結束後才完成實際的呼叫。
如果呼叫的合約不存在(帳戶不包含程式碼)、被呼叫的合約內部丟擲異常或gas不足,會導致異常。
警告 |
---|
與另一合約的任何互動都會帶來潛在的危險,特別是如果在事先不知道合約的原始碼。當前合約將控制權移交給被呼叫的合約,任何事情都將可能發生。即使被呼叫的合約繼承自已知的父合約,繼承的合約也只需要具有正確的介面,其內部實現可以是任意的,從而對呼叫者構成威脅。 |
另外,在呼叫你的系統的其他合同或在第一次呼叫返回之前回到當前合約時也要小心。因為被呼叫的合約可以通過其函式呼叫,改變當前合約的狀態變數。因此,在對你的合約中狀態變數進行任何更改之後,呼叫外部函式,這樣你的合約就不易受到重用。 |
具名呼叫和匿名函式引數(Named Calls and Anonymous Function Parameters)
函式呼叫引數如果被包含在{ }
中,可以以任何順序給出。引數列表必須與函式宣告中的引數列表重合,但可以按任意順序排列:
pragma solidity ^0.4.0;
contract C {
function f(uint key, uint value) public {
// do something
}
function g() public {
// named arguments
f({value: 2, key: 3});
}
}
省略函式引數名(Omitted Function Parameter Names)
可以省略未使用的引數(尤其是返回值)的名稱。這些引數仍然存在於堆疊上,但它們是不可訪問的。
pragma solidity ^0.4.16;
contract C {
// 省略引數名
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
通過new
建立合約(Creating Contracts via new
)
合約可以使用new
關鍵字建立新合約。必須事先知道要建立的合約的完整程式碼,因此遞迴建立依賴(recursive creation-dependencies)是不可能的。
pragma solidity ^0.4.0;
contract D {
uint x;
function D(uint a) public payable {
x = a;
}
}
contract C {
D d = new D(4); // 將作為C建構函式的一部分執行。
function createD(uint arg) public {
D newD = new D(arg);
}
function createAndEndowD(uint arg, uint amount) public payable {
// 建立同時傳送ether
D newD = (new D).value(amount)(arg);
}
}
正如在這個例子中所看到的,建立D
的例項時使用.value()
可以直接傳送ether,但是不能限制gas量。如果由於堆疊、沒有足夠的餘額或其他問題建立失敗,則引發異常。
表示式的計算次序(Order of Evaluation of Expressions)
表示式的計算順序是不確定的(準確地說是, 順序表示式樹中的子節點表示式計算順序是不確定的的, 但他們對節點本身,計算表示式順序當然是確定的)。只保證語句執行順序,以及布林表示式的短路規則。
賦值(Assignment)
析構賦值並返回多個值(Destructuring Assignments and Returning Multiple Values)
Solidity內部允許元組型別,即一系列的不同型別的物件的大小在編譯時是一個常量。這些元組可以用來同時返回多個值,並且同時將它們分配給多個變數(或左值運算):
contract C {
uint[] data;
function f() returns (uint, bool, uint) {
return (7, true, 2);
}
function g() {
// Declares and assigns the variables. Specifying the type explicitly is not possible. 宣告和賦值變數,不必顯示定義型別
var (x, b, y) = f();
// Assigns to a pre-existing variable. 賦值給已經存在的變數
(x, y) = (2, 7);
// Common trick to swap values -- does not work for non-value storage types. 交換值的技巧-對非值儲存型別不起作用
(x, y) = (y, x);
// Components can be left out (also for variable declarations). 元素可排除(對變數宣告也適用)
// If the tuple ends in an empty component, 如果元組是以空元素為結尾
// the rest of the values are discarded. 值的其餘部分被丟棄
(data.length,) = f(); // Sets the length to 7 設定長度為7
// The same can be done on the left side. 同樣可以在左側做
(,data[3]) = f(); // Sets data[3] to 2 將data[3] 設為2
// Components can only be left out at the left-hand-side of assignments, with
// one exception: 元件只能在賦值的左邊被排除,有一個例外
(x,) = (1,);
// (1,) is the only way to specify a 1-component tuple, because (1) is (1,)是定義一個元素的元組,(1)是等於1
// equivalent to 1.
}
}
陣列和結構體的組合(Complications for Arrays and Structs)
對於像陣列和結構體這樣的非值型別,賦值的語義更復雜些。賦值到一個狀態變數總是需要建立一個獨立的拷貝。另一方面,對基本型別來說,賦值到一個區域性變數需要建立一個獨立的拷貝, 即32位元組的靜態型別。如果結構體或陣列(包括bytes
和string
)從狀態變數被賦值到一個區域性變數, 區域性變數則儲存原始狀態變數的引用。第二次賦值到區域性變數不修改狀態,只改變引用。賦值到區域性變數的成員(或元素)將改變狀態。