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

状态变量可见性

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

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

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

在这里插入图片描述

函数可见性

前面的文章,我们多多少少有见到在函数参数列表后的一些关键字,那便是函数可见性修饰符。对于函数可见性这一概念,有过现代编程语言的经历大都知晓,诸如,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;
  }
}

在这里插入图片描述

合约之外的函数

在 Solidity 0.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 、存储和不在他们的作用域范围内函数。

函数参数与返回值

与其它编程语言一样,函数可能接受参数作为输入。但 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);
    }
}

状态可变性

view

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

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

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

在这里插入图片描述

Solidity 0.5.0 移除了 view的别名constant

Getter 方法自动被标记为 view

pure

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

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

  1. 读取状态变量。
  2. 访问 address(this).balance 或者 <address>.balance
  3. 访问 blocktxmsg 中任意成员 (除 msg.sigmsg.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);
    }
}

在这里插入图片描述

特别的函数

receive 接收以太函数

一个合约最多有一个 receive 函数, 声明函数为: receive() external payable { ... }

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

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

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

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

  • 写入存储
  • 创建合约
  • 调用消耗大量 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 (bytes calldata input) external [payable] returns (bytes memory output)

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

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

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

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

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

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

payablefallback函数也可以在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;
    }

}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甄齐才

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值