Expressions and Control Structures
July 18, 2018 · View on GitHub
Input Parameters and Output Parameters
与在Javascript中一样,函数可以作为输入的参数; 与Javascript和C不同,函数也可以返回任意数量的参数作为输出。
Input Parameters
输入参数的声明方式与变量相同。有个例外,未使用的参数可以省略变量名称。例如,假设我们的合约接受带有两个整数参数的外部调用,函数如下:
pragma solidity ^0.4.16;
contract Simple {
function taker(uint _a, uint _b) public pure {
// do something with _a and _b.
}
}
Output Parameters
在 returns 关键字之后,可以用相同的语法声明输出参数。例如,我们希望返回两个结果:两个给定整数的和和乘积,可以这么写:
pragma solidity ^0.4.16;
contract Simple {
function arithmetics(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
可以省略输出参数的名称,输出值也可以使用 return 语句指定。return 语句还能够返回多个值。返回参数初始化为零;如果未显式设置,则它们保持为零。
输入参数和输出参数可以用作函数体中的表达式。在那里,它们也可以在赋值的左手边使用。
Control Structures
除了 swich 和 goto, JavaScript 中的控制结构都可以在 Solidity 中使用。一般有:if, else, while, do, for, break, continue, return, ? :
注意,在C和JavaScript中没有从非布尔到布尔类型的类型转换,所以如果(1){…} 在Solidity 是无效的。
Returning Multiple Values
当函数具有多个输出参数时,返回(V0,V1,…,VN)可以返回多个值。组件的数量必须与输出参数的数目相同。
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(); }
}
这些函数调用被转换成EVM内的简单跳转。这样做的结果是当前的内存不被清除,即__传递内存引用到内部调用的函数是非常有效的__。只有同一合约的功能才能在内部调用。
External Function Calls
this.g(8) 和 c.g(2) (c 为一个合约实例)这些表达式也是一个有效的函数调用,这种函数调用是外部的(externally),是通过消息传递而不是简单的EVM跳转实现的。
请注意,关于 this 的调用不能在构造函数中使用,因为此时合约实例并未创建。其他合约的函数必须通过外部调用,对于外部函数的调用,所有的参数是复制到内存中的。
当调用其它合约的函数时,调用所发送的Wei的数量以及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)(); }
}
info 函数必须要有 payable 修饰词。注意,表达式infoFEED(ADDR)执行显式类型转换,说明“我们知道给定地址的合约是InfoFEED”,这里不会执行构造函数。必须非常谨慎地处理显式类型转换。不要在你不确定其类型的合约中调用函数。
我们可以直接使用 setFeed(InfoFeed _feed) { feed = _feed; }。feed.info.value(10).gas(800) 仅(本地)设置与函数调用一起发送的value的值和gas的量,并且只有末端的圆括号执行实际调用。如果调用的合约不存在,或者被调用的合约本身抛出异常或没有gas了,此时函数的调用会导致异常。
任何与另一合约的相互作用都会带来潜在的危险,特别是如果合约的源代码事先不知道。当前的合同将控制权移交给另一个合约(该合约做任何事都是有可能的)。即使被调用的合约从已知的父合约继承,继承合约只需要具有正确的接口。然而,合约的实现却可以是完全随意的,因而构成危险。另外,注意其可能会调用你系统的其他合约,甚至在第一次调用返回之前就返回到所调用的合约。这意味着被调用的合约可以通过其功能改变调用的合约的状态变量。编写您的函数,例如,在你的合约中对状态变量进行任何更改之后,调用外部函数,这样您的合同就不易受到重用。
Named Calls and Anonymous Function Parameters
函数调用参数可以以任何顺序给出,如果它们被包含在{}中,如下例所示。参数列表必须与函数声明中的参数列表重合,但可以按任意顺序排列。
pragma solidity ^0.4.0;
contract C {
function f(uint key, uint value) public {
// ...
}
function g() public {
// named arguments
f({value: 2, key: 3});
}
}
Omitted Function Parameter Names
可以省略未使用的参数(特别是返回参数)的名称。这些参数仍然存在于堆栈上,但它们是不可访问的。
pragma solidity ^0.4.16;
contract C {
// omitted name for parameter
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
Creating Contracts via new
可以使用new关键字创建合约实例,要创建的合约的完整代码必须事先知道,因此递归依赖创建是不可能的。
pragma solidity ^0.4.0;
contract D {
uint x;
function D(uint a) public payable {
x = a;
}
}
contract C {
D d = new D(4); // will be executed as part of C's constructor
function createD(uint arg) public {
D newD = new D(arg);
}
function createAndEndowD(uint arg, uint amount) public payable {
// Send ether along with the creation
D newD = (new D).value(amount)(arg);
}
}
正如在这个例子中所看到的,在使用.Value()选项创建D的实例时,可以向其发生ether,但是不能限制 gas 量。如果创建失败(由于out-of-stack、余额不足或其他问题),则会引发异常。
Order of Evaluation of Expressions
表达式的求值顺序是不确定的(更正式地说,未指定表达式树中一个节点的子节点的计算顺序,但它们当然在节点本身之前进行求值)。 只保证语句按顺序执行,并且完成布尔表达式的短路。
Assignment
## Destructuring Assignments and Returning Multiple Values
Solidity内部支持元组类型,即不同类型的对象列表,其大小在编译时是常量。这些元组可以用于同时返回多个值,并同时将它们分配给多个变量:
pragma solidity ^0.4.0;
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
// The same can be done on the left side.
(,data[3]) = f(); // Sets data[3] to 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
// equivalent to 1.
}
}
Complications for Arrays and Structs
对于数组和结构等非值类型,赋值语义有点复杂。赋值给状态变量总是创建一个独立的副本。另一方面,赋值给局部变量只为基本类型创建独立的副本,如:32字节的静态类型。如果将结构或数组(包括 bytes 和 string)从状态变量分配给局部变量,则本地变量保存对原始状态变量的引用。对局部变量的第二次赋值只更改引用,不修改状态。对局部变量的成员(或元素)的赋值会改变其状态。
Scoping and Declarations
声明的变量将具有初始默认值,其字节表示全为零。 变量的“默认值”是任何类型的典型“zero-state”。例如,bool的默认值为false。 uint或int类型的默认值为0.对于静态大小的数组和bytes1到bytes32,每个单独的元素将初始化为与其类型对应的默认值。最后,对于动态大小的数组,bytes 和 string,默认值是空数组或空字符串。
无论在函数内任何地方声明的变量其作用域都将是整个函数范围。这是因为Solidity从JavaScript继承了它的范围规则。这与许多语言形成对比,在这些语言中,变量仅在声明它们的范围内,直到语义块结束。 因此,以下代码是非法的并导致编译器抛出标识符已经声明(Identifier already declared)的错误:
// This will not compile
pragma solidity ^0.4.0;
contract ScopingErrors {
function scoping() {
uint i = 0;
while (i++ < 1) {
uint same1 = 0;
}
while (i++ < 2) {
uint same1 = 0;// Illegal, second declaration of same1
}
}
function minimalScoping() {
{
uint same2 = 0;
}
{
uint same2 = 0;// Illegal, second declaration of same2
}
}
function forLoopScoping() {
for (uint same3 = 0; same3 < 1; same3++) {
}
for (uint same3 = 0; same3 < 1; same3++) {// Illegal, second declaration of same3
}
}
}
除此之外,如果声明了变量,它将在函数的开头初始化为其默认值。因此,以下代码是合法的,尽管写得不好:
function foo() returns (uint) {
// baz is implicitly initialized as 0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;// never executes
}
return bar;// returns 5
}
Error handling: Assert, Require, Revert and Exceptions
Solidity使用 state-reverting 异常来处理错误。这样的异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。函数assert和require可用于检查条件并在不满足条件时抛出异常。 assert函数只应用于测试内部错误,并检查不变量。 require函数应用于确保满足有效条件(如输入或合约状态变量),或验证调用外部合约的返回值。如果使用得当,分析工具可以评估我们的合约,以确定将达到失败断言的条件和函数调用。正常运行的代码永远不会满足assert语句; 如果发生这种情况,即表明我们的合约中有bug。
还有两种触发异常的方法:revert 函数可以用来标记错误并恢复当前调用。在将来,还可能包含关于调用回复的错误的详细信息。抛出关键字也可以用作revert函数的替代。
当异常发生在子调用中时,它们会自动“冒泡”(即异常被重新抛出)。这个规则的例外是 send 和 the low-level functions call, delegatecall and callcode——在异常情况下返回false,而不是冒泡。
Solidity 不支持异常的捕获。
在以下示例中,我们可以看到如何使用require来轻松检查输入条件以及 assert 如何用于内部错误检查:
pragma solidity ^0.4.0;
contract Sharer {
function sendHalf(address addr) payable returns (uint balance) {
require(msg.value % 2 == 0); // Only allow even numbers
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
// Since transfer throws an exception on failure and
// cannot call back here, there should be no way for us to
// still have half of the money.
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
在以下情况下生成 assert-style 异常:
-
数组访问越界时
-
字节数组访问越界时
-
用0来做除数或取模时
-
左右做负数移位时
-
将过大的值或负值转换为枚举类型
-
调用内部函数类型的零初始化变量。
-
assert 函数的参数表达式为false
在以下情况下生成 require-style 异常:
-
调用到throw
-
调用的 require 函数的参数表达式为false
-
如果通过消息调用去调用函数,但它没有完成正常调用(即,runs out of gas, has no matching function, or throws an exception itself),除了使用 a low level operation call、send、delegatecall或callcode。The low level operations 从不抛出异常,而是通过返回false指示失败。
-
如果使用 new 关键字创建合约失败
-
如果执行一个外部函数调用,它指向一个不包含代码的合约
-
如果合约通过public函数接收 ether,而该函数没有 payable 修饰(包括 constructor 函数和 fallback 函数)
-
如果public 的getter 方法接收 ether
-
如果 a .transfer() 函数调用失败
在内部,Solidity 对 require-style 异常执行 revert 操作(指令0xFD);对于 assert-style 异常会执行一个无效的操作(指令 0xfe)来抛出该异常。在这两种情况下,EVM会还原对状态所做出的所有更改。这么做的原因是程序的执行没有达到预期的效果,此时方法不应该继续执行下去。因为我们想保留交易的原子性,最安全的做法是恢复所有的改变,并使整个交易(或至少调用)没有效果。注意,assert-style 异常消耗所有可用的gas,而require-style 异常不会消耗任何 gas(从Meavigas发布开始)。