类型

solidity是一种静态类型的语言,这意味着需要指定每个变量(state和local)的类型。solidity提供了几个基本类型,可以组合成复杂的类型。

此外,类型可以在包含运算符的表达式中相互作用。有关各种运算符的快速参考,请参见 运算符的优先顺序 .

“未定义”或“空”值的概念不存在于solidity中,但新声明的变量始终具有 default value 取决于它的类型。要处理任何意外值,应使用 revert function 还原整个事务,或用秒返回元组 bool 表示成功的价值。

价值类型

以下类型也称为值类型,因为这些类型的变量总是按值传递的,即当它们用作函数参数或在赋值中使用时,它们总是被复制的。

布尔值

bool :可能的值是常量 truefalse .

操作员:

  • ! (逻辑否定)

  • && (逻辑连接,“and”)

  • || (逻辑分离,“或”)

  • == (平等)

  • != (不等式)

运营商 ||&& 应用常见的短路规则。这意味着在表达式中 f(x) || g(y) 如果 f(x) 评估为 trueg(y) 即使有副作用也不会被评估。

整数

int / uint :各种大小的有符号和无符号整数。关键词 uint8uint256 步骤 8 (8到256位的无符号)和 int8int256 . uintint 是别名 uint256int256 ,分别。

操作员:

  • 比较: <=<==!=>=> (评估为 bool

  • 位运算符: &|^ (位异或), ~ (位否定)

  • 轮班操作员: << (左移) >> (右移位)

  • 算术运算符: +, -, unary - (only for signed integers), *, /, `` %``(模数), ** (求幂)

对于整数类型 X ,你可以使用 type(X).mintype(X).max 访问由类型表示的最小值和最大值。

警告

稠度中的整数被限制在一定范围内。例如,使用 uint32 ,这是 0 最高可达 2**32 - 1 。对这些类型执行运算有两种模式:“包装”或“取消检查”模式和“检查”模式。默认情况下,始终“检查”算术,这意味着如果操作的结果超出该类型的值范围,则调用将通过 failing assertion 。您可以使用以下命令切换到“取消选中”模式 unchecked {{ ... }} 。有关更多详细信息,请参阅关于 unchecked

比较

比较值是通过比较整数值得到的值。

位操作

位操作是在数字的两个补码表示上执行的。例如,这意味着 ~int256(0) == int256(-1) .

移位

移位运算的结果具有左操作数的类型,并截断结果以匹配该类型。右操作数必须是无符号类型,尝试按有符号类型移位将产生编译错误。

可以通过以下方式使用2的幂的乘法来“模拟”移位。请注意,对左操作数类型的截断始终在末尾执行,但没有明确提到。

  • x << y 等同于数学表达式 x * 2**y

  • x >> y 等同于数学表达式 x / 2**y ,四舍五入为负无穷大。

警告

之前的版本 0.5.0 右转 x >> y 对于负数 x 等同于数学表达式 x / 2**y 四舍五入到零,即使用右移向上舍入(向零),而不是向下舍入(向负无穷大)。

注解

不会对移位操作执行溢出检查,因为它们是针对算术操作执行的。相反,结果总是被截断。

加、减、乘

加法、减法和乘法具有通常的语义,对于上溢和下溢有两种不同的模式:

默认情况下,会检查所有算术是否存在不足或溢出,但可以使用 unchecked block ,从而产生包装算法。更多细节可以在那一节找到。

表达式 -x 相当于 (T(0) - x) 哪里 T 是类型为 x 。它只能应用于带符号的类型。的价值 -x 如果满足以下条件,则可以为正 x 是阴性的。从二的补码表示还可以得出另一个警告:

如果你有 int x = type(int).min; ,那么 -x 不符合正值范围。这意味着 unchecked {{ assert(-x == x); }} 作品,并且表达式 -x 在选中模式下使用时,将导致断言失败。

除法

由于运算结果的类型始终是其中一个操作数的类型,因此对整数进行除法始终会产生整数。在稳固中,除法向零四舍五入。这意味着 int256(-5) / int256(2) == int256(-2)

注意,与此相反,除法 literals 产生任意精度的分数值。

注解

除以零会导致 Panic error 。这张支票可以 not 通过以下方式被禁用 unchecked {{ ... }}

注解

表达式 type(int).min / (-1) 是除法导致溢出的唯一情况。在检查算术模式下,这将导致断言失败,而在包装模式下,该值将为 type(int).min

模运算 a % n 生成余数 r 操作数除后 a 按操作数 n 在哪里 q = int(a / n)r = a - (n * q) .这意味着模的结果与其左操作数(或零)相同,并且 a % n == -(-a % n) 保留为负数 a

  • int256(5) % int256(2) == int256(1)

  • int256(5) % int256(-2) == int256(1)

  • int256(-5) % int256(2) == int256(-1)

  • int256(-5) % int256(-2) == int256(-1)

注解

以零为模导致 Panic error 。这张支票可以 not 通过以下方式被禁用 unchecked {{ ... }}

求幂

求幂仅适用于指数中的无符号类型。求幂的结果类型始终等于基数的类型。请注意其大小足以容纳结果,并为潜在的断言失败或包装行为做好准备。

注解

在选中模式下,求幂仅使用相对便宜的 exp 用于小型基地的操作码。对于以下情况: x**3 ,该表达式 x*x*x 可能会更便宜。在任何情况下,汽油成本测试和优化器的使用都是可取的。

注解

注意 0**0 由EVM定义为 1 .

定点数

警告

固数还不完全支持定点数。它们可以声明,但不能分配给或来自。

fixed / ufixed :各种尺寸的有符号和无符号固定点号。关键词 ufixedMxNfixedMxN 在哪里 M 表示类型和 N 表示有多少个小数点可用。 M 必须能被8整除,从8位到256位。 N 必须介于0和80之间(含0和80)。 ufixedfixed 是别名 ufixed128x18fixed128x18 ,分别。

操作员:

  • 比较: <=<==!=>=> (评估为 bool

  • 算术运算符: +, -, unary -, *, /, `` %``(模块)

注解

浮点数的主要区别 (floatdouble 在许多语言中,更精确地说,IEEE754数字)和定点数字是整数和小数部分(小数点后的部分)所用的位数在前者是灵活的,而在后者中则是严格定义的。通常,在浮点中,几乎整个空间都用来表示数字,而只有少数位定义小数点的位置。

地址

地址类型有两种风格,基本相同:

  • address :保留一个20字节的值(以太坊地址的大小)。

  • address payable 一样 address 但是有了额外的成员 transfersend .

这种区别背后的想法是 address payable 是一个地址,你可以发送到 Ether ,而平原 address 不能用 Ether 发送。

类型转换:

隐式转换自 address payableaddress 允许,而转换自 addressaddress payable 必须通过显式 payable(<address>) .

显式来回转换 address 是允许的 uint160 ,整数字面值, bytes20 和合同类型。

仅类型的表达式 address 可以将合同型转换为合同型 address payable 通过显式转换 payable(...) 。对于合同类型,仅当合同可以接收以太时才允许此转换,即合同具有 receive 或付费后备功能。请注意, payable(0) 是有效的,并且是此规则的例外。

注解

如果需要类型为 address 并计划将Ether发送给它,然后将其类型声明为 address payable 要使此要求可见,请执行以下操作。此外,请尽可能早地进行区分或转换。

操作员:

  • <=, <, ==, !=, >= and >

警告

如果将使用较大字节大小的类型转换为 address ,例如 bytes32 然后 address 被截断。为了减少编译器0.4.24及更高版本的转换模糊性,您必须在转换中显式地进行截断。以32字节的值为例 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC .

你可以使用 address(uint160(bytes20(b))) 从而导致 0x111122223333444455556666777788889999aAaa ,或者您可以使用 address(uint160(uint256(b))) 从而导致 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc .

注解

两者之间的区别 addressaddress payable 是在0.5.0版本中引入的。同样,从该版本开始,协定不会从地址类型派生,但仍可以显式转换为 addressaddress payable 或具有回退功能。

地址的成员

有关地址的所有成员的快速参考,请参阅 地址类型的成员 .

  • balance and transfer

可以使用属性查询地址的余额 balance 并使用 transfer 功能:

address payable x = address(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

这个 transfer 如果当前合同余额不够大或收款账户拒绝 Ether 转账,则功能失败。这个 transfer 失败时功能恢复。

注解

如果 x 是合同地址,其代码(更具体地说:ITS 接收乙醚功能 ,如果存在,或 回退函数 (如果有)将与 transfer 调用(这是EVM的一个功能,无法阻止)。如果该执行耗尽气体或以任何方式失败,乙醚传输将被恢复,当前合同将异常终止。

  • send

send是 transfer .如果执行失败,当前合同将不会异常终止,但 send 将返回 false .

警告

使用中有一些危险 send :如果调用堆栈深度为1024,则传输失败(调用方始终可以强制执行此操作),如果收件人耗尽气体,则传输也会失败。因此,为了安全传输 Ether ,请始终检查 send 使用 transfer 或者更好:使用收款人取款的模式。

  • call, delegatecall and staticcall

为了与不遵守ABI的合同进行接口,或者对编码进行更直接的控制,函数 calldelegatecallstaticcall 提供。他们都要一张单人票 bytes memory 参数并返回成功条件(作为 bool )以及返回的数据 (bytes memory )。功能 abi.encodeabi.encodePackedabi.encodeWithSelectorabi.encodeWithSignature 可用于对结构化数据进行编码。

示例:

bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

警告

所有这些功能都是低级功能,应小心使用。具体地说,任何未知的契约都可能是恶意的,如果您调用它,您将控制权移交给该契约,而该契约反过来又会回调到您的契约中,因此当调用返回时,请准备好更改状态变量。与其他契约交互的常规方法是调用契约对象上的函数 (x.f()

注解

solidity的早期版本允许这些函数接收任意参数,并且还将处理类型为的第一个参数 bytes4 不同的是。在0.5.0版中删除了这些边缘案例。

可以用调整器来调节供气。 gas 修改器:

address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));

同样,也可以控制提供的以太值:

address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

最后,可以组合这些修饰符。它们的顺序并不重要:

address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

以类似的方式,函数 delegatecall 可以使用:区别在于只使用给定地址的代码,所有其他方面(存储、余额等)都取自当前合同。目的 delegatecall 是使用存储在另一个合同中的库代码。用户必须确保两个合同中的存储布局都适合使用DelegateCall。

注解

在宅基地之前,只有一个有限的变种叫做 callcode 不提供对原始文件的访问 msg.sendermsg.value 价值观。此函数在0.5.0版中被删除。

从拜占庭开始 staticcall 也可以使用。这基本上与 call ,但在被调用函数以任何方式修改状态时将恢复。

所有三个功能 calldelegatecallstaticcall 是非常低级的函数,只能用作 最后手段 因为它们破坏了类型的安全性。

这个 gas 选项在所有三种方法上都可用,而 value 选项仅在以下情况下可用 call

注解

最好避免依赖智能合约代码中的硬编码气体值,而不管状态是从中读取还是写入,因为这可能会有许多陷阱。而且,未来天然气的供应可能会改变。

注解

所有合同都可以转换为 address 类型,因此可以使用查询当前合同的余额 address(this).balance .

合同类型

每个 contract 定义自己的类型。您可以将合同隐式转换为它们继承的合同。合同可以明确地转换为 address 类型。

与之间的显式转换 address payable 只有在合同类型具有接收或应付回退功能时,类型才可能。转换仍然使用 address(x) . 如果合同类型没有接收或应付回退功能,则转换为 address payable 可以使用 payable(address(x)) .您可以在有关 address type .

注解

在0.5.0版本之前,直接从地址类型派生的合同与 addressaddress payable .

如果声明合同类型的局部变量 (MyContract c )可以调用该合同上的函数。注意从相同合同类型的某个地方分配它。

您还可以实例化契约(这意味着它们是新创建的)。您可以在 'Contracts via new' 部分。

合同的数据表示与 address 类型和此类型也用于 ABI .

合同不支持任何运营商。

合同类型的成员是合同的外部函数,包括标记为 public .

为了合同 C 你可以用 type(C) 访问 type information 关于合同。

固定大小字节数组

值类型 bytes1bytes2bytes3 ,., bytes32 保存从1到最多32个字节的序列。

操作员:

  • 比较: <=<==!=>=> (评估为 bool

  • 位运算符: &|^ (位异或), ~ (位否定)

  • 轮班操作员: << (左移) >> (右移位)

  • 索引访问:如果 x 属于类型 bytesI 然后 x[k] 对于 0 <= k < I 返回 k th字节(只读)。

移位运算符使用无符号整数类型作为右操作数(但返回左操作数的类型),它表示要移位的位数。按有符号类型移位将产生编译错误。

成员:

  • .length 生成字节数组的固定长度(只读)。

注解

类型 bytes1[] 是一个字节数组,但是由于填充规则,它为每个元素浪费了31字节的空间(存储中除外)。最好使用 bytes 改为键入。

注解

在0.8.0版之前, byte 以前是的别名 bytes1

动态调整字节数组大小

bytes

动态调整字节数组大小,请参见 数组 .不是值类型!

string

动态调整大小的utf-8编码字符串,请参见 数组 .不是值类型!

地址文字

通过地址校验和测试的十六进制文字,例如 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AFaddress 键入。长度在39到41位之间且未通过校验和测试的十六进制文字会产生错误。您可以在前面加上(对于整数类型)或追加(对于bytesNN类型)零来删除错误。

注解

混合大小写地址校验和格式定义于 EIP-55 .

有理和整数文本

整型文字由0-9范围内的一系列数字组成。它们被解释为小数。例如, 69 意思是69。八进制文字不存在于实度中,前导零无效。

小数部分文本由 . 一侧至少有一个数字。示例包括 1..11.3 .

也支持科学记数法,其中基数可以有分数,指数不能。示例包括 2e10-2e102e-102.5e1 .

下划线可用于分隔数字文本的数字,以帮助可读性。例如,十进制 123_000 ,十六进制 0x2eff_abde ,科学十进制符号 1_2e345_678 都是有效的。下划线只能在两位数字之间使用,并且只能使用一个连续的下划线。在包含下划线的数字字面值中没有附加的语义意义,下划线将被忽略。

数字-文字表达式保留任意精度,直到它们转换为非文字类型(即通过将它们与非文字表达式一起使用或通过显式转换)。这意味着计算不会溢出,而且在数字文本表达式中也不会截断除数。

例如, (2**800 + 1) - 2**800 结果是常数 1 (属于类型) uint8 )虽然中间结果甚至不适合机器的字大小。此外, .5 * 8 结果为整数 4 (尽管中间使用了非整数)。

任何可以应用于整数的运算符也可以应用于数字文字表达式,只要操作数是整数。如果其中任何一个是小数,则不允许位运算,如果指数是小数,则不允许求幂(因为这可能导致非有理数)。

始终在中执行以文字数字作为左(或基)操作数、以整数类型作为右(指数)操作数的移位和求幂运算 uint256 (对于非负数字面值)或 int256 (对于负数字面值)类型,与右(指数)操作数的类型无关。

警告

整型文字上的除法,用于在0.4.0版之前的solidity中截断,但现在它转换为有理数,即 5 / 2 不等于 2 ,但为了 2.5 .

注解

solidity对于每个有理数都有一个数字文本类型。整数文本和有理数文本属于数字文本类型。此外,所有数字文本表达式(即只包含数字文本和运算符的表达式)都属于数字文本类型。所以数字文字表达式 1 + 22 + 1 对于有理数3,两者都属于相同的数字文本类型。

注解

数字文本表达式一旦与非文本表达式一起使用,就会转换为非文本类型。忽略类型,将表达式的值赋给 b 下面的计算结果为整数。因为 a 属于类型 uint128 ,表达式 2.5 + a 但是必须有一个合适的类型。因为对于 2.5uint128 ,solidity编译器不接受此代码。

uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

字符串文本和类型

字符串文字是用双引号或单引号编写的 ("foo"'bar' ),它们也可以分成多个连续的部分 ("foo" "bar" 等于 "foobar" )这在处理长字符串时非常有用。它们不像C语言那样意味着后面有零; "foo" 表示三个字节,而不是四个字节。与整型文本一样,它们的类型也可以变化,但可以隐式转换为 bytes1bytes32 如果合适的话, bytes 并且 string .

例如,使用 bytes32 samevar = "stringliteral" 字符串文字在分配给 bytes32 类型。

字符串文字只能包含可打印的ASCII字符,即0x1F之间的字符。。0x7E。

此外,字符串文字还支持以下转义字符:

  • \<newline> (避开实际换行符)

  • \\ (反斜杠)

  • \' (单引号)

  • \" (双引号)

  • \n (换行)

  • \r (回车)

  • \t (制表符)

  • \xNN (十六进制转义,见下文)

  • \uNNNN (Unicode转义,见下文)

\xNN 接受十六进制值并插入适当的字节,而 \uNNNN 采用Unicode码位并插入一个UTF-8序列。

注解

在0.8.0版之前,有三个额外的转义序列: \b\f\v 。它们通常有其他语言版本,但在实践中很少需要。如果您确实需要它们,它们仍然可以通过十六进制转义插入,即 \x08\x0c\x0b ,就像任何其他ASCII字符一样。

以下示例中的字符串的长度为10个字节。它以换行字节开头,后跟双引号、单引号和反斜杠字符,然后(不带分隔符)字符序列 abcdef .

"\n\"\'\\abc\
def"

任何不是换行符的Unicode行终止符(即LF、VF、FF、CR、NEL、LS、PS)都被视为终止字符串文本。仅当字符串前面不是换行时才终止它 \ .

Unicode文字

而常规字符串文本只能包含ASCII、Unicode文本-前缀为关键字 unicode –可以包含任何有效的UTF-8序列。它们还支持与常规字符串字面值完全相同的转义序列。

string memory a = unicode"Hello 😃";

十六进制文字

十六进制文字的前缀是关键字 hex 并用双引号或单引号括起来 (hex"001122FF"hex'0011_22_FF' ). 它们的内容必须是十六进制数字,可以选择使用一个下划线作为字节边界之间的分隔符。文本的值将是十六进制序列的二进制表示。

多个由空格分隔的十六进制文本连接为一个文本: hex"00112233" hex"44556677" 等于 hex"0011223344556677"

十六进制文字的行为类似 string literals 具有相同的可兑换性限制。

枚举类型

枚举是在实心中创建用户定义类型的一种方式。它们可以显式转换为所有整数类型,但不允许进行隐式转换。从整数的显式转换在运行时检查该值是否位于枚举范围内,并导致 Panic error 不然的话。枚举至少需要一个成员,声明时其默认值为第一个成员。枚举的成员不能超过256个。

数据表示与C中的枚举相同:选项由后面的无符号整数值表示,该值从 0 .

使用 type(NameOfEnum).mintype(NameOfEnum).max 您可以获得给定枚举的最小值和最大值。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;
    }

    // Since enum types are not part of the ABI, the signature of "getChoice"
    // will automatically be changed to "getChoice() returns (uint8)"
    // for all matters external to Solidity.
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

    function getDefaultChoice() public pure returns (uint) {
        return uint(defaultChoice);
    }

    function getLargestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).max;
    }

    function getSmallestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).min;
    }
}

