Skip to content

Latest commit

 

History

History
300 lines (249 loc) · 11.9 KB

File metadata and controls

300 lines (249 loc) · 11.9 KB

函数与事件

函数

函数参数

函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。例如,如果希望合约接受有两个整数形参的函数的外部调用,可以像下面这样写:

pragma solidity ^0.5.0;

contract Simple {
    uint sum;
    function taker(uint _a, uint _b) public {
        sum = _a + _b;
    }
}

函数参数可以当作为本地变量,也可用在等号左边被赋值。

函数返回值

函数返回变量的声明方式在关键字returns之后,与参数的声明方式相同。

例如,如果我们需要返回两个结果:两个给定整数的和与积,应该写作:

pragma solidity ^0.5.0;

contract Simple {
    function arithmetic(uint _a, uint _b) public pure returns (uint o_sum, uint o_product) {
        o_sum = _a + _b;
        o_product = _a * _b;
    }
}

返回变量名可以被省略。 返回变量可以当作为函数中的本地变量,没有显式设置的话,会使用 默认值 <default-value> 返回变量可以显式给它附一个值,也可以使用 return 语句指定,使用return语句可以一个或多个值。

pragma solidity ^0.5.0;

contract Simple {
    function arithmetic(uint _a, uint _b)
        public
        pure
        returns (uint o_sum, uint o_product)
    {
        return (_a + _b, _a * _b);
    }
}

这个形式等同于赋值给返回参数,然后用 return; 退出。

当函数需要使用多个值,可以用语句 return (v0, v1, ..., vn) 。 参数的数量需要和声明时候一致。

作用域

变量无论在函数内什么位置定义,其作用域均为整个函数,而非大多数据语言常见的块级作用域。下面的例子会报错DeclarationError: Undeclared identifier.

pragma solidity ^0.5.0;

contract ScopeErr{
  function f() pure public returns (uint8) {
    { uint8 a = 0;}

    return a;
  }
}

上例中,由于变量作用域不同,导致a在函数里面并没有被声明,在返回的时候就会报错。

来看一个稍微隐蔽点的例子:

pragma solidity ^0.5.0;

contract FunctionScope{
  function f() public view returns (uint8){
    for(var i = 0; i < 10; i++){
      //do sth
    }
    return i;
  }
}

读者可以试想一下上述代码能否编译通过呢。 答案是不可以的,原因就是因为i变量虽然是在for循环中被定义,但它的的作用域不是整个函数,但是在函数内的任何位置均可以访问到这个变量。

函数调用

函数是合约中代码的可执行单元,除了对区块链分布式系统设计的internalexternal这两种不同的函数调用方式,Solidity还提供对函数可见性控制的语法。 Solidity封装了两种函数的调用方式internalexternal两种。

internal调用,实现时转为简单的EVM跳转,可以直接使用上下文环境中的数据,对于引用传递将会变得非常高效(不用拷贝数据)。在当前的代码单元内,如对合约内函数,引入的函数库,以及父类合约中的函数直接使用即是以internal方式的调用;

pragma solidity ^0.5.0;

contract SimpleAuction {
   function f() public {}
   function callInternally() public {
       f();
   }
}

在上述代码中,callInternally()以internal的方式对f()函数进行了调用。

external调用,实现为合约的外部消息调用。合约初始化时不能external的方式调用自身函数,因为合约还未初始化完成。

pragma solidity ^0.5.0;

contract A {
    function f() public pure returns (uint8) {
        return 0;
    }
}

contract B {
    function callExternal(A a) public pure returns (uint8) {
        return a.f();
    }
}

虽然合约A和B的代码在一个文件中,但部署到区块链上后,它们是完全独立的两个合约,它们之间的调用方法是通过消息调用的,上述代码中,在合约B中的callExternal()external的方式调用了合约Af()external调用时,实际是向目标合约发送一个消息调用,消息中的函数定义部分是一个24字节大小的消息体 ,20个字节的地址,4个字节的函数签名。 external调用还有另一种写法,可以在合约的调用函数前加this来强制的以external方式调用,需要注意这里的this和大多数语言的意思都不一致。

pragma solidity ^0.5.0;

contract A {
    function f() public {}
    function callExternally() public {
        this.f();
    }
}

Solidity的函数除了internalexternal的调用方式区别之外,还有函数可见性的修饰语法。

函数可见性

Solidity为函数提供了四种可见性,externalpublicinternalprivate

external

  • 声明为external的函数可以从其他合约或者通过Transaction调用,所以声明为external的函数是合约对外接口的一部分。
  • 不能以internal的方式进行调用。
  • 有时在接收大的数组时性能会更好。
pragma solidity ^0.5.0;

contract FuntionTest{
    function externalFunc() external{}
    function callFunc() public {
        //以`external`的方式调用函数
        this.externalFunc();
    }
}

声明为external的externalFunc()只能以external的方式进行调用,以internal的方式调用会报Error: Undeclared identifier.

public 声明为public的函数既可以用internal的方式调用,也允许以external的方式调用,也是合约对外接口的一部分。

pragma solidity ^0.5.0;

contract FuntionTest{
    //默认是public函数
    function publicFunc() public {}
    // function publicFunc(){} 等价于 function publicFunc() public {}

    function callFunc() public {
        //以`internal`的方式调用函数
        publicFunc();
        
        //以`external`的方式调用函数
        this.publicFunc();
    }
}

