• 区块链中的函数(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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在下面的例子中,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); // 访问内部成员(从继承合约访问父合约成员)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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(); // 外部访问
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果你有一个数组类型的 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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    现在可以使用 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这将会生成以下形式的函数,在结构体内的映射和数组(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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    函数

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

    合约之外的函数(也称为“自由函数”)始终具有隐式的 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在合约之外定义的函数仍然在合约的上下文内执行。 他们仍然可以访问变量 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    外部函数 不可以接受多维数组作为参数 如果原文件加入 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    返回变量名可以被省略。 返回变量可以当作为函数中的本地变量,没有显式设置的话,会使用 :ref: 默认值 返回变量可以显式给它附一个值(像上面),也可以使用 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个形式等同于赋值给返回参数,然后用 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    纯函数能够使用 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    函数重载

    合约可以具有多个不同参数的同名函数,称为“重载”(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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 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 {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    以上两个 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

  • 相关阅读:
    【MATLAB源码-第59期】基于matlab的QPSK,16QAM164QAM等调制方式误码率对比,调制解调函数均是手动实现未调用内置函数。
    SSM基于上述环境实现简单CUDA操作
    23、Mybatis查询功能4(查询结果为一个map集合(多条数据))
    电影评分数据分析案例-Spark SQL
    RCTF 2024 WEB wp
    linux 学习笔记
    软考高级信息系统项目管理师系列之二:信息化和信息系统习题
    Nim游戏
    通过 Prometheus 编写 TiDB 巡检脚本(脚本已开源,内附链接)
    MySQL数据库面试题总结(2022最新版)
  • 原文地址:https://blog.csdn.net/qq_40713201/article/details/126312260