注解

枚举也可以在文件级声明,在协定或库定义之外。

用户定义的值类型

用户定义的值类型允许在基本值类型上创建零成本抽象。这类似于别名,但具有更严格的类型要求。

用户定义的值类型使用 type C is V ,在哪里 C 是新引入的类型的名称,并且 V 必须是内置值类型(“基础类型”)。该函数 C.wrap 用于从基础类型转换为自定义类型。类似地,函数 C.unwrap 用于从自定义类型转换为基础类型。

类型 C 没有任何运算符或绑定的成员函数。特别是,即使是操作员 == 未定义。不允许与其他类型进行显式和隐式转换。

这些类型的值的数据表示形式继承自底层类型,并且底层类型也在ABI中使用。

下面的示例说明了自定义类型 UFixed256x18 表示具有18个小数和对该类型执行算术运算的最小库的十进制定点类型。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

// Represent a 18 decimal, 256 bit wide fixed point type using a user defined value type.
type UFixed256x18 is uint256;

/// A minimal library to do fixed point operations on UFixed256x18.
library FixedMath {
    uint constant multiplier = 10**18;

    /// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked
    /// arithmetic on uint256.
    function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
    }
    /// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked
    /// arithmetic on uint256.
    function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
    }
    /// Take the floor of a UFixed256x18 number.
    /// @return the largest integer that does not exceed `a`.
    function floor(UFixed256x18 a) internal pure returns (uint256) {
        return UFixed256x18.unwrap(a) / multiplier;
    }
    /// Turns a uint256 into a UFixed256x18 of the same value.
    /// Reverts if the integer is too large.
    function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(a * multiplier);
    }
}

