表达式和控制结构
输入参数和输出参数
与Javascript一样,函数可以将参数作为输入; 与Javascript和C不同,它们也可以返回任意数量的参数作为输出。
输入参数
输入参数的声明方式与变量相同。 作为例外,未使用的参数可以省略变量名称。 例如,假设我们希望我们的合约接受一种具有两个整数的外部调用,我们会写下如下:
pragma solidity ^0.4.0;
contract Simple {
function taker(uint _a, uint _b) {
// do something with _a and _b.
}
}
1
2
3
4
5
6
7
输出参数
输出参数可以在返回关键字之后以相同的语法声明。 例如,假设我们希望返回两个结果:两个给定整数的总和和乘积,那么我们将写:
pragma solidity ^0.4.0;
contract Simple {
function arithmetics(uint _a, uint _b) returns (uint o_sum, uint o_product) {
o_sum = _a + _b;
o_product = _a * _b;
}
}
1
2
3
4
5
6
7
8
可以省略输出参数的名称。 也可以使用return语句指定输出值。 返回语句还能够返回多个值,请参阅返回多个值。 返回参数初始化为零; 如果没有明确设置,它们将保持为零。
输入参数和输出参数可以用作函数体中的表达式。 在那里,他们也可以在任务的左边使用。
控制结构
来自JavaScript的大多数控件结构都可以使用Solidity,除了switch和goto。 所以有: if, else, while, do, for, break, continue, return, ?:,具有C或JavaScript中已知的通常语义。
圆括号不能被省略为条件,但卷边可以在单个语句体上省略。
请注意,没有类型转换从非布尔类型到布尔类型,因为在C和JavaScript中,所以if (1) { ... }无效Solidity。
返回多个值
当一个函数有多个输出参数时, return (v0, v1, ..., vn) can return multiple values. The number of components must be the same as the number of output parameters.可以返回多个值。 组件的数量必须与输出参数的数量相同。
函数调用
内部函数调用
当前合约的功能可以直接调用(“internally”),也可以递归地调用,如这个无意义的例子所示:
pragma solidity ^0.4.0;
contract C {
function g(uint a) returns (uint ret) { return f(); }
function f() returns (uint ret) { return g(7) + f(); }
}
1
2
3
4
5
6
这些函数调用被转换为EVM内部的简单跳转。 这具有当前存储器不被清除的效果,即将存储器引用传递到内部称为功能是非常有效的。 只能在内部调用相同合同的功能。
外部函数调用
表达式this.g(8);和c.g(2); (其中c是合约实例)也是有效的函数调用,但这一次,函数将被称为“外部”,通过消息调用,而不是直接通过跳转。 请注意,这是函数调用不能在构造函数中使用,因为实际的合同尚未创建。
其他合同的职能必须被外部调用。 对于外部调用,所有函数参数都必须复制到内存中。
当调用其他合同的功能时,可以使用特殊选项.value()和.gas()指定与呼叫和气体一起发送的数量:
pragma solidity ^0.4.0;
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)(); }
}
1
2
3
4
5
6
7
8
9
10
11
必须用于info的payable,否则,.value() 选项将不可用。
请注意,InfoFeed(addr)表达式执行显式类型转换,表示“我们知道给定地址的合同类型为InfoFeed”,并且不执行构造函数。 显式类型转换必须非常谨慎地处理。 不要调用,你不知道它的类型合同上的功能。
我们也可以直接使用 function setFeed(InfoFeed _feed) { feed = _feed; }。注意feed.info.value(10).gas(800)只有(本地)设置通过函数调用发送的gas的值和数量,只有末端的括号执行实际调用。
函数调用导致异常,如果所谓的合同没有(在这个意义上,该帐户不包含代码)存在,或者如果被叫合同本身抛出一个异常或熄灭气体。
与另一个合约的任何交互都会产生潜在的危险,特别是如果合约的源代码未提前知道。 目前的合约对被叫合约进行了控制,可能会对任何事情产生影响。 即使被叫合约从已知的母合约中继承,继承合约只需要具有正确的接口。 然而,合约的执行可以是完全任意的,从而构成危险。 另外,如果在第一次呼叫返回之前调用了您的系统的其他合约,甚至重新进入呼叫合约,您应该做好准备。 这意味着被叫合同可以通过其功能改变呼叫合同的状态变量。 编写你的功能,例如,调用外部函数发生在您的合同中状态变量的任何更改后,您的合同不容易受到重入漏洞的攻击。
命名调用和匿名功能参数
函数调用参数也可以通过名称,以任何顺序给出,如果它们被包含在{}中,可以在下面的例子中看到。 参数列表必须与名称和函数声明中的参数列表重合,但可以按任意顺序排列。
pragma solidity ^0.4.0;
contract C {
function f(uint key, uint value) {
// ...
}
function g() {
// named arguments
f({value: 2, key: 3});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
省略函数参数名
可以省略未使用参数的名称(特别是返回参数)。 这些名字仍然存在于堆栈中,但是它们是无法访问的。
pragma solidity ^0.4.0;
contract C {
// 省略参数名称
function func(uint k, uint) returns(uint) {
return k;
}
}
1
2
3
4
5
6
7
8
创建新合约
合同可以使用关键字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) {
// 创建的时候发送ether
D newD = (new D).value(amount)(arg);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如示例所示,可以使用.value()选项将Ether转发到创建,但不可能限制气体量。 如果创建失败(由于堆栈不足,余额不足或其他问题),则抛出异常。
表达式的评估顺序
没有指定表达式的评估顺序(更正式地,表达式树中的一个节点的子节点被评估的顺序未被指定,但是当然在节点本身之前进行评估)。 只保证按照顺序执行语句,完成布尔表达式的短路。 有关详细信息,请参阅运算符优先顺序。
分配
解析分配和返回多个值
内部的Solidity允许元组类型,即在编译时大小不变的潜在不同类型的对象列表。 这些元组可以用来同时返回多个值,并且同时将它们分配给多个变量(或一般的值):
pragma solidity ^0.4.0;
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);
// 互换值的常用技巧对于非价值存储类型不起作用。
(x, y) = (y, x);
// 组件可以省略(也可以用于变量声明)。
// 如果元组以空组件结束,其余的值将被丢弃。
(data.length,) = f(); // 设置长度为 7
// 在左边也可以做同样的事情。
(,data[3]) = f(); // Sets data[3] to 2
// 组件只能在作业的左侧排除,但有一个例外:
(x,) = (1,);
// (1,) 是指定1元组元的唯一方法,因为(1)等于1。
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
并发症数组和结构
赋值的语义对于非数值类型(如数组和结构体)来说有点复杂。 分配给状态变量总是创建一个独立的副本。 另一方面,分配给局部变量仅为基本类型创建独立的副本,即适合32个字节的静态类型。 如果结构体或数组(包括字节和字符串)从状态变量分配给局部变量,则局部变量保存对原始状态变量的引用。 对本地变量的第二个赋值不会修改状态,只会更改引用。 对局部变量的成员(或元素)的分配会改变状态。
范围界定和声明
声明的变量将具有初始默认值,其字节表示全为零。 变量的“默认值”是任何类型的典型“零状态”。 例如,bool的默认值为false。 uint或int类型的默认值为0.对于静态大小的数组和bytes1到bytes32,每个单独的元素将被初始化为与其类型对应的默认值。 最后,对于动态大小的数组,字节和字符串,默认值为空数组或字符串。
在函数中任何地方声明的变量将在整个函数的范围内,无论它在哪里被声明。 这是因为Solidity从JavaScript继承其范围规则。 这与许多语言形成对比,在这些语言中,只有范围被限定到变量才被声明,直到语义块结束。 因此,以下代码是非法的,并导致编译器抛出错误,标识符已声明:
// 这不会编译
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的非法,第二个声明
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
除此之外,如果一个变量被声明,它将在函数的开头被初始化为其默认值。 因此,以下代码是合法的,尽管写得不好:
function foo() returns (uint) {
// baz被隐式初始化为0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;// 永不执行
}
return bar;// 返回 5
}
1
2
3
4
5
6
7
8
9
10
错误处理: Assert, Require, Revert and Exceptions
Solidity使用状态恢复异常来处理错误。 这种异常将撤消在当前调用(及其所有子调用)状态的所有变化,也标志的错误给调用者。 方便函数assert和require可以用于检查条件,如果条件不满足则抛出异常。 assert函数只能用于测试内部错误,并检查不变量。 应该使用require函数来确保满足输入或合同状态变量的有效条件,或者验证从外部合同的调用返回值。 如果正确使用,分析工具可以评估您的合同,以识别将达到失败断言的条件和函数调用。 正常运行的代码不应该达到失败的断言声明; 如果发生这种情况,您的合同中会出现一个您应该修复的错误。
还有另外两种方法可以触发异常:revert函数可用于标记错误并恢复当前的调用。 将来可能还可以包括有关恢复调用中的错误的详细信息。 throw关键字也可以用作revert()的替代方法。
从0.4.13版本,throw关键字已被弃用,将来会被淘汰。
当子调用中发生异常时,它们会自动“冒泡”(即异常被重新引导)。 此规则的异常是发送和低级函数调用,委托调用和调用代码 - 在异常情况下返回false而不是“冒泡”。
作为EVM设计的一部分,如果调用帐户不存在,低级呼叫,委托呼叫和呼叫代码将返回成功。 如果需要,必须在调用前检查是否存在。
捕捉异常还不可能。
在下面的示例中,您可以看到如何使用需求来轻松检查输入条件,以及断言如何用于内部错误检查:
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);
// 由于转移抛出异常失败,不能在这里回调,我们应该没有办法仍然有一半的钱。
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
在以下情况下会生成assert样式异常:
1.如果您以太大或负数索引访问数组(比如x[i] 在 i >= x.length or i < 0)
2.如果您以太大或负数索引访问固定长度的bytesN。
3.如果您划分或模数为零(例如5/0或23%0)。
4.如果通过负移动量。
5.如果转换过大或负进枚举类型的值。
6.如果调用内部函数类型的零初始化变量。
7.如果您使用一个评估为false的参数调用assert。
在以下情况下会生成require-style异常:
1.调用throw
2.调用require并且条件为false
3.如果您通过消息调用调用函数,但是它没有正确完成(即,用尽了气体,没有匹配的功能,或者引发异常本身),除非使用低级别的操作call,send,delegatecall或callcode。 低级别的操作不会抛出异常,而是通过返回false来指示失败。
4.如果您使用new关键字,但合同创建未正常完成创建合同(见上文的“无法正常完成”的定义)。
5.如果您执行一个定向不包含代码的合同的外部函数调用。
6.如果您的合约通过无功能修改器(包括构造函数和后备功能)通过公共函数接收Ether。
7.如果你的合约通过一个公共的getter函数接收Ether。
8.如果.transfer()失败。
在内部,Solidity对require-style异常执行一个还原操作(0xfd指令),并执行一个无效操作(指令0xfe)来抛出一个assert-style异常。 在这两种情况下,这将导致EVM恢复对状态所做的所有更改。 恢复原因是没有安全的方式来继续执行,因为没有发生预期的效果。 因为我们要保留交易的原子性,所以最安全的做法是恢复所有的变化,并使整个事务(或至少调用)无效。 请注意,断言风格的异常消耗调用中可用的所有gas,而需求风格的异常将不会消耗从大都会版本开始的任何gas。 |
|