Solidity 源文件的布局
源文件可以包含任意数量的合约定义、import、pragma 和 using for 指令和结构、枚举、函数、错误和常量变量定义。
SPDX 许可证标识符
如果智能合约的源代码可用,则可以更好地建立对智能合约的信任。由于提供源代码总是涉及版权方面的法律问题,Solidity 编译器鼓励使用机器可读的 SPDX 许可证标识符。
每个源文件都应以说明其许可证的注释开头:
// SPDX-License-Identifier: MIT
编译器不会验证许可证是否属于 SPDX 允许的列表的一部分,但它会在字节码元数据中包含提供的字符串。
如果您不想指定许可证或源代码不是开源的,请使用特殊值 UNLICENSED。请注意,UNLICENSED(不允许使用,不在 SPDX 许可证列表中)与 UNLICENSE(将所有权利授予所有人)不同。 Solidity 遵循 npm 的建议。
当然,提供此评论并不能免除您与许可相关的其他义务,例如必须在每个源文件或原始版权所有者中提及特定的许可标题。
编译器在文件级别的文件中的任何位置都可以识别注释,但建议将其放在文件的顶部。
有关如何使用 SPDX 许可证标识符的更多信息,请访问 SPDX 网站。
Pragmas
pragma 关键字用于启用某些编译器功能或检查。
pragma 指令始终位于源文件的本地,因此如果要在整个项目中启用它,则必须将 pragma 添加到所有文件中。
如果您导入另一个文件,该文件中的编译指示不会自动应用于导入文件。
版本编译指示
源文件可以(并且应该)使用版本编译指示进行注释,以拒绝使用可能引入不兼容更改的未来编译器版本进行编译。
我们试图将它们保持在绝对最低限度,并以语义变化也需要语法变化的方式引入它们,但这并不总是可能的。
因此,至少对于包含重大更改的版本,通读更改日志总是一个好主意。这些版本始终具有 0.x.0 或 x.0.0 形式的版本。
版本pragma使用如下:pragma solidity ^0.5.2;
带有上述行的源文件不能使用 0.5.2 版本之前的编译器编译,并且它也不能在 0.6.0 版本开始的编译器上工作(这第二个条件是使用 ^ 添加的)。
因为在 0.6.0 版之前不会有重大更改,因此您可以确保您的代码按照您的预期方式进行编译。编译器的确切版本不固定,因此仍然可以发布错误修复版本。
可以为编译器版本指定更复杂的规则,这些规则与 npm 使用的语法相同。
- NOTE
使用版本编译指示不会更改编译器的版本。
它也不会启用或禁用编译器的功能。
它只是指示编译器检查其版本是否与 pragma 所需的版本匹配。
如果不匹配,编译器会发出错误。
ABI 编码器编译指示
通过使用 pragma abicoder v1 或 pragma abicoder v2,您可以在 ABI 编码器和解码器的两种实现之间进行选择。
新的 ABI 编码器 (v2) 能够编码和解码任意嵌套的数组和结构。它可能会产生不太理想的代码,并且没有像旧编码器那样接受过多的测试,但从 Solidity 0.6.0 开始被认为是非实验性的。
您仍然必须使用 pragma abicoder v2; 显式激活它。由于它将从 Solidity 0.8.0 开始默认激活,因此可以选择使用 pragma abicoder v1; 选择旧编码器。
新编码器支持的类型集是旧编码器支持的类型的严格超集。使用它的合约可以与没有限制的合约进行交互。
仅当非 abicoder v2 合约不尝试进行需要仅由新编码器支持的解码类型的调用时,反向才是可能的。编译器可以检测到这一点并将发出错误。
只需为您的合约启用 abicoder v2 就足以消除错误。
- NOTE
此编译指示适用于激活它的文件中定义的所有代码,无论该代码最终在何处结束。
这意味着选择了源文件以使用 ABI coder v1 编译的合约仍然可以包含通过从另一个合约继承来使用新编码器的代码。
如果新类型仅在内部使用而不在外部函数签名中使用,则允许这样做。
- NOTE
在 Solidity 0.7.4 之前,可以使用 pragma Experimental ABIEncoderV2 选择 ABI coder v2,但无法显式选择 coder v1,因为它是默认设置。
Experimental Pragma
第二个 pragma 是实验性 pragma。它可用于启用默认情况下尚未启用的编译器或语言的功能。当前支持以下实验性编译指示:
ABIEncoderV2
因为 ABI coder v2 不再被认为是实验性的,它可以通过 pragma abicoder v2(请参见上文)从 Solidity 0.7.4 开始选择。
SMTChecker
在构建 Solidity 编译器时必须启用此组件,因此它并非在所有 Solidity 二进制文件中都可用。构建说明解释了如何激活此选项。
在大多数版本中,它为 Ubuntu PPA 版本激活,但不适用于 Docker 映像、Windows 二进制文件或静态构建的 Linux 二进制文件。
如果您在本地安装了 SMT 求解器并通过节点(而不是通过浏览器)运行 solc-js,则可以通过 smtCallback 为 solc-js 激活它。
如果您使用 pragma experimental SMTChecker;,那么您会收到额外的安全警告,这些警告是通过查询 SMT 求解器获得的。
该组件尚不支持 Solidity 语言的所有功能,并且可能会输出许多警告。如果它报告不支持的功能,则分析可能不完全正确。
导入其他源文件
语法和语义
Solidity 支持导入语句,以帮助模块化您的代码,这些代码类似于 JavaScript(从 ES6 开始)中可用的代码。
但是,Solidity 不支持默认导出的概念。
在全局级别,您可以使用以下形式的导入语句:
import "filename";
文件名部分称为导入路径。 该语句将所有全局符号从“文件名”(以及从那里导入的符号)导入当前全局范围(与 ES6 不同,但向后兼容 Solidity)。
不推荐使用这种形式,因为它会意外地污染命名空间。 如果您在“文件名”中添加新的顶级项目,它们会自动出现在所有从“文件名”导入的文件中。
最好显式地导入特定符号。
下面的例子创建了一个新的全局符号 symbolName,它的成员是来自“filename”的所有全局符号:
import * as symbolName from "filename";
这导致所有全局符号都以 symbolName.symbol 格式可用。
这种语法的一个变体不是 ES6 的一部分,但可能有用的是:
import "filename" as symbolName;
这相当于 import * as symbolName from "filename";
。
如果存在命名冲突,您可以在导入时重命名符号。
例如,下面的代码创建了新的全局符号 alias 和 symbol2,它们分别从“文件名”内部引用 symbol1 和 symbol2。
import {symbol1 as alias, symbol2} from "filename";
导入路径
为了能够在所有平台上支持可重现的构建,Solidity 编译器必须抽象出存储源文件的文件系统的细节。
由于这个原因,导入路径不直接引用主机文件系统中的文件。
相反,编译器维护一个内部数据库(虚拟文件系统或简称 VFS),其中为每个源单元分配一个唯一的源单元名称,该名称是一个不透明且非结构化的标识符。导入语句中指定的导入路径被翻译成源单元名称,用于在该数据库中查找对应的源单元。
使用标准 JSON API,可以直接提供所有源文件的名称和内容作为编译器输入的一部分。在这种情况下,源单元名称确实是任意的。
但是,如果您希望编译器自动查找源代码并将其加载到 VFS 中,则您的源单元名称的结构需要使导入回调能够找到它们。
使用命令行编译器时,默认导入回调仅支持从主机文件系统加载源代码,这意味着您的源单元名称必须是路径。一些环境提供更通用的自定义回调。
例如,Remix IDE 提供了一个允许您从 HTTP、IPFS 和 Swarm URL 导入文件或直接引用 NPM 注册表中的包的工具。
有关虚拟文件系统和编译器使用的路径解析逻辑的完整描述,请参见路径解析。
Comments
可以使用单行注释 (//
) 和多行注释 (/*...*/
)。
// This is a single-line comment.
/*
This is a
multi-line comment.
*/
- NOTE
单行注释由 UTF-8 编码中的任何 unicode 行终止符(LF、VF、FF、CR、NEL、LS 或 PS)终止。
结束符在注释之后仍然是源代码的一部分,所以如果它不是ASCII符号(这些是NEL、LS和PS),就会导致解析器错误。
此外,还有另一种类型的注释,称为 NatSpec 注释,在样式指南中有详细说明。
它们用三斜杠 (///
) 或双星号块 (/** ... */
) 编写,应直接在函数声明或语句上方使用。
参考资料
https://docs.soliditylang.org/en/latest/layout-of-source-files.html