请注意如何 UFixed256x18.wrapFixedMath.toUFixed256x18 具有相同的签名,但执行两个截然不同的操作: UFixed256x18.wrap 函数返回一个 UFixed256x18 它具有与输入相同的数据表示形式,而 toUFixed256x18 返回一个 UFixed256x18 它们具有相同的数值。

函数类型

函数类型是函数的类型。函数类型的变量可以从函数中分配,函数类型的函数参数可以用于向函数调用传递函数和从函数调用返回函数。功能类型有两种味道- 内部的外部的 功能:

内部函数只能在当前协定内调用(更具体地说,在当前代码单元内,还包括内部库函数和继承函数),因为它们不能在当前协定的上下文之外执行。调用内部函数是通过跳到其条目标签来实现的,就像在内部调用当前契约的函数一样。

外部函数由地址和函数签名组成,它们可以通过外部函数调用传递和返回。

函数类型表示如下:

function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]

与参数类型不同,返回类型不能为空-如果函数类型不应返回任何内容,则返回整个 returns (<return types>) 必须省略部分。

默认情况下,函数类型是内部的,因此 internal 关键字可以省略。注意,这只适用于函数类型。必须为合同中定义的函数显式指定可见性,它们没有默认值。

转换:

A函数类型 A 是隐式可转换为函数类型的 B 当且仅当它们的参数类型相同时,它们的返回类型也相同,它们的内部/外部属性也相同,并且 A 的状态易变性更具限制性。 B 。尤其是:

  • pure 函数可以转换为 viewnon-payable 功能

  • view 函数可以转换为 non-payable 功能

  • payable 函数可以转换为 non-payable 功能

