solidity语言学习(8) —— 表达式和控制结构

输入参数和输出参数

和JavaScript一样,solidity的函数也可以使用参数作为输入;但与JavaScript和C不同的是,函数可能也会返回任意数量的参数作为输出
输入参数
输入参数声明的方式和变量是一样的。但是例外的是,不使用的参数可以省略变量名。比如,假设我们想要我们的合约接受一种含两个整形的外部调用,我们可以这样写:

pragma solidity ^0.4.16;

contract Simple{
   function taker(uint _a,uint _b) public pure{
        //do something with _a and _b
    }
}

输出参数
输出参数能在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 指令也同样适用于返回多个值,详见后面的Returning Multiple Values.返回参数将被初始化为0;如果他们没有被特别设定,那么他们将保持为0;。
输入和输出参数能在函数体被用作表达式,也能用在赋值的左边。

控制结构(Control Structures)

大部分JavaScript的控制结构都适用于Solidity,除了switch和goto。所以if,else,while,do,for,break,continue,return,?=,都可以使用,与C和JavaScript中语义相同。

圆括号在声明条件时不能够省略,但花括号在仅声明时可以被省略。

注意这里没有像C或者JavaScript里面的从非bool到bool型的类型转换,所以诸如 if (1) {...}在这里是不合法的。

返回多个值(Returning Multiple Values)
当一个函数由多个输出值时,return (v0,v1,...,vn)能够返回多个值。其中每个成员的数量都必须与(声明的)输出参数的数量相同。

函数调用

内部函数调用
当前合约的函数能够被直接调用,我们可以在下面这个无意义的例子中看到:

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中都被转换为简单的转移。这一点当当前内存并不clear时有很大影响(This has has effect that the current memory is not cleared),比如说,将内存引用传递给声明为内部的函数是非常有效率的。内部调用仅仅发送在同一合约调用该合约中的函数时。

外部函数调用(External Function Calls)
诸如 this.g(8); and c.g(2)(c是一个合约实例)都是合法的函数调用,但是条件是这些函数被声明为“externally”,通过一个消息来调用 或者其他非直接的调用。请注意 使用 this 的函数声明方式不能够被用于结构体,因为实际的合约还没有被创造出来(我估计就是编译时生成结构体时还没有生成函数)

其他合约中的函数必须被声明为外部函数(才能被调用)。对于外部调用来说,所有函数参数都必须被拷贝到内存中。

当调用其他合约的函数时,Wei的余额将随着调用一起被传递过去,并且gas(和gas的value)能使用一些特殊的选项 .value() 和 .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)(); }
}

修饰 payable 必须被使用与 info,否则.value()选项将不合法。

注意,InfoFeed(addr)的表述显示一个显式的类型转换,说明“我们已知在指定地址合约的类型是InfoFeed”并且这并不会执行一个结构体(?)。显式的类型转换必须受到特么注意。而且当你不知到一个合约的类型时,永远你不要调用一个合约中的函数。

我们也可以直接使用function setFeed(InfoFeed _feed) { feed = _feed; }。但是小心 feed.info.value(10).gas(800)仅仅设定了和函数调用一起发送的value和gas额度,并且只有圆括号在最后能够让这个调用实际生效。

外部函数调用当调用的合约还没有存在时(意即账户中还没有包含代码)或这当调用的合约自己抛出异常或者gas用尽时,会抛出异常信息。

warning:
任何与其他合约的交互都将导致潜在的危险,特别是当合同的源代码并非提前已知的情况。当当前合同将控制权移交给其他合同时,它可能会做任何事。即使被调用的合同继承自一个已知的父合同,继承的合同也仅仅要求有一个正确的接口而已。而本身合同的实施依然是完全是任意和暗藏危险的。另外,对可能的调用你系统的其它合同,甚至是回调到之前已经调用过的合同的情况要做好准备,这以为这被调用合同可能会通过其函数改变调用合同的状态变量。因此,在编写合约时,对于外部函数的调用要发生在所有的状态变量发生任何改变之后,这样你的合同才不会给重进入利用的的机会。

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

省略函数参数名称
对于未使用的参数,尤其是返回的参数,可以省略。这些参数依然能在栈中展现出来,但他们是无法触及的。

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)(0.4.24新增)

一个合约可以创造一个新合约,通过使用 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); // 将被执行为C的结构体的一部分

   function createD(uint arg) public {
       D newdD = new D(arg);
   } 

   function createAndEndowD(uint arg, uint amount) public payable {
      // Send ether along with the creation
      D newD = (new D).value(amount)(arg);
   }
}

如例子中所见,通过创造一个实例D并使用 .value()选项来传递Ether是允许的,但是限制gas的额度是不行的。如果创造失败(因为栈溢出,没有足够财产或者其他问题),也将抛出一个异常

