本节是《Solidity by Example》的中文翻译与深入讲解,专为零基础或刚接触区块链开发的小白朋友打造。我们将通过“示例 + 解说 + 提示”的方式,带你逐步理解每一段 Solidity 代码的实际用途与背后的逻辑。
Solidity 是以太坊等智能合约平台使用的主要编程语言,就像写网页要用 HTML 和 JavaScript,写智能合约就需要会 Solidity。
如果你从没写过区块链代码也没关系,只要你了解一点点编程概念,比如“变量”“函数”“条件判断”,我们就能从最简单的例子开始,一步步建立你的 Solidity 编程思维。
Mapping
映射使用语法 mapping(keyType => valueType)
创建。
键类型(keyType)可以是任何内置值类型、字节、字符串或任何合约。
值类型(valueType)可以是任何类型,包括另一个映射或数组。
映射不可迭代。
- 什么是映射(Mapping)?
- 映射是 Solidity 中的一种数据结构,类似于其他编程语言中的哈希表或字典,用于存储键值对(key-value pairs)。
- 语法为
mapping(keyType => valueType)
,其中:- keyType:键的类型,可以是内置类型(如
uint
、address
)、bytes
、string
或合约类型。 - valueType:值的类型,可以是任何类型,包括基本类型(
uint
、bool
)、复杂类型(数组、结构体)甚至另一个映射。
- keyType:键的类型,可以是内置类型(如
- 映射的特点:
- 键值存储:每个键对应一个值,访问时通过键快速查找值。
- 默认值:如果某个键未被设置,访问时返回类型的默认值(例如,
uint
的默认值是 0,bool
的默认值是false
)。 - 不可迭代:映射不能像数组一样遍历所有键值对(没有内置的迭代方法)。
- 存储在区块链:映射是状态变量,存储在区块链的
storage
中,修改需要 Gas。
- 用途:
- 映射常用于:
- 存储用户数据(例如,地址到余额的映射)。
- 记录状态(例如,地址到权限的映射)。
- 实现复杂数据结构(例如,嵌套映射)。
- 映射常用于:
- 不可迭代的限制:
- 无法直接获取映射中的所有键或值,需要额外维护一个数组来存储键列表(如果需要迭代)。
// SPDX-License-Identifier: MIT
// 声明代码采用 MIT 开源许可证,允许自由使用、修改和分发代码。
pragma solidity ^0.8.26;
// 指定 Solidity 编译器版本必须大于或等于 0.8.26 并且小于 0.9.0。
// `pragma` 指令确保合约使用兼容的编译器版本,`^0.8.26` 表示支持 0.8.26 或更高版本(但不超过 0.9.0)。
contract Mapping {
// 定义一个名为 `Mapping` 的智能合约。
// 合约是一个运行在以太坊区块链上的程序,包含数据(状态变量)和逻辑(函数)。
// 这个合约的目的是展示如何使用简单的映射(mapping)来存储和操作键值对。
// Mapping from address to uint
// 从地址到无符号整数的映射
mapping(address => uint256) public myMap;
// 声明一个名为 `myMap` 的映射,键类型是 `address`(以太坊地址),值类型是 `uint256`(无符号整数)。
// `public` 表示映射可以被外部访问,Solidity 会自动生成一个 getter 函数(`myMap(address)`),返回对应地址的值。
// 存储在区块链上,未初始化的键返回默认值 0。
function get(address _addr) public view returns (uint256) {
// 定义一个名为 `get` 的公共函数,接受一个参数 `_addr`(类型为 `address`)。
// `public` 表示函数可以被外部调用。
// `view` 表示函数只读取区块链状态,不修改,链下调用免费。
// 返回值类型为 `uint256`,表示对应地址在 `myMap` 中的值。
// Mapping always returns a value.
// If the value was never set, it will return the default value.
// 映射总是返回一个值。
// 如果该值从未被设置,将返回默认值。
return myMap[_addr];
// 返回 `myMap` 中键 `_addr` 对应的值。
// 如果 `_addr` 未设置值,返回 `uint256` 的默认值 0。
}
function set(address _addr, uint256 _i) public {
// 定义一个名为 `set` 的公共函数,接受两个参数:
// `_addr`(类型为 `address`):映射的键。
// `_i`(类型为 `uint256`):要设置的值。
// `public` 表示函数可以被外部调用。
// 没有 `view` 或 `pure`,表示函数会修改区块链状态,需消耗 Gas。
// Update the value at this address
// 更新该地址的值
myMap[_addr] = _i;
// 将 `myMap` 中键 `_addr` 对应的值更新为 `_i`。
// 修改映射会更新区块链存储,消耗 Gas。
}
function remove(address _addr) public {
// 定义一个名为 `remove` 的公共函数,接受一个参数 `_addr`(类型为 `address`)。
// `public` 表示函数可以被外部调用。
// 没有 `view` 或 `pure`,表示函数会修改区块链状态,需消耗 Gas。
// Reset the value to the default value.
// 将值重置为默认值。
delete myMap[_addr];
// 使用 `delete` 操作符将 `myMap` 中键 `_addr` 对应的值重置为默认值(`uint256` 的默认值 0)。
// 修改区块链状态,消耗 Gas。
}
}
contract NestedMapping {
// 定义一个名为 `NestedMapping` 的智能合约。
// 这个合约的目的是展示嵌套映射(mapping 嵌套 mapping)的使用。
// Nested mapping (mapping from address to another mapping)
// 嵌套映射(从地址到另一个映射的映射)
mapping(address => mapping(uint256 => bool)) public nested;
// 声明一个名为 `nested` 的嵌套映射:
// - 外层映射的键是 `address`(以太坊地址)。
// - 内层映射的键是 `uint256`(无符号整数),值是 `bool`(布尔值)。
// `public` 表示映射可以被外部访问,Solidity 会生成 getter 函数(`nested(address, uint256)`)。
// 存储在区块链上,未初始化的键返回默认值 `false`。
function get(address _addr1, uint256 _i) public view returns (bool) {
// 定义一个名为 `get` 的公共函数,接受两个参数:
// `_addr1`(类型为 `address`):外层映射的键。
// `_i`(类型为 `uint256`):内层映射的键。
// `public` 表示函数可以被外部调用。
// `view` 表示函数只读取区块链状态,不修改,链下调用免费。
// 返回值类型为 `bool`,表示嵌套映射中的值。
// You can get values from a nested mapping
// even when it is not initialized
// 你可以从嵌套映射中获取值,即使它未被初始化
return nested[_addr1][_i];
// 返回 `nested` 中键 `_addr1` 的内层映射中键 `_i` 对应的值。
// 如果 `_addr1` 或 `_i` 未设置值,返回 `bool` 的默认值 `false`。
}
function set(address _addr1, uint256 _i, bool _boo) public {
// 定义一个名为 `set` 的公共函数,接受三个参数:
// `_addr1`(类型为 `address`):外层映射的键。
// `_i`(类型为 `uint256`):内层映射的键。
// `_boo`(类型为 `bool`):要设置的值。
// `public` 表示函数可以被外部调用。
// 没有 `view` 或 `pure`,表示函数会修改区块链状态,需消耗 Gas。
nested[_addr1][_i] = _boo;
// 将 `nested` 中键 `_addr1` 的内层映射中键 `_i` 对应的值更新为 `_boo`。
// 修改区块链状态,消耗 Gas。
}
function remove(address _addr1, uint256 _i) public {
// 定义一个名为 `remove` 的公共函数,接受两个参数:
// `_addr1`(类型为 `address`):外层映射的键。
// `_i`(类型为 `uint256`):内层映射的键。
// `public` 表示函数可以被外部调用。
// 没有 `view` 或 `pure`,表示函数会修改区块链状态,需消耗 Gas。
delete nested[_addr1][_i];
// 使用 `delete` 操作符将 `nested` 中键 `_addr1` 的内层映射中键 `_i` 对应的值重置为默认值(`bool` 的默认值 `false`)。
// 修改区块链状态,消耗 Gas。
}
}
Mapping
和 NestedMapping
是两个简单的智能合约,展示了 Solidity 中的映射(mapping)和嵌套映射的使用:
- Mapping 合约:实现一个简单的映射,从以太坊地址 (
address
) 映射到无符号整数 (uint256
),并提供获取、设置和删除值的函数。 - NestedMapping 合约:实现一个嵌套映射,从地址 (
address
) 映射到另一个映射(从uint256
到bool
),同样提供获取、设置和删除值的函数。
代码做什么?
Mapping 合约
- 映射
myMap
:- 存储一个从
address
到uint256
的键值对。 - 例如,可以记录每个地址的余额(如
myMap[0x123...] = 100
)。 - 未设置的键(例如
myMap[0x456...]
)返回默认值 0。
- 存储一个从
- 函数
get
:- 输入一个地址
_addr
,返回myMap
中对应的值。 - 因为是
view
函数,链下调用免费。
- 输入一个地址
- 函数
set
:- 输入地址
_addr
和值_i
,更新myMap
中对应键的值。 - 修改区块链状态,消耗 Gas。
- 输入地址
- 函数
remove
:- 输入地址
_addr
,将myMap
中对应键的值重置为 0。 - 修改区块链状态,消耗 Gas。
- 输入地址
NestedMapping 合约
- 嵌套映射
nested
:- 存储一个从
address
到另一个映射的键值对,内层映射从uint256
到bool
。 - 例如,可以记录每个地址的某些编号是否被标记(
nested[0x123...][1] = true
)。 - 未设置的键返回默认值
false
。
- 存储一个从
- 函数
get
:- 输入地址
_addr1
和编号_i
,返回nested
中对应的布尔值。 - 因为是
view
函数,链下调用免费。
- 输入地址
- 函数
set
:- 输入地址
_addr1
、编号_i
和布尔值_boo
,更新nested
中对应键的值。 - 修改区块链状态,消耗 Gas。
- 输入地址
- 函数
remove
:- 输入地址
_addr1
和编号_i
,将nested
中对应键的值重置为false
。 - 修改区块链状态,消耗 Gas。
- 输入地址
关键点:
- 映射的特点:
- 映射是键值对存储,类似于字典,键快速定位值。
- 未设置的键返回类型的默认值(
uint256
为 0,bool
为false
)。 - 映射存储在区块链的
storage
中,修改需要 Gas。
- 嵌套映射:
- 允许更复杂的数据结构,例如多层键值关系。
- 语法为
mapping(keyType1 => mapping(keyType2 => valueType))
。 - 同样不可迭代,未初始化的键返回默认值。
- 不可迭代:
- 无法直接遍历映射的所有键值对。
- 如果需要迭代,需额外维护一个数组存储键。
- Gas 成本:
- 部署合约时,初始化映射(即使为空)需要 Gas(存储合约代码)。
- 调用
set
或remove
修改映射,消耗 Gas(更新区块链存储)。 - 调用
get
是view
操作,链下调用免费。
- 用途:
- 映射常用于:
- 存储用户余额(
address => uint256
)。 - 记录权限(
address => bool
)。 - 管理复杂关系(嵌套映射,例如
address => uint256 => bool
)。
- 存储用户余额(
- 映射常用于:
映射的注意事项
- 默认值:
- 映射总是返回一个值,未初始化的键返回类型默认值(
uint256
为 0,bool
为false
)。 - 无需显式初始化,节省存储空间。
- 映射总是返回一个值,未初始化的键返回类型默认值(
- 不可迭代:
- 无法遍历映射的所有键值对,需额外维护键列表(例如数组)。
- 迭代需求可能增加 Gas 成本(数组存储和遍历)。
- Gas 优化:
- 写入映射(
set
)和删除(remove
)消耗 Gas,尽量减少不必要的操作。 - 嵌套映射的每次写入涉及多层存储,Gas 成本更高。
- 读取映射(
get
)链下免费,适合频繁查询。
- 写入映射(
- 安全性:
- 确保键的正确性(例如,检查地址是否有效)。
public
映射对所有人可见,敏感数据需谨慎存储。- 避免在循环中频繁写入映射,可能导致 Gas 超支。
- 嵌套映射:
- 适合复杂数据结构,但增加代码复杂度和 Gas 成本。
- 确保内层映射的键类型适合用例(例如
uint256
用于编号)。