函数类型之间不能进行其他转换。

关于的规则 payablenon-payable 可能有点混乱,但实际上,如果函数 payable ,这意味着它也接受零 Ether 的付款,所以它也 non-payable .另一方面,A non-payable 函数将拒绝发送给它的 Ether ,因此 non-payable 无法将函数转换为 payable 功能。

如果函数类型变量未初始化,则调用它会导致 Panic error 。如果在使用后调用函数,也会发生同样的情况 delete 这就去办。

如果外部函数类型在solidity上下文之外使用,则它们将被视为 function 类型,将地址与函数标识符一起编码为一个 bytes24 类型。

请注意,当前合同的公共功能既可以用作内部功能,也可以用作外部功能。使用 f 作为一个内部函数,只需使用 f ,如果要使用其外部窗体,请使用 this.f .

可以将内部类型的函数赋给内部函数类型的变量,而不管它是在哪里定义的。这包括合同和库的私有函数、内部函数和公共函数以及免费函数。外部函数类型则只兼容公共函数和外部合同函数。库被排除在外,因为它们需要 delegatecall 并使用 a different ABI convention for their selectors 。接口中声明的函数没有定义,因此指向它们也没有意义。

成员:

外部(或公共)功能具有以下成员:

注解

用于拥有附加成员的外部(或公共)函数 .gas(uint).value(uint) . 它们在Solidity 0.6.2中被弃用,在Solidity 0.7.0中被删除。而是使用 {{gas: ...}}{{value: ...}} 分别指定发送到函数的气体量或wei的量。看到了吗 External Function Calls 更多信息。

说明如何使用成员的示例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.4 <0.9.0;

contract Example {
    function f() public payable returns (bytes4) {
        assert(this.f.address == address(this));
        return this.f.selector;
    }

    function g() public {
        this.f{gas: 10, value: 800}();
    }
}

说明如何使用内部函数类型的示例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

library ArrayUtils {
    // internal functions can be used in internal library functions because
    // they will be part of the same code context
    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {
        r = new uint[](self.length);
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]);
        }
    }

    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
    )
        internal
        pure
        returns (uint r)
    {
        r = self[0];
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]);
        }
    }

    function range(uint length) internal pure returns (uint[] memory r) {
        r = new uint[](length);
        for (uint i = 0; i < r.length; i++) {
            r[i] = i;
        }
    }
}


contract Pyramid {
    using ArrayUtils for *;

    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }

    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

另一个使用外部函数类型的示例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;


contract Oracle {
    struct Request {
        bytes data;
        function(uint) external callback;
    }

    Request[] private requests;
    event NewRequest(uint);

    function query(bytes memory data, function(uint) external callback) public {
        requests.push(Request(data, callback));
        emit NewRequest(requests.length - 1);
    }

    function reply(uint requestID, uint response) public {
        // Here goes the check that the reply comes from a trusted source
        requests[requestID].callback(response);
    }
}


contract OracleUser {
    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
    uint private exchangeRate;

    function buySomething() public {
        ORACLE_CONST.query("USD", this.oracleResponse);
    }

    function oracleResponse(uint response) public {
        require(
            msg.sender == address(ORACLE_CONST),
            "Only oracle can call this."
        );
        exchangeRate = response;
    }
}

注解

lambda或内联函数是计划的,但尚未得到支持。

引用类型

引用类型的值可以通过多个不同的名称进行修改。与使用值类型变量时获得独立副本的值类型相比。因此,处理引用类型必须比处理值类型更仔细。目前,引用类型包括结构、数组和映射。如果使用引用类型,则始终必须显式提供存储该类型的数据区域: memory (其生存期仅限于外部函数调用), storage (存储状态变量的位置,其生存期仅限于合同的生存期)或 calldata (包含函数参数的特殊数据位置)。

