原文地址: Advanced Solidity Topics
翻译: JulySong
在新生课程中,我们查看了一些基本的 Solidity 语法。我们涵盖了变量、数据类型、函数、循环、条件流和数组。
然而,Solidity 还有一些东西,这些东西对于大二及以后的编码任务很重要。在本教程中,我们将介绍一些更重要的 Solidity 主题。
喜欢视频? 如果您想从视频中学习,我们的 YouTube 上有本教程的录音。它分为两部分,并且有时间戳。单击下面的屏幕截图观看视频,或继续阅读教程!
第 1 部分 视频
第 2 部分 视频
索引
映射(Mappings) Solidity 中的映射就像其他编程语言中的哈希图或字典一样。它们用于将数据存储在键值对中。
映射是使用语法创建的 mapping (keyType => valueType)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract Mapping { // 从地址映射到 uint mapping(address => uint) public myMap; function get(address _addr) public view returns (uint) { // 映射总是返回一个值。 // 如果该值从未设置,它将返回默认值。 // uint 的默认值为 0 return myMap[_addr]; } function set(address _addr, uint _i) public { // 更新这个地址的值 myMap[_addr] = _i; } function remove(address _addr) public { // 将值重置为默认值。 delete myMap[_addr]; } }
我们还可以创建嵌套映射,其中key
指向第二个嵌套映射。为此,我们将 设置valueType
为映射本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 contract NestedMappings { // 从地址映射 => (从 uint 映射到 bool) mapping(address => mapping(uint => bool)) public nestedMap; function get(address _addr1, uint _i) public view returns (bool) { // 你可以从嵌套映射中获取值 // 即使它没有初始化 // bool 类型的默认值为 false return nestedMap[_addr1][_i]; } function set( address _addr1, uint _i, bool _boo ) public { nestedMap[_addr1][_i] = _boo; } function remove(address _addr1, uint _i) public { delete nestedMap[_addr1][_i]; } }
枚举(Enums) 这个词Enum
代表Enumerable
。它们是用户定义的类型,其中包含一组常量(称为成员)的人类可读名称。它们通常用于将变量限制为仅具有几个预定义值之一。由于它们只是人类可读常量的抽象,实际上,它们在内部表示为uint
s。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract Enum { // 代表不同可能运输状态的 enum Status { Pending, Shipped, Accepted, Rejected, Canceled } // 声明一个 Status 类型的变量 // 这只能包含一个预定义的值 Status public status; // 因为枚举在内部由uint 表示 // 此函数将始终返回一个 uint // Pending = 0 // Shipped = 1 // Accepted = 2 // Rejected = 3 // Canceled = 4 // 不能大于 4被返回 function get() public view returns (Status) { return status; } // 为输入传递一个 uint 以更新值 function set(Status _status) public { status = _status; } // 更新特定枚举成员的值 function cancel() public { status = Status.Canceled; // 将设置状态 = 4 } }
结构(Structs) 结构的概念存在于许多高级编程语言中。它们用于定义您自己的数据类型,这些数据类型将相关数据组合在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract TodoList { // 声明一个将两种数据类型组合在一起的 struct TodoItem { string text; bool completed; } // 创建一个 TodoItem 结构数组 TodoItem[] public todos; function createTodo(string memory _text) public { // 有多种初始化结构的方法 // 方法 1 - 像函数一样调用它 todos.push(TodoItem(_text, false)); // 方法 2 - 显式设置其键 todos.push(TodoItem({ text: _text, completed: false })); // 方法 3 - 初始化一个空结构体,然后设置单个属性 TodoItem memory todo; todo.text = _text; todo.completed = false; todos.push(todo); } // 更新一个结构体值 function update(uint _index, string memory _text) public { todos[_index].text = _text; } // 更新完成 function toggleCompleted(uint _index) public { todos[_index].completed = !todos[_index].completed; } }
视图和纯函数(View and Pure Functions) 您可能已经注意到,我们一直在编写的一些函数在函数头中指定了view
或pure
关键字之一。这些是特殊关键字,表示函数的特定行为。
Getter 函数(返回值的函数)可以声明为view
或pure
。
View
: 不改变任何状态值的函数
Pure
: 不改变任何状态值,但也不读取任何状态值的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract ViewAndPure { // 声明一个状态变量 uint public x = 1; // 承诺不修改状态(但可以读取状态) function addToX(uint y) public view returns (uint) { return x + y; } // 承诺不修改或读取状态 function add(uint i, uint j) public pure returns (uint) { return i + j; } }
函数修饰符(Function Modifiers) 修饰符是可以在函数调用之前和/或之后运行的代码。它们通常用于限制对某些功能的访问、验证输入参数、防止某些类型的攻击等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract Modifiers { address public owner; constructor() { // 设置合约部署者为合约 owner = msg.sender; } // 创建一个只允许所有者调用函数的 modifier onlyOwner() { require(msg.sender == owner, "You are not the owner"); // 下划线是修饰符内部使用的特殊字符 // 它告诉 Solidity 此时执行该修饰符所用的函数 // 因此,这个修饰符将首先执行上述检查 // 然后运行其余代码 _; } // 创建一个函数并对其应用 onlyOwner 修饰符 function changeOwner(address _newOwner) public onlyOwner { // 只有当修饰符通过检查成功时,我们才会到达这一点 // 所以这个事务的调用者必须是当前的所有者 owner = _newOwner; } }
活动(Events) 事件允许合约在以太坊区块链上执行日志记录。例如,可以稍后解析给定合约的日志以在前端界面上执行更新。它们通常用于允许前端界面侦听特定事件并更新用户界面,或用作廉价的存储形式。
1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract Events { // 声明一个记录地址和字符串事件的 event TestCalled(address sender, string message); function test() public { // 记录一个事件 emit TestCalled(msg.sender, "Someone called test()!"); } }
构造函数(Constructors) constructor
是一个可选函数,在首次部署合约时执行。您还可以将参数传递给构造函数。
P.S. - 如果你还记得,我们实际上在新生跟踪加密货币和 NFT 教程中使用了构造函数!
1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract X { string public name; // 部署合约时需要提供一个字符串参数 constructor(string memory _name) { // 部署合约时会立即设置 name = _name; } }
遗产(Inheritance) 继承是一个合同可以继承另一个合同的属性和方法的过程。Solidity 支持多重继承。合约可以使用is
关键字继承其他合约。
注意:我们实际上还在 Freshman Track Cryptocurrency 和 NFT 教程中进行了继承——我们分别从ERC20
和ERC721
合约继承。
具有可以被子合约覆盖的函数的父合约必须声明为virtual
函数。
将要覆盖父函数的子合约必须使用override
关键字。
如果父合约以相同的名称共享方法或属性,则继承顺序很重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /* 继承图 A / \ B C / / D E */ contract A { // 声明一个可以被子函数覆盖的虚函数 foo() function foo() public pure virtual returns (string memory) { return "A"; } } contract B is A { // 覆盖 A.foo(); // 但也允许这个函数被更多的子函数覆盖 // 所以我们指定了两个关键字 - virtual 和 override function foo() public pure virtual override returns (string memory) { return "B"; } } contract C is A { // 类似于上面的合约 B function foo() public pure virtual override returns (string memory) { return "C"; } } // 从多个合约继承时,如果一个函数被多次定义,则使用最右边的父合约的函数。 contract D is B, C { // D.foo() 返回 "C" // 因为 C 是函数 foo() 的最右边父级; // override (B,C) 表示我们要覆盖两个父级中存在的方法 function foo() public pure override (B, C) returns (string memory) { // super 是一个特殊的关键字,用于调用functions // 在父合约中 return super.foo(); } } contract E is C, B { // E.foo() 返回 "B" // 因为 B 是函数 foo() 的最右边父级; function foo() public pure override (C, B) returns (string memory) { return super.foo(); } }
转移 ETH(Transferring ETH) 有三种方法可以将 ETH 从合约转移到其他地址。但是,其中两种方法在最新版本中不再是 Solidity 推荐的方法,因此我们将跳过它们。
目前,从合约中转移 ETH 的推荐方式是使用该call
功能。该call
函数返回一个bool
指示传输成功或失败的信息。
如何在普通的以太坊账户地址接收以太币 如果将 ETH 转移到普通账户(如 Metamask 地址),您无需做任何特殊的事情,因为所有此类账户都可以自动接受 ETH 转移。
如何在合约中接收 ETH 但是,如果您正在编写一份合约,希望能够直接接收 ETH 转账,您必须至少具有以下功能之一
receive() external payable
fallback() external payable
如果msg.data
是空值则调用receive()
,否则使用fallback()
。
msg.data
是一种指定任意数据和事务的方法。您通常不会手动使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract ReceiveEther { /* 调用哪个函数,fallback() 还是 receive()? 发送以太币 | msg.data 为空? / \ yes no / \ receive() 存在吗? fallback() / \ 是 否 / \ receive() fallback() */ // 接收以太币的函数。msg.data 必须为空 receive() external payable {} // 当 msg.data 不为空时调用 fallback 函数 fallback() external payable {} function getBalance() public view returns (uint) { return address(this).balance; } } contract SendEther { function sendEth(address payable _to) public payable { // 只需将在此应付函数中收到的 ETH 转发到给定地址 uint amountToSend = msg.value; // call 返回一个 bool 值,指定成功或失败 (bool success, bytes memory data) = _to.call{value: msg.value}(""); require(success == true, "Failed to send ETH"); } }
调用外部合约(Calling External Contracts) 合约可以通过调用其他合约实例上的函数来调用其他合约,例如A.foo(x, y, z)
. 为此,您必须有一个接口A
来告诉您的合约存在哪些功能。Solidity 中的接口行为类似于头文件,并且与我们在从前端调用合约时使用的 ABI 具有相似的用途。这允许合约知道如何编码和解码函数参数并返回值以调用外部合约。
注意:您使用的接口不需要很广泛。即它们不一定需要包含外部合同中存在的所有功能 - 只需要您可能在某个时候调用的那些功能。
假设有一个外部ERC20
合约,我们有兴趣调用该balanceOf
函数来检查我们合约中给定地址的余额。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; interface MinimalERC20 { // 只需在接口包含我们感兴趣的函数 function balanceOf(address account) external view returns (uint256); } contract MyContract { MinimalERC20 externalContract; constructor(address _externalContract) { // 初始化一个 MinimalERC20 合约实例 externalContract = MinimalERC20(_externalContract); } function mustHaveSomeBalance() public { // 要求该交易的调用者在外部 ERC20 合约中的代币余额为非零 uint balance = externalContract.balanceOf(msg.sender); require(balance > 0, "You don't own any tokens of external contract"); } }
Import 声明(Import Statements) 为了保持代码的可读性,您可以将 Solidity 代码拆分为多个文件。Solidity 允许导入本地和外部文件。
本地 Import 假设我们有一个这样的文件夹结构:
1 2 ├── Import.sol └── Foo.sol
Foo.sol
在哪里
1 2 3 4 5 6 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract Foo { string public name = "Foo"; }
我们可以像这样导入Foo
和使用它Import.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; // 从当前目录导入 Foo.sol import "./Foo.sol"; contract Import { // 初始化 Foo.sol Foo public foo = new Foo(); // 通过获取 Foo.sol 的名称来测试它。 function getFooName() public view returns (string memory) { return foo.name(); } }
注意:当我们使用 Hardhat 时,我们也可以通过 将合约安装为节点模块npm
,然后从node_modules
文件夹中导入合约。这些也算作本地导入,因为从技术上讲,当您安装软件包时,您正在将合同下载到本地计算机。
外部 Import 您也可以通过简单地复制 URL 从 Github 导入。我们在新生课程的加密货币和 NFT 教程中做到了这一点。
1 2 3 4 5 6 // https://github.com/owner/repo/blob/branch/path/to/Contract.sol import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol"; // 示例从 openzeppelin-contract repo 导入 ERC20.sol // 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
- 它确保数学运算不会导致整数下溢或溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 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; // 如果 z 溢出,抛出错误 require(z >= x, "uint overflow"); return z; } } contract TestSafeMath { function testAdd(uint x, uint y) public pure returns (uint) { return SafeMath.add(x, y); } }
练习题
🤔 映射(Mappings)的行为类似于哪种数据结构?
A: Arrays
B: Trees
C: Graphs
D: Hashmaps
🤔 枚举(enums)有什么用?
A: 将变量限制为仅具有几个预定义值之一。
B: 将函数限制为仅具有几个预定义值之一
C: 将接口限制为仅具有几个预定义值之一
🤔 结构(structs)是用来做什么的?
A: 创建自定义数据类型
B: 限制变量只有几个预定义的值
C: 存储键值对
D: 登录到区块链
🤔 纯函数(Pure functions)用于将数据写入区块链
A: 是的
B: 错误
🤔 视图函数(View functions)不会改变任何状态值
A: 是的
B: 错误
🤔 修饰符(modifiers)不用于什么?
A: 限制对某些功能的访问
B: 验证输入参数
C: 功能替换
D: 防止某些类型的攻击
🤔 事件(events)有什么用?
A: 登录到区块链
B: 创建自定义数据类型
C: 创建在特定时间间隔后运行的函数
🤔 构造函数(constructor)是可选的吗?
A: 是的
B: 错误
🤔 contract B is A { }
A: A 继承了 B
B: B 继承 A
C: A 和 B 是同一份合约
🤔 你必须有一个智能合约才能将 ETH 发送到另一个 ETH 地址
A: 是的
B: 错误
🤔 如果你想让你的合约直接接收 ETH 转账,你必须使用“receive() external pay”或“fallback() external pay”
A: 是的
B: 错误
🤔 库可以包含状态变量
A: 是的
B: 错误
参考答案:
D
A
A
B
A
C
A
A
B
B
A
B