Skip to main content

高级 Solidity 主题

在新生课程中,我们研究了一些基本的 Solidity 语法。我们涵盖了变量、数据类型、函数、循环、条件流和数组。

然而,Solidity 还有一些东西,这些东西在大二及以后的编码作业中会很重要。在本教程中,我们将介绍一些更重要的 Solidity 主题。

更喜欢视频?

如果您想从视频中学习,我们在 YouTube 上提供了本教程的录音。它被分成两部分,并有时间戳。通过点击下面的截图来观看视频,或者继续阅读该教程!

第一部分

第二部分

索引

  • 映射
  • 枚举
  • 结构
  • 视图和纯函数
  • 函数修改器
  • 事件
  • 构造函数
  • 继承性
  • 转移 ETH
  • 调用外部合约
  • 导入语句
  • Solidity 库

映射

Solidity 中的映射就像其他编程语言中的哈希图或字典。它们被用来存储键值对中的数据。

映射是通过语法 mapping (keyType => valueType) 创建的。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Mapping {
// Mapping from address to uint
mapping(address => uint) public myMap;

function get(address _addr) public view returns (uint) {
// Mapping always returns a value.
// If the value was never set, it will return the default value.
// The default value for uint is 0
return myMap[_addr];
}

function set(address _addr, uint _i) public {
// Update the value at this address
myMap[_addr] = _i;
}

function remove(address _addr) public {
// Reset the value to the default value.
delete myMap[_addr];
}
}

我们还可以创建嵌套映射,其中的键指向第二个嵌套映射。要做到这一点,我们将 valueType 设置为映射本身。

contract NestedMappings {
// 从地址映射 => (从 uint 到 bool 的映射)
mapping(address => mapping(uint => bool)) public nestedMap;

function get(addr1, uint _i) public view returns (bool) {
// 你可以从一个嵌套的映射中获取值
// 即使它没有被初始化
// bool类型的默认值是false
return nestedMap[_addr1][_i]
}

函数 set(
地址 _addr1,
uint _i,
bool _boo
) 公共 {
nestedMap[_addr1][_i] = _boo;
}

function remove(addr1, uint _i) public {
删除nestedMap[_addr1][_i]
}

}

枚举

Enum 这个词代表了 Enumerable。它们是用户定义的类型,包含一组常量的可读名称,称为成员。它们通常被用来限制一个变量只能有几个预定义的值中的一个。由于它们只是人类可读常量的抽象,实际上,它们在内部被表示为 uints。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Enum {
// Enum 代表不同的可能运输状态
enum Status {
待定。
已发货。
已接受。
拒绝。
取消了
}

// 声明一个类型为Status的变量。
// 它只能包含一个预定义的值
状态 public status;

// 因为枚举在内部是用uint表示的
// 这个函数将总是返回一个uint值
// 待处理 = 0
// 已发送 = 1
// 已接受 = 2
// 被拒绝 = 3
// 取消 = 4
// 高于4的值不能被返回
function get() public view returns (Status) {
返回状态。
}

// 传递一个uint作为输入来更新值
function set(Status _status) public {
status = _status;
}

// 更新值到一个特定的枚举成员
function cancel() public {
status = Status.Canceled; // 将设置 status = 4
}

}

结构

结构的概念存在于许多高级编程语言中。它们被用来定义你自己的数据类型,将相关数据组合在一起。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract TodoList {
// Declare a struct which groups together two data types
struct TodoItem {
string text;
bool completed;
}

// Create an array of TodoItem structs
TodoItem[] public todos;

function createTodo(string memory _text) public {
// There are multiple ways to initialize structs

// Method 1 - Call it like a function
todos.push(TodoItem(_text, false));

// Method 2 - Explicitly set its keys
todos.push(TodoItem({ text: _text, completed: false }));

// Method 3 - Initialize an empty struct, then set individual properties
TodoItem memory todo;
todo.text = _text;
todo.completed = false;
todos.push(todo);
}

// Update a struct value
function update(uint _index, string memory _text) public {
todos[_index].text = _text;
}

// Update completed
function toggleCompleted(uint _index) public {
todos[_index].completed = !todos[_index].completed;
}
}

视图和纯函数

你可能已经注意到,我们所写的一些函数在函数头中指定了一个视图或纯函数的关键字。这些是特殊的关键字,表示函数的特定行为。