更改数据位置的分配或类型转换将始终引发自动复制操作,而在某些情况下,同一数据位置内的分配仅复制存储类型。

数据位置

每种引用类型都有一个关于其存储位置的附加注释,即“数据位置”。有三个数据位置: memorystoragecalldata 。Calldata是存储函数参数的不可修改、非持久区域,其行为主要类似于内存。

注解

如果可以,尝试使用 calldata 作为数据位置,因为这样可以避免复制,而且还可以确保数据不能修改。数组和结构 calldata 数据位置也可以从函数返回,但不能分配这样的类型。

注解

在0.6.9版之前,引用类型参数的数据位置限制为 calldata 在外部函数中, memory 在公共活动中,或者 memorystorage 内部的和私人的。现在 memorycalldata 在所有函数中都允许使用,而不管其可见性如何。

注解

在0.5.0版本之前,可以忽略数据位置,并根据变量类型、函数类型等默认为不同的位置,但所有复杂类型现在必须给出明确的数据位置。

数据位置和分配行为

数据位置不仅与数据的持久性有关,还与分配的语义有关:

  • 工作分配介于 storagememory (或来自 calldata )始终创建独立副本。

  • 工作分配来自 memorymemory 仅创建引用。这意味着对一个内存变量的更改在引用相同数据的所有其他内存变量中也可见。

  • 工作分配来自 storage 到A 地方的 存储变量也只分配一个引用。

  • 所有其他工作分配给 storage 总是复制。这种情况下的示例是对状态变量或存储结构类型的局部变量成员的赋值,即使局部变量本身只是一个引用。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    // The data location of x is storage.
    // This is the only place where the
    // data location can be omitted.
    uint[] x;

    // The data location of memoryArray is memory.
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // works, copies the whole array to storage
        uint[] storage y = x; // works, assigns a pointer, data location of y is storage
        y[7]; // fine, returns the 8th element
        y.pop(); // fine, modifies x through y
        delete x; // fine, clears the array, also modifies y
        // The following does not work; it would need to create a new temporary /
        // unnamed array in storage, but storage is "statically" allocated:
        // y = memoryArray;
        // This does not work either, since it would "reset" the pointer, but there
        // is no sensible location it could point to.
        // delete y;
        g(x); // calls g, handing over a reference to x
        h(x); // calls h and creates an independent, temporary copy in memory
    }

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

数组

数组可以具有编译时固定大小,也可以具有动态大小。

固定大小数组的类型 k 和元素类型 T 写为 T[k] 和动态大小的数组 T[] .

例如,一个由5个动态数组组成的数组 uint 写为 uint[][5] .与其他一些语言相比,这个符号是颠倒的。在 Solidity 方面, X[3] 始终是包含三个类型元素的数组 X ,即使 X 本身就是一个数组。其他语言(如C)则不是这样。

索引是以零为基础的,访问与声明的方向相反。

例如,如果您有一个变量 uint[][5] memory x ,你可以进入第七区 uint 在第三个动态数组中使用 x[2][6] ,要访问第三个动态数组,请使用 x[2] 。同样,如果您有一个数组 T[5] a 对于一种类型 T 那也可以是一个数组,那么 a[2] 始终具有类型 T

数组元素可以是任何类型,包括映射或结构。应用了类型的一般限制,因为映射只能存储在 storage 数据位置和公共可见函数需要的参数是 ABI types .

可以标记状态变量数组 public 并具有稳固性,创造 getter .数字索引成为getter的必需参数。

访问超过其结尾的数组会导致断言失败。方法 .push().push(value) 可用于在数组末尾追加新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。

bytesstring AS数组

类型的变量 bytesstring 是特殊的数组。这个 bytes 类型类似于 bytes1[] ,但是它被紧密地打包在calldata和内存中。 string 等于 bytes 但不允许长度或索引访问。

Solidity没有字符串操作函数,但有第三方字符串库。还可以使用以下命令通过keccak256-hash比较两个字符串 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) 并使用以下命令连接两个字符串 bytes.concat(bytes(s1), bytes(s2))

您应该使用 bytes 完毕 bytes1[] 因为它更便宜,因为 bytes1[] 在元素之间添加31个填充字节。作为一般规则,使用 bytes 对于任意长度的原始字节数据和 string 用于任意长度字符串(UTF-8)数据。如果可以将长度限制为一定的字节数,请始终使用以下值类型之一 bytes1bytes32 因为它们便宜多了。

注解

如果要访问字符串的字节表示形式 s 使用 bytes(s).length / bytes(s)[7] = 'x'; .请记住,您访问的是UTF-8表示的低级字节,而不是单个字符。

bytes.concat 功能

您可以串联可变数量的 bytesbytes1 ... bytes32 使用 bytes.concat 。该函数返回单个 bytes memory 数组,该数组包含没有填充的参数内容。如果要使用字符串参数或其他类型,则需要将它们转换为 bytesbytes1 /./``bytes32``首先。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract C {
    bytes s = "Storage";
    function f(bytes calldata c, string memory m, bytes16 b) public view {
        bytes memory a = bytes.concat(s, c, c[:2], "Literal", bytes(m), b);
        assert((s.length + c.length + 2 + 7 + bytes(m).length + 16) == a.length);
    }
}

如果你打电话给 bytes.concat 如果没有参数,它将返回一个空 bytes 数组。

分配内存数组

可以使用 new 操作员。与存储阵列不同,它是 not 可以调整内存阵列的大小(例如 .push 成员函数不可用)。您要么事先计算所需的大小,要么创建一个新的内存数组并复制每个元素。

由于所有变量都是实心变量,因此新分配的数组的元素始终使用 default value

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        assert(a.length == 7);
        assert(b.length == len);
        a[6] = 8;
    }
}

数组常量

数组文字是一个或多个表达式的逗号分隔列表,用方括号括起来 ([...] )。例如 [1, a, f(3)] 。数组文字的类型按如下方式确定:

它始终是一个静态大小的内存数组,其长度是表达式的数量。

数组的基类型是列表中第一个表达式的类型,因此所有其他表达式都可以隐式转换为它。如果这是不可能的,则是类型错误。

仅有一个类型可以将所有元素转换为类型是不够的。其中一个元素必须属于该类型。

在下面的示例中, [1, 2, 3]uint8[3] memory ,因为每个常量的类型都是 uint8 。如果希望结果是 uint[3] memory 类型,则需要将第一个元素转换为 uint

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] memory) public pure {
        // ...
    }
}

