高级 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);
    }
}