Solidity之旅(十三)函数及其可见性和状态可变性

01

状态变量可见性

在这之前的文章里,给出的例子中,声明的状态变量都修饰为public,因为我们将状态变量声明为public后,Solidity编译器自动会为我们生成一个与状态变量同名的、且函数可见性为public的函数!

在Solidity中,除了可以将状态变量修饰为public,还可以修饰为另外两种:internal、private。

  • public

对于public状态变量会自动生成一个,与状态变量同名的public修饰的函数。以便其他的合约读取他们的值。当在用一个合约里使用是,外部方式访问(如:this.x)会调用该自动生成的同名函数,而内部方式访问(如:x)会直接从存储中获取值。Setter函数则不会被生成,所以其他合约不能直接修改其值。

  • internal

内部可见性状态变量只能在它们所定义的合约和派生合同中访问,它们不能被外部访问。这是状态变量的默认可见性。

  • private

私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract stateVarsVisible {
   uint public num;
   function showNum() public returns(uint){
      num += 1;
      return num;
   }
}

contract outsideCall {
   function myCall() public returns(uint){
      //实例化合约
      stateVarsVisible sv = new stateVarsVisible();
      //调用 getter 函数
      return sv.num();
   }
}

图片

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract stateVarsVisible {
   uint internal num;
   function showNum() public returns(uint){
      num += 1;
      return num;
   }
   function fn() external returns(uint){
      return num;
   }
}
contract sub is stateVarsVisible {
   function myNum() public returns(uint){
      return stateVarsVisible.num;
   }
}
contract outsideCall {
   function myCall() public returns(uint){
      //实例化合约
      stateVarsVisible sv = new stateVarsVisible();
      //外部合约 不能访问
      //return sv.num();
   }
}

图片

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract stateVarsVisible {
   uint private num;
   function showNum() public returns(uint){
      num += 1;
      return num;
   }
   function fn() external returns(uint){
      return num;
   }
}
contract sub is stateVarsVisible {
   function myNum() public returns(uint){
   //派生合约 无法访问 基合约 的状态变量
      return stateVarsVisible.num;
   }
}

图片

02

函数可见性