我们可以看到声明为public的publicFunc()允许两种调用方式。

internal 在当前的合约或继承的合约中,只允许以internal的方式调用。

pragma solidity ^0.4.5;

contract A{
    //默认是public函数
    function internalFunc() internal{}

    function callFunc(){
        //以`internal`的方式调用函数
        internalFunc();
    }
}
contract B is A{
    //子合约中调用
    function callFunc(){
        internalFunc();
    }
}

上述例子中声明为internal的internalFunc()在定义合约,和子合约中均只能以internal的方式可以进行调用。

private 只能在当前合约中被访问(不可在被继承的合约中访问)。 即使声明为private,仍能被所有人查看到里面的数据。访问权限只是阻止了其它合约访问函数或修改数据。

pragma solidity ^0.4.5;

contract A{
    //默认是public函数
    function privateFunc() private{}

    function callFunc(){
        //以`internal`的方式调用函数
        privateFunc();
    }
}
contract B is A{
    //不可调用`private`
    function callFunc(){
        //privateFunc();
    }
}

上述例子中,声明为private的privateFunc()只能在定义的合约中以internal的方式进行调用。

函数装饰器

使用 修饰器 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修饰器 是合约的可继承属性, 并可能被派生合约覆盖。

pure装饰器,在函数中使用pure装饰器后表示函数不会修改或访问状态变量,对外界没有任何影响。

pragma solidity ^0.4.23;
contract HelloWorld{
    function testPure(uint a,uint b) public pure returns(uint){
        return a+b;
    }
}

比如testPure函数,既没有修改传递进来的值(值传递),又没有修改状态变量,只是单纯的进行了一个计算,并不会消耗任何gas,应为执行使用的是本地cpu计算,并没有消耗任何链上资源。

view装饰器,在函数中使用view装饰器后表示函数不会修改状态变量,对链上只有读取操作,同样也不消耗gas。其实很简单,因为作为一个全节点来说,会同步保存所有的信息,保存在本地中,那么我们要查看区块链上的资源,同样可以直接在一个全节点之上查询数据即可,我不需要全世界的节点都知道。都去同时的处理这笔事务。我也不需要将调用这笔函数的信息记录在区块链上。

pragma solidity ^0.5.0;

contract HelloWorld{
    string public name = "qyuan";
    function getName() public view returns (string memory) {
        return name;
    }
}

payable装饰器,允许函数在调用时可以接收和发送以太币。 constant装饰器等同于view装饰器,不过在0.5.0后面的版本移除了。

回退函数

以太坊的智能合约,可以声明一个匿名函数(unnamed function),叫做回退函数(Fallback)函数,这个函数不带任何参数,也没有返回值。当向这个合约发送消息时,如果没有找到匹配的函数就会调用fallback函数。比如向合约转账,但要合约接收 Ether,那么 fallback 函数必须声明为 payable,否则试图向此合约转 ETH 将失败。如下:

function() payable public { // payable 关键字,表明调用此函数,可向合约转 Ether。
}

向合约发送send、transfer、call 消息时候都会调用 fallback 函数,不同的是 send 和 transfer有2300gas的限制,也就是传递给fallback的只有2300gas,这个gas只能用于记录日志,因为其他操作都将超过2300gas。但call则会把剩余的所有gas都给fallback函数,这有可能导致循环调用。严重威胁智能合约安全的代码重入漏洞就是因为通过调用call函数转账时触发fallback函数所引起的。

事件

事件是能方便地调用以太坊虚拟机日志功能的接口,说到事件必然就会提到日志,事件被触发后就会被记录到区块链上就成为了日志,事件是一种行为,日志是对这种行为的记录。 事件是以太坊EVM提供的一种基础设施,用来实现一些交互功能,比如通知UI,返回函数调用结果等。

当定义的事件被触发时,被记录到日志中,日志又与合约相关联一起存储到区块链中,只要某个区块可以访问,其相关的日志就能访问,但是在合约中,并不能直接访问日志中的事件数据。

pragma solidity ^0.4.21;
contract SimpleAuction {
    event HighestBidIncreased(address bidder, uint amount); // 事件

    function bid() public payable {
        // ...
        emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
    }
}

使用event关键字定义一个事件,参数列表中为要记录的日志参数的名称及类型。

监听事件

var event = myContract.HighestBidIncreased(function(error, result){   
    for(let key in result){
        console.log(key + " : " + result[key]);
    }
});

检索日志

Indexed属性,可以在事件参数上增加Indexed属性,最多可以对三个参数增加这样的属性,加上这个属性,就可以允许在web3.js中通过对加了这个属性的参数值进行过滤。

var event = myContract.transfer({value: "100"});

上面实现的是对value值为100的日志,过滤后的返回。 如果你想同时匹配多个值,还可以传入一个要匹配的数组。

var event = myContract.transfer({value: ["99","100","101"]});

增加了indexed的参数值会存到日志结构的Topic部分,便于快速查找。而未增加indexed的参数会存在data部分成为了原始日志。需要注意的是,如果增加indexed属性的是数组(包括stringbytes),那么只会在Topic存储对应的sha3哈希值,不会存储原始数据。 因为Topic是用于快速查找的,不能存任意长度的数据,所以通过Topic实际存在的是数组这种非固定长度数据的哈希结果,要查找时,是将要查找的内容与Topic内容进行匹配,但不能反推哈希结果,从而不能得到原始的值。