合约

合约是 Solidity 中我们需要处理的最高级、最复杂,同时也是最重要的类型。我们的大部分功能都是在合约中实现。

日光之下,并无新物。合约其实类似于面向对象编程中的

合约中定义了一系列的方法以及状态变量。当我们要启用一个合约时,我们将合约的代码部署到某一个以太坊地址上,我们把这个过程称为合约部署。

部署完成后,其他用户就可以通过该地址来跟合约交互,例如进行一次投票。

假设用户 A 需要调用地址 0x01 处的合约中的 vote 方法进行投票,则用户 A 将该请求发送到以太坊网络上,接收到的 EVM0x01 处取出保存的合约代码,执行其中的 vote 方法代码。

合约中的代码以及状态变量(在合约内声明,保存在存储区中的变量)会被永久性地存储在以太坊区块链中。

声明合约

合约通过 contract Name {} 进行声明。在合约内部可以声明:

  • 合约状态变量
  • 合约函数
  • 自定义类型
  • struct 结构体
ContractDeclare.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract ContractDeclare {
    address[] usersAddress;               // 状态变量
    mapping (address => uint) tokenCount; // 状态变量
    type Age is uint8;                    // 自定义类型
    struct User {                         // struct 结构体
      address addr;
      string nickname;
      Age age;
    }
    User[] public users;                         // 状态变量

    // 合约函数
    function register(string calldata nickname, Age age) external {
      User memory newUser;
      newUser.nickname = nickname;
      newUser.age = age;
      newUser.addr = msg.sender;
      users.push(newUser);
    }
}

部署合约

我们可以通过 web3.jstruffleRemix IDE 等工具,将合约代码由外部部署到以太坊上。这种部署方式是通过以太坊网络向外暴露的 RPC 接口或 HTTP 接口等实现的。

除此之外,也可以在智能合约 A 内部,通过 new 关键字来实例化另一个合约 B,从而把合约 B 部署到以太坊上。我们以 合约工厂 为例,不同的用户可以通过该合约工厂的 createContract 函数来创建一张属于该用户的合约:

ContractFactory.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

/// @title 用户合约
contract UserContract {
    address owner;
    string nickname;

    constructor(string memory _nickname) {
        owner = msg.sender;
        nickname = _nickname;
    }
}

/// @title 合约工厂
contract ContractFactory {
    mapping(address => UserContract) public contractMap; // 记录每个用户的合约

    /// @notice 创建用户合约
    function createContract(string calldata nickname) external {
        UserContract c = new UserContract(nickname);
        contractMap[msg.sender] = c;
    }
}

构造函数

在上面的 UserContract 合约中,我们可以看到一个特殊的合约函数 constructor,该函数称为构造函数,当且仅当合约被创建时调用。在该函数中我们可以对合约做一些初始化工作,例如初始化状态变量。

一个合约只能有一个 constructor 函数。

合约销毁

Solidity 可以通过 selfdestruct 来销毁一个合约,包括它的代码以及存储变量。但是根据 EIP-6049 提案,该方法已经被标志为 deprecated(过时的),因为未来可能会有破坏性的改动。

selfdestruct(payableAddress) 接受一个可支付地址参数,合约当前的以太币余额会转被该地址,然后销毁当前合约。

状态变量可见性

声明状态变量语法:

状态变量有三种可见性:

  1. public: 公共
  2. internal: 内部
  3. private: 私有
ContractFactory.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract StateVarVisibility {
  string public nickname;      // 公共状态变量
  address private owner;       // 私有状态变量
  uint internal userCount;     // 内部状态变量
}

公共状态变量

公共状态变量拥有最大的可见性,无论是合约外部、合约内部或者派生合约,都可以访问该变量。

当从外部访问一个合约的公共状态变量时,需要注意合约对外只暴露函数接口。也就是说外部只能访问合约内的公共函数或外部函数。

因此,为了让外部能够访问公共状态变量,Solidity 会为它们创建一个同名的 getter 函数。对于 Java 程序员可能会对这种方式非常熟悉。

ContractFactory.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Son {
    uint public age;

    function getAge() external view returns (uint) {
        return age; // 内部访问,直接访问状态变量 age
    }
}

contract Parent {
    function getSonAge() external returns (uint) {
        Son s = new Son();
        return s.age(); // 外部访问 age,需要调用同名 getter 函数
    }
}

内部状态变量

标为 internal 的内部状态变量,它只对合约内部,以及它的派生合约可见。对于派生合约的内容,可以参阅 《派生与继承》

私有状态变量

私有状态变量,只对当前合约可见,即使是派生合约也不能访问它。

函数可见性

合约内的函数有四种可见性:

  • public:公共函数
  • external:外部函数
  • internal:内部函数
  • private:私有函数
FunctionVisibility.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract FunctionVisibility {
    // 公共函数
    function publicFunc(string memory name) public {}

    // 外部函数
    function externalFunc(string calldata name) external {}

    // 内部函数
    function internalFunc(string memory name) internal {}

    // 私有函数
    function privateFunc(string memory name) private {}
}

公共函数

公共函数对合约内部、外部、派生合约可见。

外部函数

外部函数可以视为合约的对外接口,它只对外部可见。

内部函数

内部函数对当前合约内部、派生合约可见。

私有函数

私有函数只对当前合约内部可见,即使是派生合约该函数也是不可见的。

this

Solidity 中同样使用 this 关键字代表当前合约实例。

Solidity 中,当在合约内部,访问当前合约中的状态变量或者函数时,例如调用当前合约中的函数 f,不需要使用 this.f(),只需要直接写 f()

f 是一个公共函数时,在合约内部,f() 表示直接通过内部调用来调用 fthis.f() 则表示通过外部调用调用 f

同样道理,如果在当前合约内部要使用外部调用的方式访问公共状态变量,则应该调用其 getter 函数:this.var()