样式指南
简介
本指南旨在为编写 Solidity 代码提供编码约定。本指南应被视为一个不断演变的文档,随着时间的推移会发生变化,因为会发现有用的约定,并且旧的约定会变得过时。
许多项目会实施自己的样式指南。如果有冲突,项目特定的样式指南优先。
本样式指南的结构和许多建议来自 Python 的 pep8 样式指南。
本指南的目标 *不是* 要成为编写 Solidity 代码的正确方法或最佳方法。本指南的目标是 *一致性*。Python 的 pep8 中的一段引用很好地概括了这个概念。
注意
样式指南是关于一致性的。与本样式指南保持一致非常重要。在一个项目中保持一致性更加重要。在一个模块或函数中保持一致性是最重要的。
但最重要的是:**要知道何时不一致**——有时样式指南根本不适用。有疑问时,请使用你的最佳判断力。查看其他示例并决定哪种看起来最佳。如有疑问,请随时提出!
代码布局
缩进
每个缩进级别使用 4 个空格。
制表符或空格
空格是首选缩进方法。
应避免混合使用制表符和空格。
空行
用两个空行包围 Solidity 源代码中的顶层声明。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
// ...
}
contract B {
// ...
}
contract C {
// ...
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
// ...
}
contract B {
// ...
}
contract C {
// ...
}
在合约中,用一个空行包围函数声明。
在相关的一行代码组之间可以省略空行(例如,抽象合约的存根函数)。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract A {
function spam() public virtual pure;
function ham() public virtual pure;
}
contract B is A {
function spam() public pure override {
// ...
}
function ham() public pure override {
// ...
}
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract A {
function spam() virtual pure public;
function ham() public virtual pure;
}
contract B is A {
function spam() public pure override {
// ...
}
function ham() public pure override {
// ...
}
}
最大行长
建议的最大行长为 120 个字符。
换行应符合以下准则。
第一个参数不应附加到左括号。
应使用一个且仅一个缩进。
每个参数应独占一行。
结束元素
);
应放置在最后一行上。
函数调用
是
thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3
);
否
thisFunctionCallIsReallyLong(longArgument1,
longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(longArgument1,
longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(
longArgument1, longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3);
赋值语句
是
thisIsALongNestedMapping[being][set][toSomeValue] = someFunction(
argument1,
argument2,
argument3,
argument4
);
否
thisIsALongNestedMapping[being][set][toSomeValue] = someFunction(argument1,
argument2,
argument3,
argument4);
事件定义和事件发射器
是
event LongAndLotsOfArgs(
address sender,
address recipient,
uint256 publicKey,
uint256 amount,
bytes32[] options
);
emit LongAndLotsOfArgs(
sender,
recipient,
publicKey,
amount,
options
);
否
event LongAndLotsOfArgs(address sender,
address recipient,
uint256 publicKey,
uint256 amount,
bytes32[] options);
emit LongAndLotsOfArgs(sender,
recipient,
publicKey,
amount,
options);
源文件编码
首选 UTF-8 或 ASCII 编码。
导入
导入语句应始终放在文件顶部。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
import "./Owned.sol";
contract A {
// ...
}
contract B is Owned {
// ...
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
// ...
}
import "./Owned.sol";
contract B is Owned {
// ...
}
函数顺序
排序有助于读者识别他们可以调用的函数,以及更容易找到构造函数和回退定义。
函数应根据其可见性分组并排序
构造函数
接收函数(如果存在)
回退函数(如果存在)
外部
公共
内部
私有
在同一个分组中,将 view
和 pure
函数放在最后。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract A {
constructor() {
// ...
}
receive() external payable {
// ...
}
fallback() external {
// ...
}
// External functions
// ...
// External functions that are view
// ...
// External functions that are pure
// ...
// Public functions
// ...
// Internal functions
// ...
// Private functions
// ...
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract A {
// External functions
// ...
fallback() external {
// ...
}
receive() external payable {
// ...
}
// Private functions
// ...
// Public functions
// ...
constructor() {
// ...
}
// Internal functions
// ...
}
表达式中的空格
避免在以下情况下使用多余的空格
在括号、方括号或花括号的内部,除了单行函数声明之外。
是
spam(ham[1], Coin({name: "ham"}));
否
spam( ham[ 1 ], Coin( { name: "ham" } ) );
例外
function singleLine() public { spam(); }
在逗号、分号之前
是
function spam(uint i, Coin coin) public;
否
function spam(uint i , Coin coin) public ;
在赋值运算符或其他运算符周围有多个空格以与另一个对齐
是
x = 1;
y = 2;
longVariable = 3;
否
x = 1;
y = 2;
longVariable = 3;
不要在接收函数和回退函数中包含空格
是
receive() external payable {
...
}
fallback() external {
...
}
否
receive () external payable {
...
}
fallback () external {
...
}
控制结构
表示合约、库、函数和结构体的正文的花括号应
在与声明相同的行上打开
在与声明开头相同的缩进级别上单独一行关闭。
左花括号前面应该有一个空格。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Coin {
struct Bank {
address owner;
uint balance;
}
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Coin
{
struct Bank {
address owner;
uint balance;
}
}
相同的建议适用于控制结构 if
、else
、while
和 for
。
此外,在控制结构 if
、while
和 for
与表示条件的圆括号块之间应该有一个空格,以及在条件圆括号块与左花括号之间应该有一个空格。
是
if (...) {
...
}
for (...) {
...
}
否
if (...)
{
...
}
while(...){
}
for (...) {
...;}
对于正文包含单个语句的控制结构,如果语句包含在一行上,则可以省略花括号。
是
if (x < 10)
x += 1;
否
if (x < 10)
someArray.push(Coin({
name: 'spam',
value: 42
}));
对于具有 else
或 else if
子句的 if
块,else
应放置在与 if
的右花括号相同的行上。这是与其他块状结构规则的例外。
是
if (x < 3) {
x += 1;
} else if (x > 7) {
x -= 1;
} else {
x = 5;
}
if (x < 3)
x += 1;
else
x -= 1;
否
if (x < 3) {
x += 1;
}
else {
x -= 1;
}
函数声明
对于简短的函数声明,建议将函数正文的左花括号保留在与函数声明相同的行上。
右花括号应位于与函数声明相同的缩进级别上。
左花括号前面应该有一个空格。
是
function increment(uint x) public pure returns (uint) {
return x + 1;
}
function increment(uint x) public pure onlyOwner returns (uint) {
return x + 1;
}
否
function increment(uint x) public pure returns (uint)
{
return x + 1;
}
function increment(uint x) public pure returns (uint){
return x + 1;
}
function increment(uint x) public pure returns (uint) {
return x + 1;
}
function increment(uint x) public pure returns (uint) {
return x + 1;}
函数的修饰符顺序应为
可见性
可变性
虚拟
重写
自定义修饰符
是
function balance(uint from) public view override returns (uint) {
return balanceOf[from];
}
function increment(uint x) public onlyOwner pure returns (uint) {
return x + 1;
}
否
function balance(uint from) public override view returns (uint) {
return balanceOf[from];
}
function increment(uint x) onlyOwner public pure returns (uint) {
return x + 1;
}
对于长的函数声明,建议将每个参数放在与函数正文相同的缩进级别上的单独一行上。右括号和左括号也应放置在它们自己的行上,并位于与函数声明相同的缩进级别上。
是
function thisFunctionHasLotsOfArguments(
address a,
address b,
address c,
address d,
address e,
address f
)
public
{
doSomething();
}
否
function thisFunctionHasLotsOfArguments(address a, address b, address c,
address d, address e, address f) public {
doSomething();
}
function thisFunctionHasLotsOfArguments(address a,
address b,
address c,
address d,
address e,
address f) public {
doSomething();
}
function thisFunctionHasLotsOfArguments(
address a,
address b,
address c,
address d,
address e,
address f) public {
doSomething();
}
如果一个长的函数声明有修饰符,那么每个修饰符都应放在它自己的行上。
是
function thisFunctionNameIsReallyLong(address x, address y, address z)
public
onlyOwner
priced
returns (address)
{
doSomething();
}
function thisFunctionNameIsReallyLong(
address x,
address y,
address z
)
public
onlyOwner
priced
returns (address)
{
doSomething();
}
否
function thisFunctionNameIsReallyLong(address x, address y, address z)
public
onlyOwner
priced
returns (address) {
doSomething();
}
function thisFunctionNameIsReallyLong(address x, address y, address z)
public onlyOwner priced returns (address)
{
doSomething();
}
function thisFunctionNameIsReallyLong(address x, address y, address z)
public
onlyOwner
priced
returns (address) {
doSomething();
}
多行输出参数和返回语句应遵循在 最大行长 部分中找到的关于包装长行的相同样式建议。
是
function thisFunctionNameIsReallyLong(
address a,
address b,
address c
)
public
returns (
address someAddressName,
uint256 LongArgument,
uint256 Argument
)
{
doSomething()
return (
veryLongReturnArg1,
veryLongReturnArg2,
veryLongReturnArg3
);
}
否
function thisFunctionNameIsReallyLong(
address a,
address b,
address c
)
public
returns (address someAddressName,
uint256 LongArgument,
uint256 Argument)
{
doSomething()
return (veryLongReturnArg1,
veryLongReturnArg1,
veryLongReturnArg1);
}
对于继承的合约上的构造函数,如果其基类需要参数,则如果函数声明很长或难以阅读,建议将基构造函数以与修饰符相同的方式放在新行上。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// Base contracts just to make this compile
contract B {
constructor(uint) {
}
}
contract C {
constructor(uint, uint) {
}
}
contract D {
constructor(uint) {
}
}
contract A is B, C, D {
uint x;
constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4)
{
// do something with param5
x = param5;
}
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// Base contracts just to make this compile
contract B {
constructor(uint) {
}
}
contract C {
constructor(uint, uint) {
}
}
contract D {
constructor(uint) {
}
}
contract A is B, C, D {
uint x;
constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4) {
x = param5;
}
}
contract X is B, C, D {
uint x;
constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4) {
x = param5;
}
}
在声明具有单个语句的简短函数时,可以在一行上完成,这是允许的。
允许
function shortFunction() public { doSomething(); }
这些关于函数声明的指南旨在提高可读性。作者应使用他们的最佳判断力,因为本指南没有尝试涵盖函数声明的所有可能排列。
映射
在变量声明中,不要用空格将关键字 mapping
与其类型隔开。不要用空格将任何嵌套的 mapping
关键字与其类型隔开。
是
mapping(uint => uint) map;
mapping(address => bool) registeredAddresses;
mapping(uint => mapping(bool => Data[])) public data;
mapping(uint => mapping(uint => s)) data;
否
mapping (uint => uint) map;
mapping( address => bool ) registeredAddresses;
mapping (uint => mapping (bool => Data[])) public data;
mapping(uint => mapping (uint => s)) data;
变量声明
数组变量的声明不应该在类型和方括号之间有空格。
是
uint[] x;
否
uint [] x;
其他建议
字符串应使用双引号而不是单引号引起来。
是
str = "foo";
str = "Hamlet says, 'To be or not to be...'";
否
str = 'bar';
str = '"Be yourself; everyone else is already taken." -Oscar Wilde';
在运算符的两侧都加上一个空格。
是
x = 3;
x = 100 / 10;
x += 3 + 4;
x |= y && z;
否
x=3;
x = 100/10;
x += 3+4;
x |= y&&z;
优先级高于其他运算符的运算符可以排除周围的空格,以表示优先级。这旨在为复杂的语句提供更好的可读性。你应该始终在运算符的两侧使用相同数量的空格
是
x = 2**3 + 5;
x = 2*y + 3*z;
x = (a+b) * (a-b);
否
x = 2** 3 + 5;
x = y+z;
x +=1;
布局顺序
合约元素应按以下顺序排列
编译指示语句
导入语句
事件
错误
接口
库
合约
在每个合约、库或接口内部,使用以下顺序
类型声明
状态变量
事件
错误
修饰符
函数
注意
在事件或状态变量中声明类型可能更清晰。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.4 <0.9.0;
abstract contract Math {
error DivideByZero();
function divide(int256 numerator, int256 denominator) public virtual returns (uint256);
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.4 <0.9.0;
abstract contract Math {
function divide(int256 numerator, int256 denominator) public virtual returns (uint256);
error DivideByZero();
}
命名约定
命名约定在被广泛采用和使用时非常强大。使用不同的约定可以传达重要的 *元* 信息,否则这些信息将不会立即可用。
这里给出的命名建议旨在提高可读性,因此它们不是规则,而是准则,旨在帮助通过事物名称传达尽可能多的信息。
最后,代码库中的命名规范应始终优先于本文件中概述的任何约定。
命名风格
为了避免混淆,以下名称将用于指代不同的命名风格。
b
(单个小写字母)B
(单个大写字母)小写
大写
大写_字母_用_下划线_分隔
CapitalizedWords
(或 CapWords)mixedCase
(与 CapitalizedWords 不同,首字母是小写!)
注意
在 CapWords 中使用首字母缩略词时,将首字母缩略词的所有字母都大写。因此,HTTPServerError 比 HttpServerError 更好。在 mixedCase 中使用首字母缩略词时,将首字母缩略词的所有字母都大写,但如果它是名称的开头,则保持第一个小写。因此,xmlHTTPRequest 比 XMLHTTPRequest 更好。
避免使用的名称
l
- 小写字母 elO
- 大写字母 ohI
- 大写字母 eye
永远不要将这些用于单个字母变量名。它们通常无法与数字一和零区分开来。
合约和库名称
合约和库应使用 CapWords 风格命名。示例:
SimpleToken
、SmartBank
、CertificateHashRepository
、Player
、Congress
、Owned
。合约和库名称也应该与其文件名匹配。
如果一个合约文件包含多个合约和/或库,则文件名应与核心合约匹配。但是,如果可以避免,则不建议这样做。
如以下示例所示,如果合约名称为 Congress
,库名称为 Owned
,则它们关联的文件名应分别为 Congress.sol
和 Owned.sol
。
是
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// Owned.sol
contract Owned {
address public owner;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
在 Congress.sol
中
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
import "./Owned.sol";
contract Congress is Owned, TokenRecipient {
//...
}
否
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// owned.sol
contract owned {
address public owner;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
在 Congress.sol
中
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
import "./owned.sol";
contract Congress is owned, tokenRecipient {
//...
}
结构体名称
结构体应使用 CapWords 风格命名。示例:MyCoin
、Position
、PositionXY
。
事件名称
事件应使用 CapWords 风格命名。示例:Deposit
、Transfer
、Approval
、BeforeTransfer
、AfterTransfer
。
函数名称
函数应使用 mixedCase。示例:getBalance
、transfer
、verifyOwner
、addMember
、changeOwner
。
函数参数名称
函数参数应使用 mixedCase。示例:initialSupply
、account
、recipientAddress
、senderAddress
、newOwner
。
编写操作自定义结构体的库函数时,结构体应该是第一个参数,并且始终命名为 self
。
局部变量和状态变量名称
使用 mixedCase。示例:totalSupply
、remainingSupply
、balancesOf
、creatorAddress
、isPreSale
、tokenExchangeRate
。
常量
常量应使用全大写字母,并用下划线分隔单词。示例:MAX_BLOCKS
、TOKEN_NAME
、TOKEN_TICKER
、CONTRACT_VERSION
。
修饰符名称
使用 mixedCase。示例:onlyBy
、onlyAfter
、onlyDuringThePreSale
。
枚举
枚举,以简单类型声明的风格,应使用 CapWords 风格命名。示例:TokenGroup
、Frame
、HashStyle
、CharacterLocation
。
避免命名冲突
singleTrailingUnderscore_
当所需名称与现有状态变量、函数、内置函数或其他保留名称冲突时,建议使用此约定。
非外部函数和变量的下划线前缀
_singleLeadingUnderscore
建议将此约定用于非外部函数和状态变量(private
或 internal
)。未指定可见性的状态变量默认情况下为 internal
。
在设计智能合约时,面向公众的 API(任何帐户都可以调用的函数)是一个重要考虑因素。前导下划线可以让您立即识别此类函数的意图,但更重要的是,如果您将函数从非外部函数更改为外部函数(包括 public
)并相应地重命名它,这将迫使您在重命名时审查每个调用站点。这可以是对意外外部函数的重要手动检查,以及常见安全漏洞的来源(避免为此更改使用查找-替换-全部工具)。
NatSpec
Solidity 合约还可以包含 NatSpec 注释。它们用三个斜杠(///
)或双星号块(/** ... */
)编写,应该直接在函数声明或语句上方使用。
例如,来自 简单智能合约 的合约,添加了注释后,看起来像下面这样
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
/// @author The Solidity Team
/// @title A simple storage example
contract SimpleStorage {
uint storedData;
/// Store `x`.
/// @param x the new value to store
/// @dev stores the number in the state variable `storedData`
function set(uint x) public {
storedData = x;
}
/// Return the stored value.
/// @dev retrieves the value of the state variable `storedData`
/// @return the stored value
function get() public view returns (uint) {
return storedData;
}
}
建议使用 NatSpec 对所有公共接口(ABI 中的所有内容)进行完全注释。
请参阅有关 NatSpec 的部分以获取详细说明。