【Solidity】5.表达式和控制结构 - 深入理解Solidity

表达式和控制结构

输入参数和输出参数

与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。

相关文章

相关标签/搜索