数组文字 [1, -1] 无效,因为第一个表达式的类型为 uint8 而第二个类型是 int8 并且它们不能相互隐式转换。要使其正常工作,您可以使用 [int8(1), -1] 例如,。

由于不同类型的固定大小内存数组不能相互转换(即使基类型可以),如果要使用二维数组文字,则始终必须显式指定公共基类型:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure returns (uint24[2][4] memory) {
        uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
        // The following does not work, because some of the inner arrays are not of the right type.
        // uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
        return x;
    }
}

固定大小的内存阵列不能分配给动态大小的内存阵列,即以下情况是不可能的:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

// This will not compile.
contract C {
    function f() public {
        // The next line creates a type error because uint[3] memory
        // cannot be converted to uint[] memory.
        uint[] memory x = [uint(1), 3, 4];
    }
}

它计划在将来取消这一限制,但由于数组是如何在ABI中传递的,所以会造成一些复杂的问题。

如果要初始化动态大小的数组,则必须分配各个元素:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure {
        uint[] memory x = new uint[](3);
        x[0] = 1;
        x[1] = 3;
        x[2] = 4;
    }
}

数组成员

长度

数组具有 length 包含其元素数的成员。内存数组的长度是固定的(但是是动态的,即它可以依赖于运行时参数)。

推()

动态存储阵列和 bytes (不是) string )调用成员函数 push() 可以用来在数组末尾附加一个零初始化的元素。它返回对元素的引用,以便可以像 x.push().t = 2x.push() = b .

推(x)

动态存储阵列和 bytes (不是) string )调用成员函数 push(x) 您可以使用它在数组末尾附加给定元素。函数不返回任何内容。

pop

动态存储阵列和 bytes (不是) string )调用成员函数 pop 可以用于从数组末尾移除元素。这也隐式调用 delete 在拆下的元件上。

注解

通过调用 push() 因为存储是零初始化,而通过调用来减少长度,所以具有恒定的气体成本 pop() 其成本取决于被移除元素的“大小”。如果该元素是一个数组,则代价可能非常高,因为它包括显式清除移除的元素,类似于调用 delete 在他们身上。

注解

要在外部(而不是公共)函数中使用数组数组,需要激活ABI编码器v2。

注解

在拜占庭之前的EVM版本中,无法访问函数调用返回的动态数组。如果调用返回动态数组的函数,请确保使用设置为拜占庭模式的EVM。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // Note that the following is not a pair of dynamic arrays but a
    // dynamic array of pairs (i.e. of fixed size arrays of length two).
    // Because of that, T[] is always a dynamic array of T, even if T
    // itself is an array.
    // Data location for all state variables is storage.
    bool[2][] m_pairsOfFlags;

    // newPairs is stored in memory - the only possibility
    // for public contract function arguments
    function setAllFlagPairs(bool[2][] memory newPairs) public {
        // assignment to a storage array performs a copy of ``newPairs`` and
        // replaces the complete array ``m_pairsOfFlags``.
        m_pairsOfFlags = newPairs;
    }

    struct StructType {
        uint[] contents;
        uint moreInfo;
    }
    StructType s;

    function f(uint[] memory c) public {
        // stores a reference to ``s`` in ``g``
        StructType storage g = s;
        // also changes ``s.moreInfo``.
        g.moreInfo = 2;
        // assigns a copy because ``g.contents``
        // is not a local variable, but a member of
        // a local variable.
        g.contents = c;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // access to a non-existing index will throw an exception
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // using push and pop is the only way to change the
        // length of an array
        if (newSize < m_pairsOfFlags.length) {
            while (m_pairsOfFlags.length > newSize)
                m_pairsOfFlags.pop();
        } else if (newSize > m_pairsOfFlags.length) {
            while (m_pairsOfFlags.length < newSize)
                m_pairsOfFlags.push();
        }
    }

    function clear() public {
        // these clear the arrays completely
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // identical effect here
        m_pairsOfFlags = new bool[2][](0);
    }

    bytes m_byteData;

    function byteArrays(bytes memory data) public {
        // byte arrays ("bytes") are different as they are stored without padding,
        // but can be treated identical to "uint8[]"
        m_byteData = data;
        for (uint i = 0; i < 7; i++)
            m_byteData.push();
        m_byteData[3] = 0x08;
        delete m_byteData[2];
    }

    function addFlag(bool[2] memory flag) public returns (uint) {
        m_pairsOfFlags.push(flag);
        return m_pairsOfFlags.length;
    }

    function createMemoryArray(uint size) public pure returns (bytes memory) {
        // Dynamic memory arrays are created using `new`:
        uint[2][] memory arrayOfPairs = new uint[2][](size);

        // Inline arrays are always statically-sized and if you only
        // use literals, you have to provide at least one type.
        arrayOfPairs[0] = [uint(1), 2];

        // Create a dynamic byte array:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = bytes1(uint8(i));
        return b;
    }
}

数组切片

数组切片是数组连续部分上的视图。它们被写成 x[start:end] 在哪里 startend 是导致uint256类型(或隐式转换为它)的表达式。切片的第一个元素是 x[start] 最后一个元素是 x[end - 1] .

如果 start 大于 end 或如果 end 抛出的异常的长度大于数组的长度。

两个 startend 可选: start 默认为 0end 默认为数组的长度。

数组切片没有任何成员。它们可以隐式转换为其底层类型的数组,并支持索引访问。索引访问在底层数组中不是绝对的,而是相对于片的开始。

数组切片没有类型名,这意味着没有变量可以将数组切片作为类型,它们只存在于中间表达式中。

注解

到目前为止,数组切片只为calldata数组实现。

数组切片有助于ABI解码函数参数中传递的辅助数据:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.5 <0.9.0;
contract Proxy {
    /// @dev Address of the client contract managed by proxy i.e., this contract
    address client;

    constructor(address _client) {
        client = _client;
    }

    /// Forward call to "setOwner(address)" that is implemented by client
    /// after doing basic validation on the address argument.
    function forward(bytes calldata _payload) external {
        bytes4 sig = bytes4(_payload[:4]);
        // Due to truncating behaviour, bytes4(_payload) performs identically.
        // bytes4 sig = bytes4(_payload);
        if (sig == bytes4(keccak256("setOwner(address)"))) {
            address owner = abi.decode(_payload[4:], (address));
            require(owner != address(0), "Address of owner cannot be zero.");
        }
        (bool status,) = client.delegatecall(_payload);
        require(status, "Forwarded call failed.");
    }
}

结构体

solidity提供了一种以结构形式定义新类型的方法,如下例所示:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// Defines a new type with two fields.
// Declaring a struct outside of a contract allows
// it to be shared by multiple contracts.
// Here, this is not really needed.
struct Funder {
    address addr;
    uint amount;
}