表达式求值的顺序(Order of Evaluation of Expression)

表达式求值的顺序并不是特定的(更正式的说法是,欲求值的表达式树上某个节点的儿子节点顺序并不是特定的,但他们无疑会比该节点更早计算)。只能保证指令的运行是有顺序的而且布尔表达式的短循环是有顺序的。

赋值

解构赋值和多值返回(Destructuring Assignment and Returning Multiple Values)
Solidity 内部允许元组(tuple)类型,比如一个列表,在编译时它的规模是一个常数,但其中的对象是潜在的不同类型。这些元组能被用作同时返回多个值。这些元祖既可以被分配给新声明的变量,也可以给早已存在的变量(或者大多数LValue):

pragma solidity >0.4.23<0.5.0;

contract C {
   uint[] data;

   function f() public pure returns (uint,bool,uint) {
       return(7,true ,2);
   }

   function g() public {
       // 和类型一起声明的变量,并且值来自于一个返回元组的分配
       (uint x, bool b,uint y ) = f();
       // 常见的交换值的办法 —— 对非值存储类型没有作用
       (x,y) = (y,x);
       // 某些成员会被省去(对于变量声明同样)
       (data.length,,) = f();// 设定长度为7
       //  仅仅在等号左边的成员能被省去,只有一个例外;
       (x,) = (1,);
       // (1,)是唯一一个定义一个一元元组的方法,因为(1)与1是一样的
   }
}

(相信了解Python的人已经看得出来这里的规则和Python是一样的)

注意
0.4.24版本之前允许将值赋予一个规模小一些的tuple,不论是左边小一些还是右边小一些都行(某一边甚至可以是空的)。现在这一特性已被移除,两边都必须由相同的成员数量

数组和结构体的复杂情况
赋值语义对于非值类型(比如数组和结构体)有一点复杂。赋值给一个状态变量总是创造一个独立的拷贝。另一方面,指定到局部变量并创造一个独立的拷贝仅仅适用于那些最基本的类型,例如,适用与32bytes的静态类型。如果结构体或者数组(包括string 和 bytes)是从一个状态变量赋值给一个局部变量,局部变量将保留一个指针到原本的状态变量。第二次在此赋值给该局部变量时,并不能改变原本的状态,而只是改变了局部变量的指针。只有赋值给状态变量的成员或元素菜货改变其状态。

辖域与声明

一个声明了的变量都有一个默认的初始值,其字节表示全部为0.变量的默认值都是典型的”0状态”,无能他是哪种类型。比如说,bool型的默认值是false。uint和int型的默认值都是0.对于静态数组以及bytes1到bytes32,每个单个元素会根据其类型被初始化为默认值。最后,对于动态数组,bytes和string,默认值都是一个空数组或者string。

在一个函数内部任何地方声明的变量豆浆作用于整个函数,无能他在哪儿声明的(这个即将成为过去)。这是因为solidity继承了JavaScript的辖域规则。这一点与其他很多语言都不一样(他们都是从声明处到函数最后)。结果,下面这段代码就变得不再合法了,在编译时将抛出一个异常:

// This will not compile

pragma solidity ^0.4.16;

contract ScopingErrors {
    function scoping()  public {
        uint i = 0;

        while (i++ <1)  {
             uint same1 = 0;
        }

        while (i++ <2)  {
             uint same1 = 0;//非法——第二次对same1声明
        }
     }

     function minimalScoping() public  {
        {
           uint same2 = 0;
        }
        {
           uint same2 = 0;//非法,原理同上
        }
     } 

     function forLoopScoping() public {
         for (uint same3 = 0; same3 <1;same3++){
         }
         for (uint same3 = 0; same3 <1;same3++) {// 非法,道理同上
         }
      }
}

另外,如果一个变量被声明了,他将在函数一开始就被初始化为默认值,因此下面这段代码是合法的:

pragma solidity ^0.4.0;

contract C {
    function foo() public pure returns (uint) {
       // baz is implicitly initialized as 0
       uint bar = 5;
       if (true) {
          bar += baz;
        } else {
          uint baz = 10;//永不会执行
        }
        return bar;//5
    }
}

错误处理:断言(Assert)、要求(Require)、恢复(Revert)和异常(Exception)

solidity使用状态恢复异常来处理错误。这样的异常会撤销当前调用(及之前所有母调用)中所有对状态所做的改变,并且对该调用者标记一个错误。assert 和 require 这两个方便的函数来检查当前状态,并在当前状态不符合要求时抛出异常。assert 函数仅仅用在测试内部错误以及检查不变量时。require 函数应该备用于确保正确的环境,比如输入,或者合约状态变量是否符合要求,或者确认对外部合约的调用的返回值有效。如果使用得当,这些工具能够评估你的合约来确定当前状况已及可能形成错误断言的函数调用。正确地运行代码应该保证绝不形成一个错误的断言声明;如果这件事发生了,说明你的合同中有bug,你应该修复它。

