区块链中的函数(solidity0.8.13)

可见性和 getter 函数

状态变量可见性

状态变量有 3 种可见性:

  • public 对于 public 状态变量会自动生成一个 getter hanshu 函数(见下面)。 以便其他的合约读取他们的值。
    当在用一个合约里使用是,外部方式访问 (如: this.x) 会调用getter 函数,而内部方式访问 (如: x)
    会直接从存储中获取值。 Setter函数则不会被生成,所以其他合约不能直接修改其值。
  • internal 内部可见性状态变量只能在它们所定义的合约和派生合同中访问。 它们不能被外部访问。 这是状态变量的默认可见性。
  • private 私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。

设置为 private 或 internal,只能防止其他合约读取或修改信息,但它仍然可以在链外查看到。

函数可见性

由于 Solidity 有两种函数调用:外部调用则会产生一个 EVM 调用,而内部调用不会, 更进一步, 函数可以确定器被内部及派生合约的可访问性,这里有 4 种可见性:

  • external 外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f不起作用,但 this.f() 可以)。
  • public public 函数是合约接口的一部分,可以在内部或通过消息调用。
  • internal 内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。
  • private private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

可见性标识符的定义位置,对于状态变量来说是在类型后面,对于函数是在参数列表和返回关键字中间。

pragma solidity  >=0.4.16 <0.9.0;

contract C {
    
    
    function f(uint a) private pure returns (uint b) {
    
     return a + 1; }
    function setData(uint a) internal {
    
     data = a; }
    uint public data;
}

在下面的例子中,D 可以调用 c.getData() 来获取状态存储中 data 的值,但不能调用 f 。 合约 E 继承自 C ,因此可以调用 compute。

pragma solidity >=0.4.16 <0.9.0;

contract C {
    
    
    uint private data;

    function f(uint a) private returns(uint b) {
    
     return a + 1; }
    function setData(uint a) public {
    
     data = a; }
    function getData() public returns(uint) {
    
     return data; }
    function compute(uint a, uint b) internal returns (uint) {
    
     return a+b; }
}

// 下面代码编译错误
contract D {
    
    
    function readData() public {
    
    
        C c = new C();
        uint local = c.f(7); // 错误:成员 `f` 不可见
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // 错误:成员 `compute` 不可见
    }
}

contract E is C {
    
    
    function g() public {
    
    
        C c = new C();
        uint val = compute(3, 5); // 访问内部成员(从继承合约访问父合约成员)
    }
}

Getter 函数

编译器自动为所有 public 状态变量创建 getter 函数。对于下面给出的合约,编译器会生成一个名为 data 的函数, 该函数没有参数,返回值是一个 uint 类型,即状态变量 data 的值。 状态变量的初始化可以在声明时完成。

pragma solidity  >=0.4.16 <0.9.0;

contract C {
    
    
    uint public data = 42;
}

contract Caller {
    
    
    C c = new C();
    function f() public {
    
    
        uint local = c.data();
    }
}

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

pragma solidity >=0.4.16 <0.9.0;

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

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

pragma solidity >=0.4.0 <0.9.0;

contract arrayExample {
    
    
  // public state variable
  uint[] public myArray;

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

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

现在可以使用 getArray() 获得整个数组,而 myArray(i) 是返回单个元素。

下一个例子稍微复杂一些:

pragma solidity ^0.4.0 <0.9.0;

contract Complex {
    
    
    struct Data {
    
    
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
        uint[3] c;
        uint[] d;
        bytes e;
    }
    mapping (uint => mapping(bool => Data[])) public data;
}

这将会生成以下形式的函数,在结构体内的映射和数组(byte 数组除外)被省略了,因为没有好办法为单个结构成员或为映射提供一个键。

function data(uint arg1, bool arg2, uint arg3)
    public
    returns (uint a, bytes3 b, bytes memory e)
{
    
    
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
    e = data[arg1][arg2][arg3].e;
}

函数

可以在合约内部和外部定义函数。

合约之外的函数(也称为“自由函数”)始终具有隐式的 internal 可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.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 {
    
    
        // This calls the free function internally.
        // The compiler will add its code to the contract.
        uint s = sum(arr);
        require(s >= 10);
        found = true;
    }
}

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

函数参数及返回值

与 Javascript 一样,函数可能需要参数作为输入; 而与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出。

函数参数(输入参数)

函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。

例如,如果我们希望合约接受有两个整数形参的函数的外部调用,可以像下面这样写:

