样式指南

简介

本指南旨在为编写 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 个字符。

换行应符合以下准则。

  1. 第一个参数不应附加到左括号。

  2. 应使用一个且仅一个缩进。

  3. 每个参数应独占一行。

  4. 结束元素 ); 应放置在最后一行上。

函数调用

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 {
    // ...
}

函数顺序

排序有助于读者识别他们可以调用的函数,以及更容易找到构造函数和回退定义。

函数应根据其可见性分组并排序

  • 构造函数

  • 接收函数(如果存在)

  • 回退函数(如果存在)

  • 外部

  • 公共

  • 内部

  • 私有

在同一个分组中,将 viewpure 函数放在最后。

// 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;
    }
}

相同的建议适用于控制结构 ifelsewhilefor

此外,在控制结构 ifwhilefor 与表示条件的圆括号块之间应该有一个空格,以及在条件圆括号块与左花括号之间应该有一个空格。

if (...) {
    ...
}

for (...) {
    ...
}

if (...)
{
    ...
}

while(...){
}

for (...) {
    ...;}

对于正文包含单个语句的控制结构,如果语句包含在一行上,则可以省略花括号。

if (x < 10)
    x += 1;

if (x < 10)
    someArray.push(Coin({
        name: 'spam',
        value: 42
    }));

对于具有 elseelse 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;}

函数的修饰符顺序应为

  1. 可见性

  2. 可变性

  3. 虚拟

  4. 重写

  5. 自定义修饰符

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;

布局顺序

合约元素应按以下顺序排列

  1. 编译指示语句

  2. 导入语句

  3. 事件

  4. 错误

  5. 接口

  6. 合约

在每个合约、库或接口内部,使用以下顺序

  1. 类型声明

  2. 状态变量

  3. 事件

  4. 错误

  5. 修饰符

  6. 函数

注意

在事件或状态变量中声明类型可能更清晰。

// 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 - 小写字母 el

  • O - 大写字母 oh

  • I - 大写字母 eye

永远不要将这些用于单个字母变量名。它们通常无法与数字一和零区分开来。

合约和库名称

  • 合约和库应使用 CapWords 风格命名。示例:SimpleTokenSmartBankCertificateHashRepositoryPlayerCongressOwned

  • 合约和库名称也应该与其文件名匹配。

  • 如果一个合约文件包含多个合约和/或库,则文件名应与核心合约匹配。但是,如果可以避免,则不建议这样做。

如以下示例所示,如果合约名称为 Congress,库名称为 Owned,则它们关联的文件名应分别为 Congress.solOwned.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 风格命名。示例:MyCoinPositionPositionXY

事件名称

事件应使用 CapWords 风格命名。示例:DepositTransferApprovalBeforeTransferAfterTransfer

函数名称

函数应使用 mixedCase。示例:getBalancetransferverifyOwneraddMemberchangeOwner

函数参数名称

函数参数应使用 mixedCase。示例:initialSupplyaccountrecipientAddresssenderAddressnewOwner

编写操作自定义结构体的库函数时,结构体应该是第一个参数,并且始终命名为 self

局部变量和状态变量名称

使用 mixedCase。示例:totalSupplyremainingSupplybalancesOfcreatorAddressisPreSaletokenExchangeRate

常量

常量应使用全大写字母,并用下划线分隔单词。示例:MAX_BLOCKSTOKEN_NAMETOKEN_TICKERCONTRACT_VERSION

修饰符名称

使用 mixedCase。示例:onlyByonlyAfteronlyDuringThePreSale

枚举

枚举,以简单类型声明的风格,应使用 CapWords 风格命名。示例:TokenGroupFrameHashStyleCharacterLocation

避免命名冲突

  • singleTrailingUnderscore_

当所需名称与现有状态变量、函数、内置函数或其他保留名称冲突时,建议使用此约定。

非外部函数和变量的下划线前缀

  • _singleLeadingUnderscore

建议将此约定用于非外部函数和状态变量(privateinternal)。未指定可见性的状态变量默认情况下为 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 的部分以获取详细说明。