Getter 函数(那些返回值的函数)可以被声明为 view 或 pure。

  • View。不改变任何状态值的函数
  • Prue。不改变任何状态值的函数,但也不读取任何状态值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ViewAndPure {
// Declare a state variable
uint public x = 1;

// Promise not to modify the state (but can read state)
function addToX(uint y) public view returns (uint) {
return x + y;
}

// Promise not to modify or read from state
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}
}

函数修改器

修改器是可以在一个函数调用之前和/或之后运行的代码。它们通常用于限制对某些函数的访问,验证输入参数,防止某些类型的攻击,等等。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Modifiers {
address public owner;

constructor() {
// Set the contract deployer as the owner of the contract
owner = msg.sender;
}

// Create a modifier that only allows a function to be called by the owner
modifier onlyOwner() {
require(msg.sender == owner, "You are not the owner");

// Underscore is a special character used inside modifiers
// Which tells Solidity to execute the function the modifier is used on
// at this point
// Therefore, this modifier will first perform the above check
// Then run the rest of the code
_;
}

// Create a function and apply the onlyOwner modifier on it
function changeOwner(address _newOwner) public onlyOwner {
// We will only reach this point if the modifier succeeds with its checks
// So the caller of this transaction must be the current owner
owner = _newOwner;
}
}

事件

事件允许合约在以太坊区块链上执行日志记录。例如,一个给定合约的日志可以在以后被解析,以便在前台界面上执行更新。它们通常被用来让前端界面监听特定的事件并更新用户界面,或者被用作一种廉价的存储形式。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Events {
// Declare an event which logs an address and a string
event TestCalled(address sender, string message);

function test() public {
// Log an event
emit TestCalled(msg.sender, "Someone called test()!");
}
}

构造函数

构造函数是一个可选的函数,在合约首次部署时执行。你也可以向构造函数传递参数。

P.S.--如果你还记得,我们在大一的加密货币和 NFT 教程中实际上使用了构造函数!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract X {
string public name;

// You will need to provide a string argument when deploying the contract
constructor(string memory _name) {
// This will be set immediately when the contract is deployed
name = _name;
}
}

继承

继承是一个合约可以继承另一个合约的属性和方法的程序。Solidity 支持多重继承。契约可以通过使用 is 关键字来继承其他契约。

注意:我们实际上也在新生轨道加密货币和 NFT 教程中做了继承--我们分别从 ERC20 和 ERC721 合约中继承。

一个父合约如果有一个可以被子合约覆盖的函数,必须被声明为一个虚拟函数。

一个要覆盖父函数的子合约必须使用 override 关键字。

如果父合约共享同名的方法或属性,继承的顺序就很重要。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

/* Graph of inheritance
A
/ \
B C
|\ /|
| \/ |
| /\ |
D E
*/

contract A {
// Declare a virtual function foo() which can be overridden by children
function foo() public pure virtual returns (string memory) {
return "A";
}
}

contract B is A {
// Override A.foo();
// But also allow this function to be overridden by further children
// So we specify both keywords - virtual and override
function foo() public pure virtual override returns (string memory) {
return "B";
}
}

contract C is A {
// Similar to contract B above
function foo() public pure virtual override returns (string memory) {
return "C";
}
}

// When inheriting from multiple contracts, if a function is defined multiple times, the right-most parent contract's function is used.
contract D is B, C {
// D.foo() returns "C"
// since C is the right-most parent with function foo();
// override (B,C) means we want to override a method that exists in two parents
function foo() public pure override (B, C) returns (string memory) {
// super is a special keyword that is used to call functions
// in the parent contract
return super.foo();
}
}

contract E is C, B {
// E.foo() returns "B"
// since B is the right-most parent with function foo();
function foo() public pure override (C, B) returns (string memory) {
return super.foo();
}
}

转移 ETH

有三种方法可以将 ETH 从一个合约转移到其他地址。然而,其中两种方法不再是 Solidity 最新版本中推荐的方法,因此我们将跳过这些。

目前,从合约中转移 ETH 的推荐方法是使用调用函数。调用函数返回一个 bool,表示转移的成功或失败。

如何在一个普通的以太坊账户地址中接收以太币

如果将 ETH 转移到一个普通账户(如 Metamask 地址),你不需要做任何特别的事情,因为所有这样的账户都可以自动接受 ETH 转移。

