重要类型

Catalogue
  1. 1. 可支付回退函数
  2. 2. 数据位置
  3. 3. 数组
  4. 4. 地址类型
    1. 4.0.1. 区别
    2. 4.0.2. 类型转换
    3. 4.0.3. 成员变量和函数
    4. 4.0.4. 地址字面常量
  • 5. 合约类型
    1. 5.0.1. 调用其他合约
    2. 5.0.2. 其他注意
  • 6. 事件 Events
  • 7. 参考资料
  • 可支付回退函数

    合约可以有一个未命名的函数。这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。即使 fallback 函数不能有参数,仍然可以使用 msg.data 来获取随调用提供的任何有效数据。

    除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable 。 如果不存在这样的函数,则合约不能通过普通转账交易接收以太币。

    在最坏的情况下,回退函数只有 2300 gas 可以使用(如,当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :

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

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

    如果调用方打算调用不可用的函数, 也会执行回退函数。如果要实现回退函数仅用于接收以太, 则应添加类似 require(msg.data.length == 0) 检查以防止哪些无效的调用。

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

    一个没有 payable fallback 函数的合约,可以作为 coinbase 交易 (又名 矿工区块回报 )的接收者或者作为 selfdestruct 的目标来接收以太币。一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。

    这也意味着 address(this).balance 可以高于合约中实现的一些手工记帐的总和(例如在回退函数中更新的累加器记帐)。

    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
    pragma solidity >=0.5.0 <0.7.0;

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


    // 这个合约会保留所有发送给它的以太币,没有办法返还。
    contract Sink {
    function() external payable { }
    }

    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 回退函数
    // 需要通过 uint160 转化为 ``address payable`` 类型 , 然后才可以调用 ``send``
    address payable testPayable = address(uint160(address(test)));


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

    数据位置

    所有的引用类型,如 数组 和 结构体 类型,都有一个额外注解 数据位置 ,来说明数据存储位置。 有三种位置: 内存memory 、 存储storage 以及 调用数据calldata 。 调用数据calldata 仅对外部合约函数的参数有效,同时也是必须的。 调用数据calldata 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存memory 。

    在版本0.5.0之前,数据位置可以省略,并且根据变量的类型,函数类型等有默认数据位置,但是所有复杂类型现在必须提供明确的数据位置。

    数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:

    • 在 存储storage 和 内存memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
    • 从 内存memory 到 内存memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
    • 从 存储storage 到本地存储变量的赋值也只分配一个引用。
    • 其他的向 存储storage 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面 ArrayContract 合约 更容易理解)
    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
    pragma solidity >=0.4.0 <0.7.0;

    contract Tiny {
    uint[] x; // x 的数据存储位置是 storage

    // memoryArray 的数据存储位置是 memory
    function f(uint[] memory memoryArray) public {
    x = memoryArray; // 将整个数组拷贝到 storage 中,可行
    uint[] storage y = x; // 分配一个指针(其中 y 的数据存储位置是 storage),可行
    y[7]; // 返回第 8 个元素,可行
    y.length = 2; // 通过 y 修改 x,可行
    delete x; // 清除数组,同时修改 y,可行

    // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,
    // 但 storage 是“静态”分配的:
    // y = memoryArray;

    // 下面这一行也不可行,因为这会“重置”指针,
    // 但并没有可以让它指向的合适的存储位置。
    // delete y;

    g(x); // 调用 g 函数,同时移交对 x 的引用
    h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
    }

    function g(uint[] storage ) internal pure {}
    function h(uint[] memory) public pure {}
    }

    数组

    bytes 和 string 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 调用数据calldata 和 内存memory 中会被“紧打包”,将元素连续地存在一起,不会按每 32 字节一单元的方式来存放。string 与 bytes 相同,但不允许用长度或索引来访问。

    我们更多时候应该使用 bytes 而不是 byte[] ,因为Gas 费用更低, byte[] 会在元素之间添加31个填充字节。作为一个基本规则, 对任意长度的原始字节数据使用 bytes,对任意长度字符串(UTF-8)数据使用 string。

    如果使用一个长度限制的字节数组,应该使用一个 bytes1 到 bytes32 的具体类型,因为它们便宜得多。

    地址类型

    地址类型有两种形式,他们大致相同:

    • address:保存一个20字节的值(以太坊地址的大小)。
    • address payable :可支付地址,与 address 相同,不过有成员函数 transfer 和 send 。

    区别

    简单来说是 address payable 可以接受以太币的地址,而一个普通的 address 则不能。

    大部分情况下你不需要关心 address 与 address payable 之间的区别,并且到处都使用 address。 例如,如果你在使用 取款模式, 你可以(也应该)保存地址为 address 类型, 因为可以在msg.sender 对象上调用 transfer 函数, 因为 msg.sender 是 address payable。

    address 和 address payable 的区别是在 0.5.0 版本引入的,同样从这个版本开始,合约类型不在继承自地址类型,不过如果合约有可支付的回退( payable fallback )函数,合约类型仍然可以显示转换为 address 或 address payable 。

    类型转换

    允许从 address payable 到 address 的隐式转换,而从 address 到 address payable 的转换是不可以的( 执行这种转换的唯一方法是使用中间类型,先转换为 uint160 ),如:

    1
    address payable ap = address(uint160(addr));

    地址字面常量可以隐式转换为 address payable 。

    address 可以显式和整型、整型字面常量、bytes20 及合约类型相互转换。转换时需注意:不允许以 address payable(x) 形式转换。 如果 x 是整型或定长字节数组、字面常量或具有可支付的回退( payable fallback )函数的合约类型,则转换形式 address(x) 的结果是 address payable 类型。 如果 x 是没有可支付的回退( payable fallback )函数的合约类型,则 address(x) 将是 address类型。 在外部函数签名(定义)中,address 可用来表示 address 和 address payable 类型。

    如果将使用较大字节数组类型转换为 address ,例如 bytes32 ,那么 address 将被截断。 为了减少转换歧义,0.4.24及更高编译器版本要求我们在转换中显式截断处理。 以地址 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC 为例, 如果使用 address(uint160(bytes20(b))) 结果是 0x111122223333444455556666777788889999aAaa, 而使用 address(uint160(uint256(b))) 结果是 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc 。

    成员变量和函数

    • balance 和 transfer()

    可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位):

    1
    2
    3
    address x = 0x123; // 此地址为地址字面常量,可以隐式转换为 `address payable` 。
    address myAddress = this;
    if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

    如果当前合约的余额不够多,则 transfer 函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer 函数同样会失败而进行回退。

    如果 x 是一个合约地址,且有可支付的回退( payable fallback )函数时,该函数会跟 transfer 函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether 交易会被打回,当前的合约也会在终止的同时抛出异常。

    • send

    send 是 transfer 的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 send 会返回 false。

    在使用 send 的时候会有些风险:如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证 以太币Ether 发送的安全,一定要检查 send 的返回值,使用 transfer 或者更好的办法: 使用接收者自己取回资金的模式。

    地址字面常量

    比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 这样的通过了地址校验和测试的十六进制字面常量会作为 address payable 类型。

    十六进制字面常量长度在 39 到 41 个数字之间,而没有通过校验测试(我们也会收到一个警告)的十六进制字面常量则会视为正常的有理数字面常量。

    混合大小写的地址校验和格式定义在 EIP-55 中。

    合约类型

    每一个 contract]定义都有他自己的类型。您可以隐式地将合约转换为从他们继承的合约。

    合约和 address 的数据表示是相同的。在版本0.5.0之前,合约直接从地址类型派生的。

    合约可以显式转换为 address 类型。只有当合约具有可支付回退函数时,才能显式和 address payable 类型相互转换 转换仍然使用 address(x) 执行,而不是使用 address payable(x) 。

    调用其他合约

    1. 对于已部署的合约,声明一个合约类型的局部变量(MyContract c),则可以调用该合约的函数。
    2. 对于未部署的合约, 需要知道其代码,然后创建合约对象再调用。

    其他注意

    • 合约不支持任何运算符。
    • 合约类型的成员是合约的外部函数及 public 的 状态变量。
    • 对于合约 C 可以使用 type(C) 获取合约的类型信息。

    事件 Events

    Solidity 事件是EVM的日志功能之上的抽象。 应用程序可以通过以太坊客户端的RPC接口订阅和监听这些事件。

    事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在(在 Frontier 和 Homestead 版本中会被永久保存,在 Serenity 版本中可能会改动)。 日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。

    对日志的 SPV(Simplified Payment Verification)证明是可能的,如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中。 但需要留意的是,由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。

    参考资料