【Solidity】5.表达式和控制结构

来源:互联网 发布:tensorflow 对象关系 编辑:程序博客网 时间:2024/06/03 16:14

索引

  • 【Solidity】1.一个Solidity源文件的布局
  • 【Solidity】2.合约的结构体
  • 【Solidity】3.类型
  • 【Solidity】4.单位和全局可变量
  • 【Solidity】5.表达式和控制结构
  • 【Solidity】6. 合约
  • 【Solidity】7. 部件
  • 【Solidity】8. 杂项

表达式和控制结构

输入参数和输出参数

与Javascript一样,函数可以将参数作为输入; 与Javascript和C不同,它们也可以返回任意数量的参数作为输出。

输入参数

输入参数的声明方式与变量相同。 作为例外,未使用的参数可以省略变量名称。 例如,假设我们希望我们的合约接受一种具有两个整数的外部调用,我们会写下如下:

pragma solidity ^0.4.0;contract Simple {    function taker(uint _a, uint _b) {        // do something with _a and _b.    }}

输出参数

输出参数可以在返回关键字之后以相同的语法声明。 例如,假设我们希望返回两个结果:两个给定整数的总和和乘积,那么我们将写:

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;    }}

可以省略输出参数的名称。 也可以使用return语句指定输出值。 返回语句还能够返回多个值,请参阅返回多个值。 返回参数初始化为零; 如果没有明确设置,它们将保持为零。

输入参数和输出参数可以用作函数体中的表达式。 在那里,他们也可以在任务的左边使用。

控制结构

来自JavaScript的大多数控件结构都可以使用Solidity,除了switchgoto。 所以有: 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(); }}

这些函数调用被转换为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)(); }}

必须用于infopayable,否则,.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});    }}

省略函数参数名

可以省略未使用参数的名称(特别是返回参数)。 这些名字仍然存在于堆栈中,但是它们是无法访问的。

pragma solidity ^0.4.0;contract C {    // 省略参数名称    function func(uint k, uint) returns(uint) {        return k;    }}

创建新合约

合同可以使用关键字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);    }}

如示例所示,可以使用.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。    }}

并发症数组和结构

赋值的语义对于非数值类型(如数组和结构体)来说有点复杂。 分配给状态变量总是创建一个独立的副本。 另一方面,分配给局部变量仅为基本类型创建独立的副本,即适合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的非法,第二个声明        }    }}

除此之外,如果一个变量被声明,它将在函数的开头被初始化为其默认值。 因此,以下代码是合法的,尽管写得不好:

function foo() returns (uint) {    // baz被隐式初始化为0    uint bar = 5;    if (true) {        bar += baz;    } else {        uint baz = 10;// 永不执行    }    return bar;// 返回 5}

错误处理: 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;    }}

在以下情况下会生成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.如果您通过消息调用调用函数,但是它没有正确完成(即,用尽了气体,没有匹配的功能,或者引发异常本身),除非使用低级别的操作callsenddelegatecallcallcode。 低级别的操作不会抛出异常,而是通过返回false来指示失败。
4.如果您使用new关键字,但合同创建未正常完成创建合同(见上文的“无法正常完成”的定义)。
5.如果您执行一个定向不包含代码的合同的外部函数调用。
6.如果您的合约通过无功能修改器(包括构造函数和后备功能)通过公共函数接收Ether。
7.如果你的合约通过一个公共的getter函数接收Ether。
8.如果.transfer()失败。

在内部,Solidity对require-style异常执行一个还原操作(0xfd指令),并执行一个无效操作(指令0xfe)来抛出一个assert-style异常。 在这两种情况下,这将导致EVM恢复对状态所做的所有更改。 恢复原因是没有安全的方式来继续执行,因为没有发生预期的效果。 因为我们要保留交易的原子性,所以最安全的做法是恢复所有的变化,并使整个事务(或至少调用)无效。 请注意,断言风格的异常消耗调用中可用的所有gas,而需求风格的异常将不会消耗从大都会版本开始的任何gas。