// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Foo {
function bar(bytes3[2] memory) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes memory, bool, uint[] memory) public pure {}
因此,对于我们的例子 Foo
,如果我们想用 69
和 true
做参数调用 baz
,
我们总共需要传送 68 字节,可以分解为:
0xcdcd77c0
: 方法ID。这源自ASCII格式的 baz(uint32,bool)
签名的 Keccak 哈希的前 4 字节。
0x0000000000000000000000000000000000000000000000000000000000000045
: 第一个参数,
一个被用 0 值字节补充到 32 字节的 uint32 值 69
。
0x0000000000000000000000000000000000000000000000000000000000000001
: 第二个参数,
一个被用 0 值字节补充到 32 字节的 boolean 值 true
。
合起来就是:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
它返回一个 bool
。比如它返回 false
,
那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000
,
一个 bool
值。
如果我们想用 ["abc", "def"]
做参数调用 bar
,我们总共需要传送 68 字节,可以分解为:
0xfce353f6
: 方法ID。源自 bar(bytes3[2])
的签名。
0x6162630000000000000000000000000000000000000000000000000000000000
: 第一个参数的第一部分,
一个 bytes3
值 "abc"
(左对齐)。
0x6465660000000000000000000000000000000000000000000000000000000000
: 第一个参数的第二部分,
一个 bytes3
值 "def"
(左对齐)。
合起来就是:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
如果我们想用 "dave"
, true
和 [1,2,3]
作为参数调用 sam
,
我们总共需要传送 292 字节,可以分解为:
0xa5643bf2
: 方法ID。这是从签名 sam(bytes,bool,uint256[])
中导出的。注意, uint
被替换为其典型代表 uint256
。
0x0000000000000000000000000000000000000000000000000000000000000060
: 第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是 0x60
。
0x0000000000000000000000000000000000000000000000000000000000000001
: 第二个参数:boolean 的 true。
0x00000000000000000000000000000000000000000000000000000000000000a0
: 第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是 0xa0
。
0x0000000000000000000000000000000000000000000000000000000000000004
: 第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。
0x6461766500000000000000000000000000000000000000000000000000000000
: 第一个参数的内容: "dave"
的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。
0x0000000000000000000000000000000000000000000000000000000000000003
: 第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。
0x0000000000000000000000000000000000000000000000000000000000000001
: 第三个参数的第一个数组元素。
0x0000000000000000000000000000000000000000000000000000000000000002
: 第三个参数的第二个数组元素。
0x0000000000000000000000000000000000000000000000000000000000000003
: 第三个参数的第三个数组元素。
合起来就是:
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
动态类型的使用
用值为 (0x123, [0x456, 0x789], "1234567890", "Hello, world!")
的签名参数调用
函数 f(uint256,uint32[],bytes10,bytes)
,其的编码方式如下:
我们取 keccak("f(uint256,uint32[],bytes10,bytes)")
的前四个字节,即 0x8be65246
。
然后我们对所有四个参数的头部部分进行编码。对静态类型 uint256
和 bytes10
,
这些是我们要直接传递的值,而对于动态类型 uint32[]
和 bytes
,
我们使用其数据区开始的偏移量,从需编码的值的开始位置算起
(即不计算包含函数签名哈希值的前四个字节)。也就是:
0x0000000000000000000000000000000000000000000000000000000000000123
( 0x123
补充到 32 字节)
0x0000000000000000000000000000000000000000000000000000000000000080
(第二个参数的数据部分起始位置的偏移量,4*32 字节,正好是头部的大小)
0x3132333435363738393000000000000000000000000000000000000000000000
( "1234567890"
从右边补充到 32 字节)
0x00000000000000000000000000000000000000000000000000000000000000e0
(第四个参数的数据部分起始位置的偏移量 = 第一个动态参数的数据部分起始位置的偏移量 + 第一个动态参数的数据部分的长度 = 4*32 + 3*32,参考后文)
在此之后,跟着第一个动态参数的数据部分, [0x456, 0x789]
:
0x0000000000000000000000000000000000000000000000000000000000000002
(数组元素个数,2)
0x0000000000000000000000000000000000000000000000000000000000000456
(第一个数组元素)
0x0000000000000000000000000000000000000000000000000000000000000789
(第二个数组元素)
最后,我们将第二个动态参数的数据部分 "Hello, world!"
进行编码:
0x000000000000000000000000000000000000000000000000000000000000000d
(元素个数,在这里是字节数:13)
0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000
( "Hello, world!"
从右边补充到 32 字节)
最后,合并到一起的编码就是(为了清晰,在函数选择器和每 32 字节之后加了换行):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
让我们使用相同的原理来对一个签名为 g(uint256[][],string[])
,参数值为
([[1, 2], [3]], ["one", "two", "three"])
的函数来进行编码;但从最原子的部分开始:
首先我们将第一个根数组 [[1, 2], [3]]
的第一个嵌入的动态数组 [1, 2]
的长度和数据进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个数组中的元素数量 2;元素本身是 1
和 2
)
0x0000000000000000000000000000000000000000000000000000000000000001
(第一个元素)
0x0000000000000000000000000000000000000000000000000000000000000002
(第二个元素)
然后我们对第一个根数组 [[1, 2], [3]]
的第二个嵌入式动态数组 [3]
的长度和数据进行编码:
0x0000000000000000000000000000000000000000000000000000000000000001
(第二个数组中的元素数量 1;元素数据是 3
)
0x0000000000000000000000000000000000000000000000000000000000000003
(第一个元素)
然后我们需要为各自的动态数组 [1, 2]
和 [3]
找到偏移量 a
和 b
。
为了计算偏移量,我们可以看一下第一个根数组的编码数据 [[1, 2], [3]]
在编码中枚举每一行。
0 - a - [1, 2] 的偏移量
1 - b - [3] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 数组的计数
3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 数组的计数
6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
偏移量 a
指向数组 [1, 2]
内容的开始位置,即第 2 行的开始(64 字节);
所以 a = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 b
指向数组 [3]
内容的开始位置,即第 5 行的开始(160 字节);
所以 b = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
然后我们对第二个根数组的嵌入字符串进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(单词 "one"
中的字符个数)
0x6f6e650000000000000000000000000000000000000000000000000000000000
(单词 "one"
的 utf8 编码)
0x0000000000000000000000000000000000000000000000000000000000000003
(单词 "two"
中的字符个数)
0x74776f0000000000000000000000000000000000000000000000000000000000
(单词 "two"
的 utf8 编码)
0x0000000000000000000000000000000000000000000000000000000000000005
(单词 "three"
中的字符个数)
0x7468726565000000000000000000000000000000000000000000000000000000
(单词 "three"
的 utf8 编码)
作为与第一个根数组的并列,因为字符串也属于动态元素,我们也需要找到它们的偏移量 c
, d
和 e
:
0 - c - "one" 的偏移量
1 - d - "two" 的偏移量
2 - e - "three" 的偏移量
3 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
8 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码
偏移量 c
指向字符串 "one"
内容的开始位置,即第 3 行的开始(96 字节);
所以 c = 0x0000000000000000000000000000000000000000000000000000000000000060
。
偏移量 d
指向字符串 "two"
内容的开始位置,即第 5 行的开始(160 字节);
所以 d = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
偏移量 e
指向字符串 "three"
内容的开始位置,即第 7 行的开始(224 字节);
所以 e = 0x00000000000000000000000000000000000000000000000000000000000000e0
。
注意,根数组的嵌入元素的编码并不互相依赖,且具有对于函数签名 g(string[],uint256[][])
所相同的编码。
然后我们对第一个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个根数组的元素数量 2;这些元素本身是 [1, 2]
和 [3]
)
而后我们对第二个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(第二个根数组的元素数量 3;这些字符串本身是 "one"
, "two"
和 "three"
)
最后,我们找到根动态数组元素 [[1, 2], [3]]
和 ["one", "two", "three"]
的偏移量 f
和 g
。
汇编数据的正确顺序如下:
0x2289b18c - 函数签名
0 - f - [[1, 2], [3]] 的偏移量
1 - g - ["one", "two", "three"] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]] 的元素计数
3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2] 的偏移量
4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3] 的偏移量
5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的元素计数
6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的元素计数
9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
10 - 0000000000000000000000000000000000000000000000000000000000000003 - ["one", "two", "three"] 的元素计数
11 - 0000000000000000000000000000000000000000000000000000000000000060 - 的偏移量"one"
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - 的偏移量"two"
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - 的偏移量"three"
14 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
16 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
18 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
19 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码
偏移量 f
指向数组 [[1, 2], [3]]
内容的开始位置,即第 2 行的开始(64 字节);
所以 f = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 g
指向数组 ["one", "two", "three"]
内容的开始位置,即第 10 行的开始(320 字节);
所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140
。
事件是Ethereum日志/事件观察协议的一个抽象。日志条目提供了合约的地址,
一系列最多四个主题和一些任意长度的二进制数据。
事件利用现有的函数ABI,以便将其(连同接口规范)解释为一个正确的类型化结构。
给定一个事件名称和一系列的事件参数,我们把它们分成两个子系列:那些有索引的和那些没有索引的。
那些被索引的参数,可能多达3个(对于非匿名事件)或4个(对于匿名事件),
与事件签名的Keccak散列一起使用,形成日志条目的主题。
那些没有索引的则构成事件的字节数组。
实际上,使用该ABI的日志条目被描述为:
address
: 合约的地址(由以太坊真正提供);
topics[0]
: keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")
canonical_type_of
是一个可以返回给定参数的权威类型的函数,例如,对 uint indexed foo
它会返回 uint256
)。
如果事件被声明为 anonymous
,那么 topics[0]
不会被生成;
topics[n]
: 如果事件没有被声明为 anonymous
, 则为 abi_encode(EVENT_INDEXED_ARGS[n - 1])
或者如果它被声明为该类型,则为 abi_encode(EVENT_INDEXED_ARGS[n])
( EVENT_INDEXED_ARGS
是被索引的 EVENT_ARGS
的系列);
data
: EVENT_NON_INDEXED_ARGS
的ABI编码
( EVENT_NON_INDEXED_ARGS
是一系列没有索引的 EVENT_ARGS
, abi_encode
是ABI编码函数,
用于从一个函数返回一系列类型的值,如上所述)。
对于所有长度不超过32字节的类型, EVENT_INDEXED_ARGS
数组直接包含数值,填充或符号扩展(对于有符号整数)到32字节,
就像常规ABI编码一样。然而,对于所有 “复杂” 类型或动态长度的类型,包括所有数组, string
, bytes
和结构,
EVENT_INDEXED_ARGS
将包含 Keccak散列 的特殊就地编码值(见 索引事件参数的编码),
而不是直接编码的值。这允许应用程序有效地查询动态长度类型的值(通过设置编码值的哈希值作为主题),
但使应用程序无法解码他们没有查询到的索引值。对于动态长度类型,
应用程序开发人员面临着对预定值的快速搜索(如果参数有索引)和任意值的可读性之间的权衡(这要求参数不被索引)。
开发者可以通过定义具有两个参数的事件 -- 一个是索引的,一个是不索引的 -- 来克服这种权衡,实现高效搜索和任意可读性。
在合约内部发生故障的情况下,合约可以使用一个特殊的操作码来中止执行,并恢复所有的状态变化。
除了这些效果之外,描述性数据可以返回给调用者。
这种描述性数据是对一个错误及其参数的编码,其方式与函数调用的数据相同。
作为一个例子,让我们考虑以下合约,它的 transfer
函数总是以 "余额不足" 的自定义错误返回。
open in Remix
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract TestToken {
error InsufficientBalance(uint256 available, uint256 required);
function transfer(address /*to*/, uint amount) public pure {
revert InsufficientBalance(0, amount);
返回数据的编码方式与函数 InsufficientBalance(0, amount)
对函数 InsufficientBalance(uint256,uint256)
的调用方式相同。
即 0xcf479181
, uint256(0)
, uint256(amount)
。
错误选择器 0x00000000
和 0xffffffff
是保留给将来使用的。
永远不要相信错误数据。
默认情况下,错误数据通过外部调用在链向上冒泡产生,
这意味着一个合约可能会收到一个它直接调用的任何合约中没有定义的错误。
此外,任何合约都可以通过返回与错误签名相匹配的数据来伪造任何错误,即使该错误没有在任何地方定义。
JSON
合约接口的JSON格式是由一个函数,事件和错误描述的数组给出的。
一个函数描述是一个带有字段的JSON对象:
type
: "function"
, "constructor"
, "receive"
( "接收以太币" 函数 ) 或者 "fallback"
( "默认" 函数);
name
: 函数名称;
inputs
: 数组对象,每个数组对象会包含:
name
: 参数名称;
type
: 参数的权威类型(详见下文)
components
: 供元组(tuple) 类型使用(详见下文)
outputs
: 一个类似于 inputs
的数组对象。
stateMutability
: 为下列值之一: pure
(指定为不读取区块链状态),
view
(指定为不修改区块链状态),
nonpayable
(函数不接受以太币 - 默认选项) 和 payable
(函数可接收以太币)。
构造函数(constructor), receive 函数 和 fallback 函数没有 name
或 outputs
属性。
receive 函数 和 fallback 函数也没有 inputs
属性。
向不接收以太币函数发送非零的以太币将使交易回滚。
在Solidity中,状态可变性 不可支付
是完全不指定状态可变性时的修饰语。
一个事件描述是一个有极其相似字段的 JSON 对象:
type
: 总是 "event"
name
: 事件名称;
inputs
: 对象数组,每个数组对象会包含:
name
: 参数名称。
type
: 参数的规范类型(详见下文)。
components
: 供元组(tuple) 类型使用(详见下文)
indexed
: 如果该字段是日志主题的一部分,则为 true
,如果它是日志数据段之一,则为 false
。
anonymous
: 如果事件被声明为 anonymous
,则为 true
。
错误消息如下:
type
: 总是 "error"
name
: 错误名称;
inputs
: 对象数组,每个数组对象会包含:
name
: 参数名称。
type
: 参数的权威类型(相见下文)。
components
: 供元组(tuple) 类型使用(详见下文)。
在 JSON 数组中可能有多个具有相同名称的错误,甚至具有相同的签名;
例如,如果错误源自合约中的不同文件或从另一个合约引用。
对于ABI来说,只有错误本身的名称是相关的,而不是它的定义位置。
open in Remix
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Test {
constructor() { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
error InsufficientBalance(uint256 available, uint256 required);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
可由如下 JSON 来表示:
"type":"error",
"inputs": [{"name":"available","type":"uint256"},{"name":"required","type":"uint256"}],
"name":"InsufficientBalance"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
处理元组类型
尽管名称被有意地不作为 ABI 编码的一部分,但将它们包含进 JSON 来显示给最终用户是非常合理的。
其结构会按下列方式进行嵌套:
一个拥有 name
, type
和潜在的 components
成员的对象描述了某种类型的变量。
直至到达一个元组(tuple) 类型且到那点的存储在 type
属性中的字符串以 tuple
为前缀,
也就是说,在 tuple
之后紧跟一个 []
或有整数 k
的 [k]
,才
能确定一个元组。 元组的组件元素会被存储在成员 components
中,
它是一个数组类型,且与顶级对象具有同样的结构,只是在这里不允许 已索引的(indexed)
数组元素。
示例代码:
open in Remix
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5 <0.9.0;
pragma abicoder v2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S memory, T memory, uint) public pure {}
function g() public pure returns (S memory, T memory, uint) {}
可由如下 JSON 来表示:
"name": "f",
"type": "function",
"inputs": [
"name": "s",
"type": "tuple",
"components": [
"name": "a",
"type": "uint256"
"name": "b",
"type": "uint256[]"
"name": "c",
"type": "tuple[]",
"components": [
"name": "x",
"type": "uint256"
"name": "y",
"type": "uint256"
"name": "t",
"type": "tuple",
"components": [
"name": "x",
"type": "uint256"
"name": "y",
"type": "uint256"
"name": "a",
"type": "uint256"
"outputs": []
通常,ABI 解码器是通过遵循偏移指针以简单的方式编写的,
但有些解码器可能会强制执行严格模式。
Solidity ABI 解码器目前并不强制执行严格模式,但编码器总是以严格模式创建数据。
非标准打包模式
通过 abi.encodePacked()
,Solidity支持一种非标准的打包模式,其中:
短于32字节的类型直接连接,没有填充或符号扩展。
动态类型是直接编码的,没有长度。
数组元素被填充,但仍被是直接编码
此外,不支持结构以及嵌套数组。
例如,对 int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!")
进行编码将生成如下结果
0xffff42000348656c6c6f2c20776f726c6421
^^^^ int16(-1)
^^ bytes1(0x42)
^^^^ uint16(0x03)
^^^^^^^^^^^^^^^^^^^^^^^^^^ 字符串("Hello, world!") 没有长度字段
更具体地说:
在编码过程中,所有东西都是直接编码的。这意味着没有像ABI编码那样区分头和尾,也没有对数组的长度进行编码。
abi.encodePacked
的直接参数被编码,
只要不是数组(或 string
或 bytes
),就不需要填充。
一个数组的编码是其元素的编码 与 填充的连接。
动态大小的类型,如 string
, bytes
或 uint[]
,在编码时没有长度字段。
string
或 bytes
的编码不会在末尾应用填充,
除非它是数组或结构体的一部分(然后它被填充为32字节的倍数)。
一般来说,只要有两个动态大小的元素,编码就会模糊不清,因为缺少长度字段。
如果需要填充,可以使用明确的类型转换: abi.encodePacked(uint16(0x12)) == hex"0012"
。
由于在调用函数时不使用打包编码,所以没有特别支持预留函数选择器。
由于编码是模糊的,所以没有解码功能。
如果使用 keccak256(abi.encodePacked(a,b))
并且 a
和 b
都是动态类型,
那么通过将 a
的部分移动到 b
中,很容易在哈希值中产生冲突,反之亦然。
更具体地说, abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")
。
如果你使用 abi.encodePacked
进行签名、认证或数据完整性,确保总是使用相同的类型,
并检查其中最多一个是动态的。除非有令人信服的理由,否则应首选 abi.encode
。
索引事件参数的编码
不属于值类型的索引事件参数,即数组和结构,不直接存储,
而是存储一个编码的 Keccak-256 哈希值。这个编码的定义如下:
bytes
和 string
值的编码只是字符串的内容,没有任何填充或长度前缀。
结构的编码是其成员编码的串联,总是填充为32字节的倍数(甚至是 bytes
和 string
)。
数组的编码(包括动态和静态大小)是其元素编码的连接,
总是填充为32字节的倍数(甚至是 bytes
和 string
),没有任何长度前缀。
在上面,像往常一样,一个负数被填充符号扩展,而不是零填充。
bytesNN
类型被填充在右边,而 uintNN
/ intNN
被填充在左边。
如果一个结构包含一个以上的动态大小的数组,那么它的编码是不明确的。
正因为如此,要经常重新检查事件数据,不要只依赖基于索引参数的搜索结果。