重要类型

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 函数的合约,直接接收以太币(没有函数调用,即使用 sendtransfer)会抛出一个异常, 并返还以太币(在 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 {}
    }

    数组

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

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

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

    地址类型

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

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

    区别

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

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

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

    类型转换

    允许从 address payableaddress 的隐式转换,而从 addressaddress 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 可用来表示 addressaddress payable 类型。

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

    成员变量和函数

    • balancetransfer()

    可以使用 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

    sendtransfer 的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 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 个区块哈希,所以还需要提供区块头信息。

    参考资料