如何在合约中接收以太币

但是,如果您编写的合约希望能够直接接收 ETH 转账,您必须至少有以下一个函数

  • receive() 外部支付
  • fallback() 外部支付

如果 msg.data 为空值,则调用 receive(),否则使用 fallback()。

msg.data 是一种在交易中指定任意数据的方式。你通常不会手动使用它。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ReceiveEther {
/*
Which function is called, fallback() or receive()?

send Ether
|
msg.data is empty?
/ \
yes no
/ \
receive() exists? fallback()
/ \
yes no
/ \
receive() fallback()
*/

// Function to receive Ether. msg.data must be empty
receive() external payable {}

// Fallback function is called when msg.data is not empty
fallback() external payable {}

function getBalance() public view returns (uint) {
return address(this).balance;
}
}

contract SendEther {
function sendEth(address payable _to) public payable {
// Just forward the ETH received in this payable function
// to the given address
uint amountToSend = msg.value;
// call returns a bool value specifying success or failure
(bool success, bytes memory data) = _to.call{value: msg.value}("");
require(success == true, "Failed to send ETH");
}
}

调用外部合约

合约可以通过调用其他合约实例上的函数来调用其他合约,例如 A.foo(x, y, z)。 为此,您必须为 A 提供一个接口,该接口告诉您的合约存在哪些功能。 Solidity 中的接口的行为类似于头文件,并且与我们在从前端调用合约时使用的 ABI 具有相似的用途。 这允许合约知道如何编码和解码函数参数并返回值以调用外部合约。

注意:您使用的接口不需要很广泛。 即它们不一定包含外部合约中存在的所有功能 - 仅包含您可能在某个时候调用的那些功能。

假设有一个外部 ERC20 合约,我们有兴趣调用 balanceOf 函数来检查我们合约中给定地址的余额。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

interface MinimalERC20 {
// Just include the functions we are interested in
// in the interface
function balanceOf(address account) external view returns (uint256);
}

contract MyContract {
MinimalERC20 externalContract;

constructor(address _externalContract) {
// Initialize a MinimalERC20 contract instance
externalContract = MinimalERC20(_externalContract);
}

function mustHaveSomeBalance() public {
// Require that the caller of this transaction has a non-zero
// balance of tokens in the external ERC20 contract
uint balance = externalContract.balanceOf(msg.sender);
require(balance > 0, "You don't own any tokens of external contract");
}
}

进口声明

为了保持代码的可读性,您可以将 Solidity 代码拆分为多个文件。 Solidity 允许导入本地和外部文件。

本地进口

假设我们有一个这样的文件夹结构:

├── Import.sol
└── Foo.sol

Foo.sol 内容

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Foo {
string public name = "Foo";
}

我们可以如下在 Import.sol 到导入使用 Foo

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// import Foo.sol from current directory
import "./Foo.sol";

contract Import {
// Initialize Foo.sol
Foo public foo = new Foo();

// Test Foo.sol by getting it's name.
function getFooName() public view returns (string memory) {
return foo.name();
}
}

注意:当我们使用 Hardhat 时,我们也可以通过 npm 将合约安装为节点模块,然后从 node_modules 文件夹中导入合约。 这些也算作本地导入,因为从技术上讲,当您安装软件包时,您正在将合约下载到本地计算机。

外部进口

您也可以通过简单地复制 URL 从 Github 导入。 我们在新生课程的加密货币和 NFT 教程中做到了这一点。

// https://github.com/owner/repo/blob/branch/path/to/Contract.sol
import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol";

// Example import ERC20.sol from openzeppelin-contract repo
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";

Solidity 库

库类似于 Solidity 中的合约,但有一些限制。 库不能包含任何状态变量,也不能转移 ETH。

通常,库用于向您的合约添加辅助函数。 Solidity 世界中一个非常常用的库是 SafeMath——它确保数学运算不会导致整数下溢或上溢。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

library SafeMath {
function add(uint x, uint y) internal pure returns (uint) {
uint z = x + y;
// If z overflowed, throw an error
require(z >= x, "uint overflow");
return z;
}
}

contract TestSafeMath {
function testAdd(uint x, uint y) public pure returns (uint) {
return SafeMath.add(x, y);
}
}