contract CrowdFunding {
    // Structs can also be defined inside contracts, which makes them
    // visible only there and in derived contracts.
    struct Campaign {
        address payable beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }

    uint numCampaigns;
    mapping (uint => Campaign) campaigns;

    function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID is return variable
        // We cannot use "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
        // because the right hand side creates a memory-struct "Campaign" that contains a mapping.
        Campaign storage c = campaigns[campaignID];
        c.beneficiary = beneficiary;
        c.fundingGoal = goal;
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // Creates a new temporary memory struct, initialised with the given values
        // and copies it over to storage.
        // Note that you can also use Funder(msg.sender, msg.value) to initialise.
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

合同没有提供众筹合同的全部功能,但它包含了理解结构所必需的基本概念。结构类型可以在映射和数组中使用,它们本身也可以包含映射和数组。

虽然结构本身可以是映射成员的值类型,也可以包含其类型的动态大小数组,但结构不可能包含其自身类型的成员。这种限制是必要的,因为结构的大小必须是有限的。

注意在所有函数中,结构类型是如何分配给具有数据位置的局部变量的。 storage .这不会复制结构,但只存储引用,以便对局部变量成员的赋值实际写入状态。

当然,也可以直接访问结构的成员,而不将其赋给局部变量,如 campaigns[campaignID].amount = 0 .

注解

在Solidity 0.7.0之前,包含仅存储类型(例如映射)成员的内存结构是允许的,并且如下所示的赋值 campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0) 将会起作用,只需静默跳过这些成员即可。

映射类型

映射类型使用语法 mapping(_KeyType => _ValueType) 映射类型的变量使用语法声明 mapping(_KeyType => _ValueType) _VariableName . 这个 _KeyType 可以是任何内置值类型, bytesstring ,或任何协定或枚举类型。不允许使用其他用户定义的或复杂的类型,例如映射、结构或数组类型。 _ValueType 可以是任何类型,包括映射、数组和结构。

您可以将映射想象为 hash tables ,它实际上是初始化的,这样每个可能的键都存在,并且映射到一个字节表示为零的值,一个类型的 default value .相似性到此结束,关键数据不存储在映射中,只存储在 keccak256 哈希用于查找值。

因此,映射没有被设置的键或值的长度或概念,因此如果没有有关已分配键的额外信息,则无法删除映射(请参见 清除映射

映射的数据位置只能为 storage 因此,允许状态变量作为函数中的存储引用类型,或作为库函数的参数。它们不能用作公开可见的协定函数的参数或返回参数。对于包含映射的数组和结构,这些限制也是正确的。

可以将映射类型的状态变量标记为 public 坚固性创造了 getter 为你。这个 _KeyType 成为getter的参数。如果 _ValueType 是值类型或结构,getter返回 _ValueType .如果 _ValueType 是数组或映射,getter为每个数组或映射都有一个参数 _KeyType ,递归地。

在下面的示例中, MappingExample 合同定义了 balances 映射,键类型为an address ,值类型a uint ,将以太坊地址映射为无符号整数值。作为 uint 是一个值类型,getter返回一个与类型匹配的值,可以在 MappingUser 返回指定地址处的值的协定。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(address(this));
    }
}

下面的示例是 ERC20 token . _allowances 是另一个映射类型内的映射类型的示例。下面的示例使用 _allowances 记录别人可以从你的帐户中提取的金额。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract MappingExample {

    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }

    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        _transfer(sender, recipient, amount);
        approve(sender, msg.sender, amount);
        return true;
    }

    function approve(address owner, address spender, uint256 amount) public returns (bool) {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

        _balances[sender] -= amount;
        _balances[recipient] += amount;
        emit Transfer(sender, recipient, amount);
    }
}

Iterable映射

不能迭代映射,即不能枚举它们的键。不过,可以在它们之上实现一个数据结构,并在其上进行迭代。例如,下面的代码实现了 IterableMapping 类库 User 然后合同也会添加数据,并且 sum 函数迭代以求所有值的总和。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.8 <0.9.0;

struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

struct itmap {
    mapping(uint => IndexValue) data;
    KeyFlag[] keys;
    uint size;
}

library IterableMapping {
    function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
        uint keyIndex = self.data[key].keyIndex;
        self.data[key].value = value;
        if (keyIndex > 0)
            return true;
        else {
            keyIndex = self.keys.length;
            self.keys.push();
            self.data[key].keyIndex = keyIndex + 1;
            self.keys[keyIndex].key = key;
            self.size++;
            return false;
        }
    }

    function remove(itmap storage self, uint key) internal returns (bool success) {
        uint keyIndex = self.data[key].keyIndex;
        if (keyIndex == 0)
            return false;
        delete self.data[key];
        self.keys[keyIndex - 1].deleted = true;
        self.size --;
    }

    function contains(itmap storage self, uint key) internal view returns (bool) {
        return self.data[key].keyIndex > 0;
    }

    function iterate_start(itmap storage self) internal view returns (uint keyIndex) {
        return iterate_next(self, type(uint).max);
    }

    function iterate_valid(itmap storage self, uint keyIndex) internal view returns (bool) {
        return keyIndex < self.keys.length;
    }

    function iterate_next(itmap storage self, uint keyIndex) internal view returns (uint r_keyIndex) {
        keyIndex++;
        while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
            keyIndex++;
        return keyIndex;
    }

    function iterate_get(itmap storage self, uint keyIndex) internal view returns (uint key, uint value) {
        key = self.keys[keyIndex].key;
        value = self.data[key].value;
    }
}

// How to use it
contract User {
    // Just a struct holding our data.
    itmap data;
    // Apply library functions to the data type.
    using IterableMapping for itmap;

    // Insert something
    function insert(uint k, uint v) public returns (uint size) {
        // This calls IterableMapping.insert(data, k, v)
        data.insert(k, v);
        // We can still access members of the struct,
        // but we should take care not to mess with them.
        return data.size;
    }

    // Computes the sum of all stored data.
    function sum() public view returns (uint s) {
        for (
            uint i = data.iterate_start();
            data.iterate_valid(i);
            i = data.iterate_next(i)
        ) {
            (, uint value) = data.iterate_get(i);
            s += value;
        }
    }
}

涉及lvalues的运算符

如果 a 是一个左值(即一个变量或一些可以分配给它的东西),以下运算符可用作速记符:

a += e 相当于 a = a + e 。操作员 -=*=/=%=|=&=^=<<=>>= 都相应地进行了定义。 a++a-- 相当于 a += 1 / a -= 1 但是表达式本身仍然具有先前的值 a 。相比之下, --a++a 对…有同样的效果 a 但在更改后返回值。