这里有两种办法来引发异常:revert 函数能被用于标记一个错误并且恢复当前调用。它是得讲一个含有错误细节的字符串信息回传给调用者成为可能。一个受到很多反对的关键字 throw 也能够用于revert() 的一个替代(但是仅仅没有错误信息)

note:
在0.4.13后throw关键字受到很多反对,并且在未来可能会被逐步淘汰

当异常发生在母调用中,他们会自动“沸腾”(比如异常将不断被重抛出)。符合这个规则的的异常函数为send,而其他低阶函数诸如 call,delegatecall,和callcode——这些都会在一个异常时就返回false 而非 “沸腾”(这一段没太看懂,翻译得有点莫名其妙)

warning:
当调用账户根本不存在时,低阶函数诸如 call,delegatecall,和 callcode 都会成功返回,这时EVM设计机制的一部分 。因此存在性必须要提前确认。

捕捉异常目前还暂时没有实现。

在下面的例子里,你会看到 require 怎样能被简单的用于确认输入的状况,以及assert 怎用能被用于内部错误检查。注意你可以选择性的提供一些信息字符串给require 函数,但是不能提供给assert。

pragma solidity ^0.4.22;

contract Sharer {
  function sendHalf(address addr) public payable returns (uint balance) {
      require(msg.value % 2 ==0, "Even value required.");
      uint balanceBeforeTransfer = this.balance;
      addr.transfer(msg.value / 2);
      // 因为这里 transfer 抛出一个错误异常且无法回调,
      // 我们没有任何办法依然保有一般的钱
      assert(this.banlance == banlanceBeforeTransfer - msg.value/2);
      return this.balance;
   }
}

一个断言类型的异常是在下面的情境下产生的:

  1. 如果你访问一个数列,但序列号过大或者为负(比如 x[i] while i>=x.length or i<0)
  2. 如果你访问一个定长的 bytesN,但序列号过大或为负
  3. 如果你对0做除法或求余
  4. 如果你做移位操作但是移位为负
  5. 如果你尝试将一个过大或者负值的值转化为enum类型
  6. 如果你调用一个内部函数的0初始化变量
  7. 如果你使用assert而参数赋值为false

一个require风格的异常是在如下环境中产生的:
1. 调用 throw
2. 调用require其中的参数赋值为false
3. 如果你通过消息调用来调用一个函数但它还没有完全准备好(比如所它耗尽了gas,没有匹配的函数,或者自己已经抛出一个异常),除了当一个低阶函数操作 call,send,delegatecall或者 callcode 被使用的情况之外。这些低阶函数不会抛出异常,但它们会通过返回false 来暗示错误发生。
4. 如果你使用new关键字创造一个合约,但该合约还没有准备好时(同上)
5. 如果你准备外部调用一个函数,而该函数根本没有代码
6. 如果你的合约通过公共函数收到以太币,然而该函数却没有payable修饰(包括构造函数和撤回函数)
7. 如果你的合约通过一个公共的获取函数收到以太币
8. 如果一个 .transfer()操作失败了

在内部,solidity为每个require风格的异常执行了一个回退操作,并且执行了一个无效的操作来抛出一个Assert风格的异常。在两种情况下,这将引起EVM回退掉之前状态发生的所有改变。回退的原因是没有安全的方法继续执行,因为一个期望的结果并没有发生。因为我们想保持交易的原子性,所做的最安全的事就是回退所有的改变,是整个交易(或最近一次调用)不受影响。注意 assert风格的异常将小号所有该调用可用的gas,而require异常不会消耗一开始释放的任何gas。

下面这个例子展示了一个错误字符串怎样被并用于revert 和 require:

pragma solidity ^0.4.22;

contract VendingMachine {
   function buy(uint amount) payable {
       if (amount > msg.value / 2 ether)
          revert("No enough Ether provided.");
       //  另一种方法来做
       require{
           amount <= msg.value /2 ether,
           " Not enough Ehter provided."
        };
        // 执行支付
     }
}

提供字符串会被abi编码,他会调用一个函数Error(string)。在上面这个例子,revert("Not enough Ether provided.");会造成下面的十六进制码数据被设定为错误回传数据:

0x08c379a0                                                         // Function selector for Error(string)
0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset
0x000000000000000000000000000000000000000000000000000000000000001a // String length
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data

猜你喜欢

转载自blog.csdn.net/weixin_42595515/article/details/81977369