前面的文章,我们多多少少有见到在函数参数列表后的一些关键字,那便是函数可见性修饰符。对于函数可见性这一概念,有过现代编程语言的经历大都知晓,诸如,public(公开的)、private(私有的)、protected(受保护的)用来修饰函数的可见性,Java、PHP`等便是使用这些关键字来修饰函数的可见性。

当然咯,Solidity函数对外可访问也做了修饰,分为以下4种可见性:

  • external

外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。一个外部函数f不能从内部调用(即f不起作用,但this.f()可以)。

  • public

public函数是合约接口的一部分,可以在内部或通过消息调用。

  • internal

内部可见性函数访问可以在当前合约或派生的合约访问,外部不可以访问。由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。

  • private

private函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract FunctionVisible {
   uint private num;
   function privateFn(uint x) private returns(uint y){ y = x + 5; }
   function setNum(uint x) public { num = x;}
   function getNum() public returns(uint){ return num; }
   function sum(uint x,uint y) internal returns(uint) { return x + y; }
   function showPri(uint x) external returns(uint){ x += num; return privateFn(x); }

}
contract Outside {
   function myCall() public {
      FunctionVisible fv = new FunctionVisible();
      uint res = fv.privateFn(7); // 错误:privateFn 函数是私有的
      fv.setNum(4);
      res = fv.getNum();
      res = fv.sum(3,4); // 错误:sum 函数是 内部的
    
   }
}

图片

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract FunctionVisible {
   uint private num;
   function privateFn(uint x) private view returns(uint y){ y = x + num; }
   function setNum(uint x) public { num = x;}
   function getNum() public  view returns(uint){ return num; }
   function sum(uint x,uint y) internal pure returns(uint) { return x + y; }
   function showPri(uint x) external view returns(uint){ x += num; return privateFn(x); }

}
contract Sub is FunctionVisible {
   function myTest() public pure returns(uint) {
        uint val = sum(3, 5); // 访问内部成员(从继承合约访问父合约成员)
        val = privateFn(6);  //privateFn函数是私有的,即便是派生合约也不能访问
        return val;
    }
}

图片

getter函数具有外部(external)可见性。如果在内部访问getter(即没有this.),它被认为一个状态变量。如果使用外部访问(即用this.),它被认作为一个函数。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract C {
    uint public data;
    function x() public returns(uint) {
        data = 3; // 内部访问
        uint val = this.data(); // 外部访问
        return val;
    }
}

图片

如果你有一个数组类型的public状态变量,那么你只能通过生成的getter函数访问数组的单个元素。这个机制以避免返回整个数组时的高成本gas。可以使用如myArray(0)用于指定参数要返回的单个元素。如果要在一次调用中返回整个数组,则需要写一个函数,例如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract C {
 
  uint[] public myArray;

  // 自动生成的Getter 函数
  /*
  function myArray(uint i) public view returns (uint) {
      return myArray[i];
  }
  */

  // 返回整个数组
  function getArray() public view returns (uint[] memory) {
      return myArray;
  }
}

图片

03

合约之外的函数

在Solidity0.7.0版本之后,便可以将函数定义在合约之外,我们称这种函数为“自由函数”,其函数可见性始终隐式地为internal,它们的代码包含在所有调用它们的合约中,类似于后续会讲到的库函数。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

function sum(uint[] memory arr) pure returns (uint s) {
    for (uint i = 0; i < arr.length; i++)
        s += arr[i];
}

contract ArrayExample {
    bool found;
    function f(uint[] memory arr) public {
        //编译器会将 合约外函数的代码添加到这里
        uint s = sum(arr);
        require(s >= 10); //后续会讲到
        found = true;
    }
}

图片

在合约之外定义的函数仍然在合约的上下文内执行。它们仍然可以调用其他合约,将其发送以太币或销毁调用它们的合约等其他事情。与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量this、存储和不在他们的作用域范围内函数。

04

函数参数与返回值

与其它编程语言一样,函数可能接受参数作为输入。但Solidity和golang一样,函数可以返回任意数量的值作为输出。

1、函数入参

函数的参数变量这一点倒是与声明变量是一样的,如果未能使用到的参数可以省略参数名。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Simple {
    uint sum;
    function taker(uint a, uint b) public {
        sum = a + b;
    }
}

图片

2、返回值

Solidity函数返回值与golang函数返回很类似,只不过,Solidity使用returns关键字将返回参数声明在小括号内。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Simple {
    function arithmetic(uint a, uint b) public pure returns (uint sum, uint product)
    {
        sum = a + b;
        product = a * b;
    }
}

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint , uint )
    {
        return (a + b, a * b);
    }
}

05

状态可变性

view

我们在先前的文章会看到,有些函数在修饰为public后,有多了view修饰的。而函数使用了view修饰,说明这个函数不能修改状态变量(StateVariable),只能获取状态变量的值,由于view修饰的函数不能修改存储在区块链上的状态变量,这种函数的gasfee不会很高,毕竟调用函数也会消耗gasfee。

而以下情况被认为是修改状态的:

1.修改状态变量。

2.产生事件。

3.创建其它合约。

4.使用selfdestruct。

5.通过调用发送以太币。

6.调用任何没有标记为view或者pure的函数。

7.使用低级调用。

8.使用包含特定操作码的内联汇编。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Simple {
    function f(uint a, uint b) public view returns (uint) {
        return a * (b + 42) + block.timestamp;
    }
}

图片

Solidity0.5.0移除了view的别名constant。

Getter方法自动被标记为view。

pure

若将函数声明为pure的话,那么该函数是既不能读取也不能修改状态变量(StateVariable)。

除了上面解释的状态修改语句列表之外,以下被认为是读取状态:

1.读取状态变量。

2.访问address(this).balance

或者<address>.balance。

3.访问block,tx,msg中任意成员(除msg.sig和msg.data之外)。

4.调用任何未标记为pure的函数。

5.使用包含某些操作码的内联汇编。​​​​​​​

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Simple {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}

图片

06

特别的函数

receive接收以太函数

一个合约最多有一个receive函数,声明函数为:

receive()externalpayable{...}

不需要function关键字,也没有参数和返回值并且必须是external可见性和payable修饰.它可以是virtual的,可以被重载也可以有后续会讲到的修改器modifier。

在对合约没有任何附加数据调用(通常是对合约转账)是会执行receive函数。例如通过.send()或.transfer(),如果receive函数不存在,但是有payable的接下来会讲到的fallback回退函数那么在进行纯以太转账时,fallback函数会调用。

如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常)。

更糟的是,receive函数可能只有2300gas可以使用(如,当使用send或transfer时),除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作2300gas:

  • 写入存储

  • 创建合约

  • 调用消耗大量gas的外部函数

  • 发送以太币

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Simple {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

图片

Fallback回退函数

合约可以最多有一个回退函数。函数声明为:

fallback()external[payable]

fallback(bytescalldatainput)external[payable]returns(bytesmemoryoutput)。

没有function关键字。必须是external可见性,它可以是virtual的,可以被重载也可以有后续会讲到的修改器modifier。

如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配,fallback会被调用。或者在没有receive函数时,而没有提供附加数据对合约调用,那么fallback函数会被执行。

fallback函数始终会接收数据,但为了同时接收以太时,必须标记为payable。

如果使用了带参数的版本,input将包含发送到合约的完整数据(等于msg.data),并且通过output返回数据。返回数据不是ABI编码过的数据,相反,它返回不经过修改的数据。

更糟的是,如果回退函数在接收以太时调用,可能只有2300gas可以使用。

与任何其他函数一样,只要有足够的gas传递给它,回退函数就可以执行复杂的操作。

payable的fallback函数也可以在pure·以太转账的时候执行,如果没有receive以太函数推荐总是定义一个receive函数,而不是定义一个payable的fallback函数,

如果想要解码输入数据,那么前四个字节用作函数选择器,然后用abi.decode与数组切片语法一起使用来解码ABI编码的数据:​​​​​​​

(c, d) = abi.decode(_input[4:], (uint256, uint256));

请注意,这仅应作为最后的手段,而应使用对应的函数。​​​​​​​

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Test {
    // 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
    // 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
    fallback() external { x = 1; }
    uint x;
}


// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract TestPayable {
    uint x;
    uint y;

    // 除了纯转账外,所有的调用都会调用这个函数.
    // (因为除了 receive 函数外,没有其他的函数).
    // 任何对合约非空calldata 调用会执行回退函数(即使是调用函数附加以太).
    fallback() external payable { x = 1; y = msg.value; }

    // 纯转账调用这个函数,例如对每个空empty calldata的调用
    receive() external payable { x = 2; y = msg.value; }
}

contract Caller {
    function callTest(Test test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        //  test.x 结果变成 == 1。

        // address(test) 不允许直接调用 send ,  因为 test 没有 payable 回退函数
        //  转化为 address payable 类型 , 然后才可以调用 send
        address payable testPayable = payable(address(test));

        testPayable.transfer(2 ether);
        // 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
        // test.send(2 ether);
        return true;
    }

    function callTestPayable(TestPayable test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // 结果 test.x 为 1  test.y 为 0.
        (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // 结果test.x 为1 而 test.y 为 1.

        // 发送以太币, TestPayable 的 receive 函数被调用.

        // 因为函数有存储写入, 会比简单的使用 send 或 transfer 消耗更多的 gas。
        // 因此使用底层的call调用
        (success,) = address(test).call{value: 2 ether}("");
        require(success);

        // 结果 test.x 为 2 而 test.y 为 2 ether.

        return true;
    }

}

图片

版权声明:本文为CSDN博主「甄齐才」的原创文章,遵循CC4.0BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:

https://blog.csdn.net/coco2d_x2014/article/details/12837727

文章来源:CSDN博主「甄齐才」

文章原标题:《玩以太坊链上项目的必备技能(函数及其可见性和状态可变性-Solidity之旅十三)》

旨在传播区块链相关技术,如有侵权请与我们联系删除。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值