删除

delete a 将类型的初始值赋给 a .也就是说,对于整数,它等于 a = 0 ,但它也可以用于数组,在数组中,它指定长度为零的动态数组或长度相同的静态数组,所有元素都设置为其初始值。 delete a[x] 删除索引处的项 x 保留所有其他元素和数组长度不变。这特别意味着它在数组中留下了一个空白。如果计划删除项,则 mapping 可能是个更好的选择。

对于结构,它分配一个具有所有成员重置的结构。换句话说,价值 a 之后 delete a 就像是 a 将在不转让的情况下宣布,但需注意以下事项:

delete 对映射没有影响(因为映射的键可能是任意的,通常是未知的)。因此,如果删除一个结构,它将重置所有不是映射的成员,并且除非这些成员是映射,否则还会递归到这些成员中。但是,可以删除各个键及其映射到的内容:如果 a 是一个映射,那么 delete a[x] 将删除存储在 x .

重要的是要注意 delete a 真的像是分配给 a ,即,它将新对象存储在 a .当 a 是引用变量:它将只重置 a 它本身,而不是它之前提到的值。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; // sets x to 0, does not affect data
        delete data; // sets data to 0, does not affect x
        uint[] storage y = dataArray;
        delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also
        // y is affected which is an alias to the storage object
        // On the other hand: "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        assert(y.length == 0);
    }
}

基本类型之间的转换

隐式转换

在某些情况下,在赋值过程中、向函数传递参数以及应用运算符时,编译器会自动应用隐式类型转换。一般来说,如果值类型之间的隐式转换在语义上有意义并且不丢失任何信息,则是可能的。

例如, uint8 可转换为 uint16int128int256 ,但是 int8 不能转换为 uint256 ,因为 uint256 不能保存值,例如 -1 .

如果一个运算符应用于不同的类型,编译器会尝试隐式地将一个操作数转换为另一个操作数的类型(赋值也是如此)。这意味着运算总是以其中一个操作数的类型执行。

有关哪些隐式转换是可能的详细信息,请参阅有关类型本身的部分。

在下面的示例中, yz ,加法运算的操作数不具有相同的类型,但是 uint8 可以隐式转换为 uint16 反之亦然。正因为如此, y 被转换为 z 在中执行加法之前, uint16 键入。表达式的结果类型 y + zuint16 。因为它被赋给类型为 uint32 在加法之后执行另一个隐式转换。

uint8 y;
uint16 z;
uint32 x = y + z;

显示转换

如果编译器不允许隐式转换,但您确信转换可以工作,则有时可以进行显式类型转换。这可能会导致意外的行为,并允许您绕过编译器的一些安全功能,因此请确保测试结果是您想要和期望的!

以下面的示例为例,转换一个负数 int 到A uint

int  y = -3;
uint x = uint(y);

在这段代码的末尾, x 会有价值的 0xfffff..fd (64个十六进制字符),在两个256位的补码表示中是-3。

如果将整数显式转换为较小类型,则高阶位将被截断:

uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now

如果将整数显式转换为更大的类型,则会在左侧(即较高阶端)填充。转换结果将与原始整数进行比较:

uint16 a = 0x1234;
uint32 b = uint32(a); // b will be 0x00001234 now
assert(a == b);

固定大小的字节类型在转换期间的行为不同。它们可以被认为是单个字节的序列,转换为较小的类型将切断该序列:

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b will be 0x12

如果将固定大小的字节类型显式转换为更大的类型,则会在右侧填充该类型。访问固定索引处的字节将导致转换前后的值相同(如果索引仍在范围内):

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b will be 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

由于整数和固定大小的字节数组在截断或填充时的行为不同,因此只有在整数和固定大小的字节数组具有相同大小的情况下,才允许在两者之间进行显式转换。如果要在不同大小的整数和固定大小的字节数组之间进行转换,则必须使用使所需的截断和填充规则显式的中间转换:

bytes2 a = 0x1234;
uint32 b = uint16(a); // b will be 0x00001234
uint32 c = uint32(bytes4(a)); // c will be 0x12340000
uint8 d = uint8(uint16(a)); // d will be 0x34
uint8 e = uint8(bytes1(a)); // e will be 0x12

bytes 阵列和 bytes calldata切片可以显式转换为固定字节类型 (bytes1 /./bytes32)。如果数组长度超过目标固定字节类型,则将在末尾进行截断。如果数组比目标类型短,则末尾将用零填充。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.5;

contract C {
    bytes s = "abcdefgh";
    function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) {
        require(c.length == 16, "");
        bytes16 b = bytes16(m);  // if length of m is greater than 16, truncation will happen
        b = bytes16(s);  // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0"
        bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc"
        b = bytes16(c[:8]);  // also padded with zeros
        return (b, b1);
    }
}

文字和基本类型之间的转换

整数类型

十进制和十六进制数字文字可以隐式转换为任何足以表示它而无需截断的整数类型:

uint8 a = 12; // fine
uint32 b = 1234; // fine
uint16 c = 0x123456; // fails, since it would have to truncate to 0x3456

注解

在0.8.0版之前,任何十进制或十六进制数字文字都可以显式转换为整数类型。从0.8.0开始,这样的显式转换与隐式转换一样严格,也就是说,只有在文字符合结果范围时才允许这样的转换。

固定大小字节数组

十进制数字文字不能隐式转换为固定大小的字节数组。十六进制数字文字可以是,但前提是祸不单行的位数正好适合字节类型的大小。作为例外,值为零的十进制和十六进制文字都可以转换为任何固定大小的字节类型:

bytes2 a = 54321; // not allowed
bytes2 b = 0x12; // not allowed
bytes2 c = 0x123; // not allowed
bytes2 d = 0x1234; // fine
bytes2 e = 0x0012; // fine
bytes4 f = 0; // fine
bytes4 g = 0x0; // fine

如果字符串文字和祸不单行字符串文字的字符数与字节类型的大小匹配,则它们可以隐式转换为固定大小的字节数组:

bytes2 a = hex"1234"; // fine
bytes2 b = "xy"; // fine
bytes2 c = hex"12"; // not allowed
bytes2 d = hex"123"; // not allowed
bytes2 e = "x"; // not allowed
bytes2 f = "xyz"; // not allowed

地址

如上所述 地址文字 ,通过校验和测试的正确大小的十六进制文本为 address 类型。不能将其他文本隐式转换为 address 类型。

显式转换自 bytes20 或任何整数类型 address 导致 address payable .

address a 可以转换为 address payable 通过 payable(a) .