pragma solidity >=0.4.16 <0.9.0;

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

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

外部函数 不可以接受多维数组作为参数 如果原文件加入 pragma abicoder v2; 可以启用ABI v2版编码功能,这此功能可用。 (注:在 0.7.0 之前是使用 pragma experimental ABIEncoderV2; )

内部函数 则不需要启用ABI v2 就接受多维数组作为参数。

返回变量

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

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

pragma solidity >=0.4.16 <0.9.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 语句可以一个或多个值,参阅 multiple ones 。

pragma solidity >=0.4.16 <0.9.0;

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

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

如果使用 return 提前退出有返回值的函数, 必须在用 return 时提供返回值。

返回多个值

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

状态可变性

View 视图函数

可以将函数声明为 view 类型,这种情况下要保证不修改状态。

下面的语句被认为是修改状态:

  • 修改状态变量。
  • 产生事件。
  • 创建其它合约。
  • 使用 selfdestruct。
  • 通过调用发送以太币。
  • 调用任何没有标记为 view 或者 pure 的函数。
  • 使用低级调用。
  • 使用包含特定操作码的内联汇编。
pragma solidity  >=0.5.0 <0.9.0;

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

Pure 纯函数

函数可以声明为 pure ,在这种情况下,承诺不读取也不修改状态变量。

特别是,应该可以在编译时确定一个 pure 函数,它仅处理输入参数和 msg.data ,对当前区块链状态没有任何了解。 这也意味着读取 immutable 变量也不是一个 pure 操作。

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

  • 读取状态变量。
  • 访问 address(this).balance 或者
    .balance。
  • 访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。
  • 调用任何未标记为 pure 的函数。
  • 使用包含某些操作码的内联汇编。
pragma solidity >=0.5.0 <0.9.0;

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

纯函数能够使用 revert() 和 require() 在 发生错误 时去还原潜在状态更改。

还原状态更改不被视为 “状态修改”, 因为它只还原以前在没有view 或 pure 限制的代码中所做的状态更改, 并且代码可以选择捕获 revert 并不传递还原。

这种行为也符合 STATICCALL 操作码。

特别的函数

receive 接收以太函数

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

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

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

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

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

  • 写入存储
  • 创建合约
  • 调用消耗大量 gas 的外部函数
  • 发送以太币

一个没有定义 fallback 函数或  receive 函数的合约,直接接收以太币(没有函数调用,即使用 send 或 transfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接收以太币,必须实现receive函数(使用 payable fallback 函数不再推荐,因为它会让借口混淆)。

下面是一个例子:

pragma solidity ^0.6.0;

// 这个合约会保留所有发送给它的以太币,没有办法取回。
contract Sink {
    
    
    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 可以使用,参考 receive接收函数

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.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));


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

    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`` or ``transfer``消耗更多的 gas。
        // 因此使用底层的call调用
        (success,) = address(test).call{
    
    value: 2 ether}("");
        require(success);

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

        return true;
    }

}

函数重载

合约可以具有多个不同参数的同名函数,称为“重载”(overloading),这也适用于继承函数。以下示例展示了合约 A 中的重载函数 f。

pragma solidity >=0.4.16 <0.9.0;

contract A {
    
    
    function f(uint value) public pure returns (uint out) {
    
    
        out = value;
    }

    function f(uint value, bool really) public pure returns (uint out) {
    
    
        if (really)
            out = value;
    }
}

重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。

// 以下代码无法编译
pragma solidity >=0.4.16 <0.9.0;

contract A {
    
    
    function f(B value) public pure returns (B out) {
    
    
        out = value;
    }

    function f(address value) public pure returns (address out) {
    
    
        out = value;
    }
}

contract B {
    
    
}

以上两个 f 函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity 中被认为是不同的。

重载解析和参数匹配

通过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。

返回参数不作为重载解析的依据。

pragma solidity >=0.4.16 <0.9.0;

contract A {
    
    
    function f(uint8 val) public pure returns (uint8 out) {
    
    
        out = val;
    }

    function f(uint256 val) public pure returns (uint256 out) {
    
    
        out = val;
    }
}

调用 f(50) 会导致类型错误,因为 50 既可以被隐式转换为 uint8 也可以被隐式转换为 uint256。 另一方面,调用 f(256) 则会解析为 f(uint256) 重载,因为 256 不能隐式转换为 uint8。

猜你喜欢

转载自blog.csdn.net/qq_40713201/article/details/126312260