一、Solidity 源文件结构
源文件可以包含任意数量的合约定义、import、pragma和using for指令以及struct、enum、function、error和constant variable定义。
1.1 SPDX许可证标识符
如果智能合约的源代码可用,就可以更好地建立对智能合约的信任。由于提供源代码总是涉及到版权方面的法律问题,所以Solidity编译器鼓励使用机器可读的SPDX许可标识符。每个源文件都应该以说明其许可的注释开始:
// SPDX-License-Identifier: MIT
编译器不验证许可证是否是SPDX允许的列表的一部分,但是它在字节码元数据中包含了所提供的字符串。
如果您不想指定许可证,或者源代码不是开源的,请使用特殊值UNLICENSED
。注意UNLICENSED
(不允许使用,不在SPDX许可列表中)不同于UNLICENSE
(授予所有人所有权利)。稳定性遵循npm的建议。
当然,提供此注释并不能免除您与许可相关的其他义务,例如必须在每个源文件中提到特定的许可头或原始版权所有者。
注释可以被编译器在文件级别的任何地方识别,但建议将其放在文件的顶部。
有关如何使用SPDX许可证标识符的更多信息,请参见SPDX网站。
1.2 Pragmas
pragma
关键字用于启用某些编译器特性或检查。pragma
指令始终是源文件的本地指令,因此如果您想在整个项目中启用它,则必须将pragma
添加到所有文件中。如果导入另一个文件,来自该文件的pragma
指令不会自动应用于导入文件。
Version Pragma
源文件可以(也应该)使用版本pragma进行注释,以拒绝使用可能引入不兼容更改的未来编译器版本进行编译。我们试图将它们保持在绝对的最小值,并以改变语义也需要改变语法的方式引入它们,但这并不总是可能的。因此,通读更新日志总是一个好主意,至少对于包含突破性更改的发行版是这样。这些版本总是有格式0.x.0
或x.0.0
。
version pragma
的用法如下:
pragma solidity ^0.5.2;
带有上面这一行的源文件不能在版本0.5.2之前的编译器上编译,而且它也不能在版本0.6.0之后的编译器上工作(使用^
添加了第二个条件)。因为在0.6.0
版本之前不会有破坏性的更改,所以您可以确保代码按照您想要的方式编译。编译器的确切版本并不是固定的,因此仍然可以发布错误修复版本。
可以为编译器版本指定更复杂的规则,这些规则遵循npm使用的相同语法。
注意
使用version pragma不会改变编译器的版本。它也不启用或禁用编译器的特性。它只是指示编译器检查它的版本是否与pragma要求的版本匹配。如果不匹配,编译器就会发出一个错误。
1.3 导入其他源文件
语法与语义
Solidity支持import
语句来帮助模块化你的代码,类似于JavaScript中可用的代码(从ES6开始)。但是,Solidity不支持默认导出的概念。
在全局级别,可以使用如下形式的import
语句:
import "filename";
filename
部分称为导入路径(import path)。该语句将所有全局符号从" filename "(以及导入的符号)导入当前全局作用域(与ES6不同,但与Solidity向后兼容)。不建议使用这种形式,因为它会不可预知地污染名称空间。如果你在“filename”中添加了新的顶级项目,它们会自动出现在所有从“filename”导入的文件中。最好显式地导入特定的符号。
下面的例子创建了一个新的全局符号symbolName
,它的成员都是来自"filename"
的全局符号:
import * as symbolName from "filename";
这样导致所有全局符号都可使用格式symbolName.symbol
这个语法的一个变体不是ES6的一部分,但可能有用:
import "filename" as symbolName;
这相当于 import * as symbolName from "filename";
如果有命名冲突,您可以在导入时重命名符号。例如,下面的代码创建了新的全局符号alias
和symbol2
,它们分别从“filename”
中引用symbol1
和symbol2
。
import {symbol1 as alias, symbol2} from "filename";
Import Paths
为了能够在所有平台上支持可复制的构建,Solidity编译器必须抽象出存储源文件的文件系统细节。因此,导入路径不直接引用主机文件系统中的文件。相反,编译器维护一个内部数据库(虚拟文件系统或简称VFS),其中每个源单元被分配一个唯一的源单元名称(unique source unit name
),这是一个不透明的非结构化标识符。import
语句中指定的导入路径被转换为源单元名称,并用于在此数据库中查找相应的源单元。
使用标准JSON API,可以直接提供所有源文件的名称和内容,作为编译器输入的一部分。在这种情况下,源单元名称确实是任意的。但是,如果您希望编译器自动查找源代码并将其加载到VFS中,则源单元名称的结构需要使导入回调能够定位它们。当使用命令行编译器时,默认的导入回调只支持从主机文件系统加载源代码,这意味着您的源单元名称必须是路径。一些环境提供了更通用的自定义回调。例如,Remix IDE提供了一个允许您从HTTP、IPFS和Swarm url导入文件或直接引用NPM注册表中的包。
有关虚拟文件系统和编译器使用的路径解析逻辑的完整描述,请参见路径解析。
1.4 注释
可以是单行注释(//
)和多行注释(/*…*/
)。
// This is a single-line comment.
/*
This is a
multi-line comment.
*/
单行注释由UTF-8编码中的任意unicode行结束符(LF, VF, FF, CR, NEL, LS或PS)终止。结束符仍然是注释之后源代码的一部分,所以如果它不是ASCII符号(这些是NEL, LS和PS),它将导致解析器错误。
此外,还有另一种类型的注释称为NatSpec注释,在风格指南中有详细说明。它们用三斜杠(///
)或双星号块(/**…*/
),它们应该直接用于函数声明或语句之上。
二、合约结构
Solidity中的合约类似于面向对象语言中的类。每个合约可以包含状态变量、函数、函数修饰符、事件、错误、结构体类型和Enum类型的声明。此外,合约可以从其他合约继承。
还有一些特殊类型的合约,称为库和接口。
contracts部分比这部分包含更多的细节,这部分提供了一个快速的概述。
2.1 状态变量
状态变量是其值永久存储在合约存储(storage
)中的变量。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract SimpleStorage {
uint storedData; // State variable
// ...
}
查看类型部分了解有效的状态变量类型,查看可见性和getter了解可能的可见性选择。
2.2 函数
函数是代码的可执行单元。函数通常定义在合约内部,但也可以定义在合约之外。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract SimpleAuction {
function bid() public payable { // Function
// ...
}
}
// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
return x * 2;
函数调用可以在内部或外部发生,对其他合约具有不同级别的可见性。函数接受参数并返回变量以在它们之间传递参数和值。
2.3 函数修饰符
函数修饰符可用于以声明的方式修改函数的语义(请参阅合约部分中的函数修饰符)。
重载,也就是说,使用相同的修饰符名称和不同的参数,是不可能的。
与函数一样,修饰符也可以被覆盖。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract Purchase {
address public seller;
modifier onlySeller() { // Modifier
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
function abort() public view onlySeller { // Modifier usage
// ...
}
}
2.4 事件
事件是EVM日志记录工具的方便接口。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount); // Event
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // Triggering event
}
}
有关如何声明事件以及可以从dapp中使用事件的信息,请参阅合约部分中的事件。
2.5 错误
错误允许您为失败情况定义描述性名称和数据。错误可以在revert
语句中使用。与字符串描述相比,错误要便宜得多,并允许您编码额外的数据。您可以使用NatSpec向用户描述错误。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);
contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
uint balance = balances[msg.sender];
if (balance < amount)
revert NotEnoughFunds(amount, balance);
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
}
有关更多信息,请参阅合约部分中的错误和恢复语句。
2.6 结构体类型(Struct Types)
结构体是自定义的类型,可以对多个变量进行分组(请参阅类型部分中的结构体)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Ballot {
struct Voter { // Struct
uint weight;
bool voted;
address delegate;
uint vote;
}
}
2.7 枚举类型(Enum Types)
枚举可用于创建具有有限“常量值”集的自定义类型(参见类型部分中的枚举)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Purchase {
enum State { Created, Locked, Inactive } // Enum
}
三、类型
Solidity是一种静态类型的语言,这意味着需要指定每个变量(状态和局部变量)的类型。坚固性提供了几种基本类型,它们可以组合成复杂类型。
此外,类型可以在包含运算符的表达式中相互作用。有关各种操作符的快速参考,请参见操作符的优先顺序。
在Solidity中不存在“undefined
”或“null
”值的概念,但新声明的变量总是有一个默认值,这取决于它的类型。要处理任何意外的值,您应该使用revert函数来恢复整个事务,或者返回一个元组,其中包含第二个bool
值,表示成功。
3.1 值类型(Value Types)
以下类型也称为值类型,因为这些类型的变量总是按值传递,也就是说,当它们用作函数参数或赋值时,它们总是被复制。
3.1.1 布尔
bool
:可能的值是常量true
和false
。
运算符
!
(逻辑非,logical negation
)&&
(逻辑与,logical conjunction, “and”)||
(逻辑或,logical disjunction, “or”)==
(相等, equality)!=
(不相等,inequality)
运算符||
和&&
应用通用短路规则(short-circuiting rules
)。这意味着在表达式f(x) || g(y)
中,如果f(x)
的计算结果为真,则g(y)
将不会被计算,即使它可能有副作用。
3.1.2 整数
int
/ uint
:不同大小的有符号和无符号整数。关键字uint8
到uint256
的步长8
(unsigned
的8到256 bit)和int8
到int256
。uint
和int
分别是uint256
和int256
的别名。
运算符
- 比较:
<=
,<
,==
,!=
,>=
,>
(结果为bool
) - 位运算:
&
,|
,^
( 按位异或),~
(按位取反) - 移位运算:
<<
(左移),>>
(右移) - 算术运算:
+
,-
, 一元-
(仅适用于有符号整数),*
,/
,%
(取模),**
(取幂,乘方)
对于整数类型x
,可以使用type(X).min
。min和type(X).max
访问类型可表示的最小值和最大值。
Solidity 中的整数被限制在一定的范围内。例如,对于
uint32
,这是0
到2**32 - 1
。对这些类型执行算术有两种模式:“wrapping
”或“unchecked
”模式和“checked
”模式。默认情况下,算术总是被“检查”,这意味着如果操作的结果超出了类型的值范围,则通过失败的断言恢复调用。你可以使用unchecked{…}
切换到“unchecked” mode 。更多细节可以在关于unchecked的部分中找到。
3.1.3 定长浮点型
Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量, 但不能给它们赋值或把它们赋值给其他变量。
fixed
/ ufixed
: 有符号和无符号的各种大小定长浮点型。关键字ufixedMxN
和fixedMxN
,其中M
表示类型占用的位数,N
表示可用的小数点数。M
必须能被8整除,取值范围是8到256位。N
必须在0到80之间(含)。ufixed
和fixed
分别是ufixed128x18
和fixed128x18
的别名。
运算符
- 比较 :
<=
,<
,==
,!=
,>=
,>
(结果为bool
) - 算术运算:
+
,-
, 一元-
(仅适用于有符号整数),*
,/
,%
(取模),
注意:
浮点数(在许多语言中是float
和double
,更准确地说是IEEE 754数字)和定点数( fixed point numbers)之间的主要区别在于,前者用于整数和小数部分(小数点点之后的部分)的位数是灵活的,而后者是严格定义的。一般来说,在浮点数中,几乎整个空间都用来表示数字,而只有一小部分位定义小数点的位置。
3.1.4 Address
地址类型有两种类型,它们在很大程度上是相同的:
-
address
: 保存一个20字节的值(以太坊地址的大小)。 -
address payable
: 与address
相同,但增加了额外的成员transfer
和send
。
这种区别背后的思想是,address payable
是你可以发送以太币的地址,而你不应该将以太币发送到普通address
,例如,因为它可能是一个不是为接受以太币而构建的智能合约。
类型转换:
允许从address payable
到address
的隐式转换,而从address
到address payable
的转换必须通过payable(<address>)
进行显式转换。
对于uint160
、整型字面值、bytes20
和合约类型,允许显式的address
转换。
只有类型address
和contract-type
的表达式可以通过显式转换payable(…)
转换为类型address payable
。对于合约类型,只有当合约可以接收Ether
时才允许这种转换,也就是说,合约要么具有receive或支付回退函数。注意,payable(0)
是有效的,是该规则的例外。
如果你需要一个
address
类型的变量,并计划向它发送Ether,那么将它的类型声明为address payable
以使这个需求可见。另外,尽量尽早进行这种区分或转换。address
和address payable
之间的区别是在0.5.0版引入的。同样从该版本开始,合约不能隐式转换为address
类型,但仍然可以显式转换为address
或address payable
,如果它们具有接收或应付回退函数。
运算符
<=
, <
, ==
, !=
, >=
and >
如果将使用较大字节大小的类型转换为
address
,例如bytes32
,则该地址将被截断。为了减少转换的模糊性,从版本0.4.24开始,编译器将强制您在转换中显式截断。例如,32字节的值0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC。
你可以使用address(uint160(bytes20(b)))
,这将导致0x111122223333444455556666777788889999aAaa,或者你可以使用address(uint160(uint256(b))
,这将导致0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc。
符合EIP-55的混合大小写十六进制数将自动被视为address
类型的文字。参见地址字面值。
地址成员
有关地址的所有成员的快速引用,请参见地址类型的成员。
-
balance
andtransfer
可以使用balance
查询地址余额,并使用transfer
函数将Ether(以wei为单位)发送到应付地址:
address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
如果当前合约余额不足够,或者以太币转账被接收账户拒绝,则transfer
函数失败。transfer
函数在失败时恢复。
3.1.5 Contract Types
每个合约都定义了自己的类型。您可以隐式地将合约转换为它们继承的合约。合约可以显式地转换为address
类型或从address
类型转换为合约。
只有当合约类型具有接收或应付回退函数时,才可能显式地转换到地址address payable
。转换仍然使用address(x)
执行。如果契约类型没有接收或应付回退函数,则可以使用payable(address(x))
来完成到address payable
的转换。您可以在有关地址类型的部分中找到更多信息。
如果你声明了一个合约类型的局部变量(MyContract c
),你可以调用该合约上的函数。请注意从相同的合约类型中分配它。
您还可以实例化合约(这意味着它们是新创建的)。你可以在Contracts via new部分找到更多细节。
合约的数据表示与address
类型的数据表示相同,ABI中也可使用这种类型。
合约不支持任何运算符。
合约类型的成员是合约的外部函数,包括任何标记为public
的状态变量。
对于合约C
,您可以使用type(C)
访问关于合约的类型信息。
3.1.6 长字节数组
值类型bytes1
, bytes2,
bytes3
,…,bytes32
包含一个从1到32的字节序列。
运算符
- 比较:
<=
,<
,==
,!=
,>=
,>
(结果为bool
) - 位运算:
&
,|
,^
( 按位异或),~
(按位取反) - 移位运算:
<<
(左移),>>
(右移) - 索引访问:如果
x
是bytesI
类型,则x[k]
for0 <= k < I
返回第k
个字节(只读)。
移位操作符使用无符号整数类型作为右操作数(但返回左操作数的类型),它表示要移位的位数。移位有符号整数类型将产生编译错误。
成员
.length
产生字节数组的固定长度(只读)。
类型bytes1[]
是一个字节数组,但由于填充规则,它为每个元素浪费了31个字节的空间(存储除外)。最好使用bytes
类型。
在0.8.0版本之前,byte是bytes1的别名。
3.1.7 变长字节数组
bytes
:
动态大小的字节数组,请参见数组。不是一个值类型!string
:
动态大小的utf -8编码字符串,请参阅数组。不是一个值类型!
3.1.8 地址常量
通过地址校验和测试的十六进制文字,例如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
属于地址类型。长度在39到41位之间且未通过校验和测试的十六进制字面值将产生错误。可以在前面(对于整数类型)或在后面(对于bytesNN
类型)添加零来删除错误。
混合大小写地址校验和格式在EIP-55中定义。
3.1.9 有理数和整型常量
整数字面量由0-9
范围内的数字序列组成。它们被解释为十进制。例如,69
表示69。Solidity 中不存在八进制字面值,前导零无效。
小数字面值由.
构成。小数点后至少有一个数字。例如.1
和1.3
(但不是1.
)。
还支持2e10
形式的科学计数法,其中尾数可以是小数,但指数必须是整数。字面上的MeE
相当于M * 10**E
。例如2e10
, -2e10
, 2e-10
, 2.5e1
。
下划线可用于分隔数字字面值的数字,以帮助可读性。例如,十进制123_000
、十六进制0x2eff_abde
、科学十进制1_2e345_678
都是有效的。下划线只允许在两位数之间,并且只允许一个连续的下划线。包含下划线的数字文字没有添加额外的语义含义,下划线将被忽略。
数字字面值表达式保持任意精度,直到它们被转换为非字面值类型(即,通过将它们与数字字面值表达式以外的任何东西(如布尔文字)一起使用,或通过显式转换)。这意味着计算不会溢出,除法在数字字面值表达式中不会截断。
例如,(2**800 + 1)- 2**800
的结果是常数1(类型uint8
),尽管中间结果甚至不适合机器字大小。此外,.5 * 8
的结果是整数4(尽管中间使用了非整数)。
3.1.10 字符串字面量和类型
字符串字面值可以用双引号或单引号("foo"
或'bar'
)来写,它们也可以被分成多个连续的部分("foo" "bar"
相当于"foobar"
),这在处理长字符串时很有帮助。它们并不像在C中那样意味着后面跟着零;"foo"
表示三个字节,而不是四个。与整型字面值一样,它们的类型可以变化,但它们可以隐式转换为bytes1
、…、bytes32
,(如果合适的话)也可以转成bytes
和string
。
例如,使用bytes32 samevar = "stringliteral"
,当指定给bytes32
类型时,字符串字面值将以原始字节形式解释。
字符串字面量只能包含可打印的ASCII字符,这意味着包括0x20 .. 0x7E
此外,字符串字面量还支持以下转义字符:\<newline>
转义一个实际的换行符\\
反斜杠\'
single quote\"
double quote\n
newline\r
回车\t
tab\xNN
(十六进制转义,见下文)\uNNNN
(unicode转义,见下文)
\xNN
接收一个十六进制值并插入相应的字节,而 \uNNNN
接收一个Unicode编码点并插入一个UTF-8序列。
3.1.11 Unicode文本
常规字符串字面量只能包含ASCII,而Unicode字面量(前缀为关键字 unicode
)可以包含任何有效的UTF-8序列。它们还支持与常规字符串字面量相同的转义序列。
string memory a = unicode"Hello 😃";
3.1.12 十六进制常量
十六进制文字以关键字hex
作为前缀,并用双引号或单引号括起来(hex"001122FF"
, hex'0011_22_FF'
)。它们的内容必须是十六进制数字,可以选择在字节边界之间使用单个下划线作为分隔符。字面量的值将是十六进制序列的二进制表示形式。
由空格分隔的多个十六进制字面值被串联成一个字面值:hex"00112233" hex"44556677"
等价于hex"0011223344556677"
十六进制字面值的行为与字符串字面值相似,并且具有相同的转换限制。
3.1.13 枚举
枚举是在Solidity中创建用户定义类型的一种方法。它们可以显式地转换为所有整数类型,但不允许隐式转换。从整数的显式转换在运行时检查值是否位于枚举范围内,否则将导致Panic错误。枚举需要至少一个成员,声明时它的默认值是第一个成员。枚举的成员不能超过256个。
数据表示方式与C中的枚举相同:选项由从0开始的后续无符号整数值表示。
使用type(NameOfEnum).min
和type(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;
}
}
枚举也可以在文件级别上声明,在合约或库定义之外。
3.1.14 用户定义的值类型
用户定义的值类型允许在基本值类型上创建零成本抽象。这类似于别名,但具有更严格的类型要求。
用户定义的值类型是使用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.wrap
和FixedMath.toUFixed256x18
具有相同的签名,但执行两个非常不同的操作:UFixed256x18.wrap
函数返回一个UFixed256x18
,它具有与输入相同的数据表示,而toUFixed256x18
返回一个UFixed256x18
,它具有相同的数值。
3.1.15 函数类型
函数类型是函数的类型。函数类型的变量可以从函数赋值,函数类型的函数参数可以用于向函数调用传递函数和从函数调用返回函数。函数类型有两种:内部函数和外部函数:
内部函数只能在当前合约内部(更具体地说,是在当前代码单元内部,其中还包括内部库函数和继承函数)调用,因为它们不能在当前合约的上下文中的外部执行。调用内部函数是通过跳转到它的入口标签来实现的,就像在内部调用当前合约的函数一样。
外部函数由地址和函数签名组成,它们可以通过外部函数调用传递和返回。
函数类型表示如下:
function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
与形参类型相反,返回类型不能为空——如果函数类型不应该返回任何内容,则必须省略整个returns (<return types>)
部分。
默认情况下,函数类型是内部的,因此internal
关键字可以省略。注意,这只适用于函数类型。合约中定义的函数必须显式指定可见性,它们没有默认值。
转换
函数类型A
隐式转换为函数类型B
当且仅当它们的参数类型相同,它们的返回类型相同,它们的内部/外部属性相同,并且A
的状态可变性比B
的状态可变性更具限制性。特别是:
-
pure
函数可以转换为view
函数和non-payable
函数 -
view
函数可以转换为non-payable
函数 -
payable
函数可以转换为non-payable
函数
函数类型之间不可能有其他转换。
关于payable
和non-payable
的规则可能有点令人困惑,但本质上,如果一个函数是payable
的,这意味着它也接受零以太币的支付,所以它也是non-payable
的。另一方面,non-payable
函数将拒绝发送给它的Ether,因此non-payable
函数不能转换为payable
函数。
如果一个函数类型变量没有初始化,调用它会导致Panic错误。如果在函数上使用delete
之后调用它,也会发生同样的情况。
如果在Solidity上下文之外使用外部函数类型,则将它们视为function
类型,该函数类型将地址和函数标识符一起编码为单个bytes24
类型。
请注意,当前合约的公共函数既可以用作内部函数,也可以用作外部函数。把f
作为一个内部函数,只要用f
,如果你想用它的外部形式,用this.f
。
内部函数类型的函数可以赋值给内部函数类型的变量,而不管它定义在哪里。这包括合约和库的private、internal 和 public函数,以及 free functions。另一方面,外部函数类型只与 public 和external合约函数兼容。库被排除在外,因为它们需要delegatecall
,并为它们的选择器使用不同的ABI约定。在接口中声明的函数没有定义,因此指向它们也没有意义。
成员
外部(或公共)函数有以下成员:.address
返回函数的合约的地址。
.selector
返回ABI函数选择器
演示如何使用成员的示例:
// 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;
}
}
3.2 引用类型
引用类型的值可以通过多个不同的名称进行修改。这与值类型形成对比,在值类型中,只要使用值类型的变量,就可以获得一个独立的副本。因此,处理引用类型必须比处理值类型更加谨慎。目前,引用类型包括结构体、数组和映射。如果使用引用类型,总是必须显式地提供存储该类型的数据区域:memory
(其生存期仅限于外部函数调用)、storage
(存储状态变量的位置,其生存期仅限于合约的生存期)或calldata
(包含函数参数的特殊数据位置)。
更改数据位置的赋值或类型转换总是会引起自动复制操作,而同一数据位置内的赋值在某些情况下只复制存储类型。
3.2.1 数据位置
每个引用类型都有一个额外的注释,即“数据位置”,关于它的存储位置。有三个数据位置:memory
, storage
和calldata
。calldata
是一个不可修改的、非持久的区域,函数参数存储在其中,其行为与内存非常相似。
如果可以,尝试使用
calldata
作为数据位置,因为它将避免复制,并确保数据不能被修改。具有calldata
数据位置的数组和结构体也可以从函数返回,但不可能分配这样的类型。
在0.6.9版本之前,引用类型参数的数据位置仅限于外部函数中的calldata,公共函数中的
memory
以及内部和私有函数中的memory
或storage
。现在所有函数都允许使用memory
和calldata
,不管它们的可见性如何。
在0.5.0版本之前,数据位置可以省略,并且根据变量的种类、函数类型等默认为不同的位置,但现在所有复杂类型都必须给出显式的数据位置。
数据位置和赋值行为
数据位置不仅与数据的持久性相关,还与赋值的语义相关:
-
在
storage
和memory
之间的赋值(或从 calldata 中赋值) 总是创建一个独立的拷贝。 -
从
memory
到memory
的赋值只创建引用。 这意味着对一个内存变量的改变在所有其他引用相同数据的内存变量中也是可见的。 -
从
storage
到local
存储变量的赋值也只赋值一个引用。 -
所有其他对
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;
// Similarly, "delete y" is not valid, as assignments to local variables
// referencing storage objects can only be made from existing storage objects.
// It would "reset" the pointer, but there is no sensible location it could point to.
// For more details see the documentation of the "delete" operator.
// 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 {}
}
3.2.2 数组
数组可以具有编译时的固定大小,也可以具有动态大小。
固定大小为k
且元素类型为T
的数组类型记为T[k]
,动态大小为T[]
。
例如,由5个uint
动态数组组成的数组被写入uint[][5]。与其他一些语言相比,这种符号是相反的。在Solidity中,X[3]
总是一个包含三个X
类型元素的数组,即使X
本身就是一个数组。在C等其他语言中就不是这样了。
索引是从零开始的,访问与声明的方向相反。
例如,如果你有一个变量uint[][5] memory x
,你使用x[2][6]
访问第三个动态数组中的第七个uint
,并且要访问第三个动态数组,使用x[2]
。同样,如果你有一个T
类型数组T[5] a
,T
也可以是一个数组,那么a[2]
总是有类型T
。
数组元素可以是任何类型,包括映射或结构。适用于类型的一般限制,即映射只能存储在storage
数据位置中,而公共可见函数需要ABI类型的参数。
可以将状态变量数组标记为public
,并让Solidity创建一个getter
。数值索引成为getter的必需参数。
访问一个数组超过它的结束将导致断言失败。方法.push()
和.push(value)
可用于在数组的末尾追加一个新元素,其中.push()
追加一个零初始化的元素并返回对该元素的引用。
bytes
和string
作为数组
bytes
和string
类型的变量是特殊的数组。bytes
类型类似于bytes1[]
,但它紧密地封装在calldata和memory中。string
等于bytes
,但不允许长度或索引访问。
Solidity没有字符串操作函数,但是有第三方字符串库。您还可以使用keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
比较两个字符串的keccak256-hash,并使用string.concat(s1, s2)
连接两个字符串。
您应该使用bytes
而不是bytes1[]
,因为它更便宜,因为在memory
中使用bytes1[]
会在元素之间增加31个填充字节。请注意,在storage
中,由于紧凑的包装,填充是不存在的,请参阅字节和字符串。作为一般规则,使用bytes
表示任意长度的原始字节数据,使用string
表示任意长度的字符串(UTF-8)数据。如果可以将长度限制在某个字节数,请始终使用bytes1
到bytes32
的值类型之一,因为它们要便宜得多。
如果要访问字符串
s
的字节表示形式,请使用bytes(s).length
/bytes(s)[7] = 'x';
请记住,您正在访问UTF-8表示的底层字节,而不是单个字符。
函数 bytes.concat
和 string.concat
可以使用string.concat
连接任意数量的字符串值。该函数返回一个string memory
数组,其中包含不填充的参数内容。如果希望使用其他类型的形参,而这些形参不能隐式转换为string
,则需要首先将它们转换为string
。
类似地,bytes.concat
函数可以连接任意数量的bytes
或字节bytes1 ... bytes32
值。该函数返回一个单个bytes memory
数组,其中包含参数的内容而不填充。如果您想使用字符串参数或其他不能隐式转换为bytes
的类型,则需要首先将它们转换为bytes
或bytes1
/…/bytes32
。文章来源:https://www.toymoban.com/news/detail-741753.html
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
contract C {
string s = "Storage";
function f(bytes calldata bc, string memory sm, bytes16 b) public view {
string memory concatString = string.concat(s, string(bc), "Literal", sm);
assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);
bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);
}
}
如果你调用string.concat
或bytes.concat
,不带参数,将返回一个空数组。文章来源地址https://www.toymoban.com/news/detail-741753.html
到了这里,关于Solidity 基础(二)语言描述-类型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!