原文链接
date:20170707
输入参数和输出参数
在javascript中,函数可以传递参数作为输入;Solidity于javascript和C不同,可以返回任意数量的参数作为输出。
输入参数
输入参数和变量的定义一样。在表达式中,未使用到的参数可以忽略变量名称。例如,我们合约里的一个函数,有两个整形参数,我们可以写成这样:
contract Simple {
function taker(uint _a, uint _b) {
// 对参数_a和_b进行计算
}
}
输出参数
输出参数可以在returns
之后通过同样的语法来定义。例如,我们如果想要返回两个结果:给定整形参数的和与乘积,我们可以这么写:
contract Simple {
function arithmetics(uint _a, uint _b) returns (uint o_sum, uint o_product) {
o_sum = _a + _b;
o_product = _a * _b;
}
}
输出参数的名称可以省略。输出值可以直接通过return
表达式指定。return
表达式可以返回多个值,参看返回多个值章节。返回参数被初始化为0,如果它们没有被明确指定,就会保持0.
输入参数和输出参数可以在函数体中使用。可以被重新赋值。(?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中使用,除了switch
和goto
。所以有:if
,else
,while
,do
,for
,break
,continue
,return
,?:
,它们的语义和C或者javascript一致。
条件判断的圆括号不可以省略,但是花括号,如果只有一条语句的时候,可以省略。
需要注意的是在Solidity中并没有在C和javascript中的非布尔类型转换为布尔类型。所以if (1) { ... }
在Solidity中并不试用。
返回多个值
当函数有多个输出,return (v0,v1, ..., vn)
可以返回多个值。组件数量和输出参数必须一致。
函数调用
内部函数调用
当前合约的函数可以直接调用(“内部调用”),也可以递归调用。例子如下所示:
ontract C {
function g(uint a) returns (uint ret) { return f(); }
function f() returns (uint ret) { return g(7) + f(); }
}
这些函数调用会被转换为在EVM(以太坊虚拟机)中的简单跳转。它的效果是当前的内存并没有清除,所以在内部传递内存引用是很高效的。只有同个合约内的函数可以在内部调用。
外部函数调用
表达式this.g(8)
和c.g(2)
(c
是一个合约实例)都是有效的函数调用。但是这个时候,这个调用就被称为“外部函数调用”。通过一个消息调用,并不是简单的直接的跳转。注意函数调用this
是不能用在构造函数中的,因为当前合约并未生成。
其他合约的函数调用也是外部调用。所有的外部函数调用,都会把参数拷贝到内存中。
当我们调用其他合约的函数,一定数量的Wei会随着调用发送出去,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
函数必须使用payable
标识符,否则,.value()
就无法使用。
需要注意的是在表达式InfoFeed(addr)
中,有一个显式转换:我们知道所给定地址的合约类型是InfoFeed
,并且不执行构造函数。显式转换必须特别小心。不要在不确定合约类型的时候调用合约里的函数。
我们可以直接使用function setFeed(InfoFeed _feed){feed = _feed;}
。表达式feed.info.value(10).gas(800)
只是设置了值和gas的数目。最后的圆括号才是真正的函数调用。
如果所调用的合约并不存在(账户并不包含代码),或者被调用函数本身抛出异常,或者gas不足,那函数调用会发生异常。
警告:任何与其他合约的交互都会有潜在的危险,尤其是我们对所调用合约的代码不了解的时候。当前合约把控制权交给所调用的合约,那么拥有控制权的合约可以做很多事情。即使该合约继承自所知的一个合约,继承合约只是需要实现正确的接口。接口到底怎么实现,完全是任意的,所以会有危险。另外,所调用合约可能调用其他合约,甚至还会调用本合约。这就意味着,被调用合约可以反过来改变本合约的状态变量。所以在状态变量改变完成之后再调用外部函数,合约鲁棒性就会更好。(?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.)
具名函数和匿名函数(Named Calls and Anonymous Function)
参数
函数的参数可以起名,如果参数用{}
包裹,可以是任何顺序。例子如下:
pragma solidity ^0.4.0;
contract C {
function f(uint key, uint value) { ... }
function g() {
// 名称参数
f({value: 2, key: 3});
}
}
省略参数名称
没有使用到的参数(尤其是返回值参数)可以直接省略名称。这些名称在堆栈中还是会体现出来,但是不可访问。
pragma solidity ^0.4.0;
contract C {
// 省略参数名称
function func(uint k, uint) returns(uint) {
return k;
}
}
通过new
关键字生成合约
合约可以通过new
关键字来创建新的合约。我们必须深入了解被创建的合约,递归创建依赖是不允许的。
pragma solidity ^0.4.0;
contract D {
uint x;
function D(uint a) payable {
x = a;
}
}
contract C {
D d = new D(4); // 可以看成C合约构造函数的一部分
function createD(uint arg) {
D newD = new D(arg);
}
function createAndEndowD(uint arg, uint amount) {
// 在创建的时候发送以太币
D newD = (new D).value(amount)(arg);
}
}
在例子中可以看出来,在创建的时候使用.value()
来发送以太币,但是不能限制gas的数量。如果创建失败(由于堆栈溢出,余额不足或者其他问题),会抛出异常。
表达式执行顺序
表达式的执行顺序并没有被指定(一个表达式中的节点的子节点的顺序也是没有指定的,但是会在父节点执行之前执行)。它只保证表达式按照顺序执行,并且可以实现布尔表达式的短路逻辑。参看操作符优先级章节,获取更多信息。
赋值
解构赋值和返回多个值
Solidity本身支持元组。一个不同类型的数组,在编译的时候长度一定。这些元组可以用于同时返回多个值,也可以用于同时赋值多个变量(通常是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);
// 组件可以留空 (变量声明也可以).
// 如果元组的以空白结尾,那么剩下的值就会被抛弃
(data.length,) = f(); // 设置长度为7
// 左侧也同样可以实现.
(,data[3]) = f(); // 设置 data[3] 为 2
// Components can only be left out at the left-hand-side of assignments, with
// one exception:
(x,) = (1,);
// (1,) 是声明一元元组的唯一方法, 因为 (1) 等价于1.
}
}
数组和结构体的难题
对于非值类型的赋值的含义的解释会稍微麻烦些。赋值给一个状态变量总是生成一个独立的拷贝。换句话说,对本地变量赋值,生成数据拷贝的只是针对于初级类型。例如32位大小的静态类型。如果结构体或者数组(包括bytes
和string
)从状态变量赋值给本地变量,本地变量持有源状态变量的引用。对本地变量的第二次赋值,不会影响到源状态变量,只是修改了引用。对本地变量的成员的修改会影响状态。
作用域和声明
变量被声明的时候都会有个默认的初始值,该初始值的byte表示全为0.不管变量的类型是什么,默认值都是“零状态”。例如,bool
的初始值为false
。uint
或int
的初始值为0
。对于静态大小的数组和从bytes1
到bytes32
。每个单独的元素都被初始化为类型对应的默认值。最后,对于动态大小的数组,bytes
和string
,默认值为空的数组或者字符串。
在函数中声明的变量的作用域是整个函数,不管它在哪里定义。这是因为Solidity的作用域继承自Javascript的作用域。这和很多语言是相反的,它们的作用域是自它们声明起到代码块结束。最后,下面的代码是非法的,会导致编译器抛出异常,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多次定义
}
}
}
另外,如果变量定义好之后,就会在函数开始执行之前赋值为默认值。所以,以下的代码是合法的,尽管写得很烂:
function foo() returns (uint) {
// baz 隐式初始化为0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;// 不会执行
}
return bar;// 返回 5
}
错误处理:Asset,Require,Revert和Exceptions
Solidity使用状态可回滚异常来处理错误。这种异常可以回滚当前调用(以及子调用)的所有状态变化,并对调用者标记错误。函数assert
和require
可以被用来检查条件,如果条件不满足,将抛出异常。两者的不同之处在于assert
只能用于内部错误,而require
用于检查外部条件(非法的输入或者外部组件的错误)。背后的思想是分析工具可以检查你的合约并且试图处理那些会引起异常的问题。如果通过了检查但是还是有错误,说明你合约的某个地方有问题。
有另外两种方法可以触发异常:revert
函数可以用于给函数设置错误标志并且回滚代码调用。在未来,还可以包括调用revert
的时候产生的错误细节。throw
关键字可以用于替代revert()
。
当异常在子函数调用的时候生成,它们会“往上冒泡”(异常会重复抛出)。但是send
和底层函数call
,delegatecall
和callcode
会在异常的时候返回false
,而不是"往上冒泡"。
截获异常当前还做不到。
在下面的例子中,你可以看到require
是如何非常简便的实现检查输入是否满足条件以及assert
是对内部错误如何使用的。
pragma solidity ^0.4.0;
contract Sharer {
function sendHalf(address addr) payable returns (uint balance) {
require(msg.value % 2 == 0); // 只允许偶数输入
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
// 由于交易失败的时候会抛出异常,
// 就不会执行到这里, 如果成功了,我们就会少msg.value/2数目的钱.
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
assert
类型的异常会在下面的条件下生成:
- 数组越界,索引太大或者是负数(例如,
x[i]
当i>x.length
或者i<0
) - 固定长度的
bytesN
类型越界访问。 - 被0除或者被0求模。(
5 / 0
或者23 % 0
) - 位移时指定负数。
- 将太大的数或者负数转换为枚举类型。
- 调用了初始化为"0状态"的函数变量。
- 调用
assert
的时候参数为false
。
require
类型的异常会在下面的条件下生成:
- 调用
throw
- 调用
require
,但是参数执行结果为false
。 - 如果通过消息调用来调用函数,但是并没有正常结束(例如,gas使用完毕,并没有符合的函数,或者它自己本身抛出异常),除了使用底层操作
call
,send
,delegatecall
或者callcode
。底层函数并不会抛出异常但是会返回false
,来指示执行失败。 - 如果通过关键字
new
来创建合约,但是合约没有正常结束(参看以上对“不正常结束”的定义) - 如果外部函数调用合约函数,但是合约不包含代码
- 如果通过合约的公共函数接受以太币,但是没有
payable
标识符(包含构造函数和回退函数)。 - 通过合约的公有getter函数接收以太币。
- 如果
.transfer()
失败了。
在内部,Solidity有一个回滚操作(指令0xfd
)来实现require
类型的异常。有一个非法操作 (指令0xfe
)来抛出assert
类型的异常。在两种情况都会导致EVM回滚所有的变化。回滚的原因是由于并不符合预期,继续执行代码并不安全。因为我们想保证原子操作,最安全的就是直接回滚变化,使得交易没有发生过一样。注意,assert
类型的异常会消耗所有的gas,但是自从Metropolis版本起revert
类型的异常并不会消耗gas。