university.alchemy8
Your Goal: Vote Changing 你的目标:更改投票
Modify the castVote function to allow voters to change their vote on a particular proposal.
修改castVote函数,允许投票者更改他们对特定提案的投票。
答案
1 | |
问题
我不太懂这两行:
1 | |
能不能解释的详细一点
好的,我来详细解释一下这两行代码。
1. mapping(uint => mapping(address => bool)) public hasVoted;
这行代码声明了一个 二维映射。我们来分解这个声明:
mapping(uint => mapping(address => bool)):uint: 外层mapping的键是一个uint类型,即提案的编号(proposalId)。mapping(address => bool): 内层mapping的键是一个address类型,表示每个选民(即每个地址)。对应的值是一个bool类型,表示该选民是否已经对该提案投过票。true表示已经投过票,false表示没有投票。
所以,这个映射的意思是:对于每个提案(通过 uint proposalId 标识),我们记录每个选民(address)是否已经投过票。它的作用是 检查选民是否已经投票。
举个例子:
假设有两个提案,分别编号为 0 和 1。如果一个选民地址 0x123... 对提案 0 投过票,hasVoted[0][0x123...] 的值为 true。如果没有投票,则为 false。
2. mapping(uint => mapping(address => bool)) public votes;
这行代码声明了另一个 二维映射,与上面的 hasVoted 类似,但作用不同:
mapping(uint => mapping(address => bool)): 还是和上面一样,外层的uint代表提案的编号(proposalId),内层是address,表示选民地址,值是bool,表示选民对某个提案的投票情况。- 这个
bool值的含义是:true表示选民支持该提案(投了赞成票),false表示选民反对该提案(投了反对票)。
- 这个
所以,这个映射的作用是 记录每个选民在每个提案上的投票支持情况。
举个例子:
假设有两个提案,编号分别是 0 和 1。选民地址 0x123... 对提案 0 投了赞成票,那么 votes[0][0x123...] 的值就是 true。如果他投了反对票,那么 votes[0][0x123...] 就是 false。
总结这两个映射的作用:
hasVoted映射:- 用来记录每个选民是否对特定提案投过票。
- 主要用于防止选民重复投票。如果
hasVoted[proposalId][msg.sender] == true,说明该选民已经对该提案投过票,不能再次投票。
votes映射:- 用来记录每个选民在某个提案上投票的具体结果(支持或反对)。
- 如果
votes[proposalId][msg.sender] == true,说明该选民支持该提案;如果votes[proposalId][msg.sender] == false,说明该选民反对该提案。 - 这个映射允许我们在选民更改投票时进行更新(例如,选民从支持变成反对,或反之)。
为什么需要两个映射?
hasVoted主要用于判断选民是否已经投过票。这个映射只关心是否投过票,而不关心具体的投票内容(支持或反对)。votes主要用于存储选民在某个提案上的投票结果(支持或反对)。这可以用来在选民更改投票时更新票数。
因此,hasVoted 映射确保每个选民只能投一次票,而 votes 映射用于记录投票的具体选择,并在选民更改投票时更新。
普通mapping
| Key键值对 | value |
|---|---|
| address地址 | bool布尔值 |
编写方法:mapping(address => bool) public members;
members是这个映射的名字members[address] = false; 这样赋值
两层映射
好的,下面是一个符合你要求的表格,展示了如何使用 两层映射 来存储数据:
两层映射(二维映射)示例表格:
| Key1 (外层映射的键) | Key2 (内层映射的键) | Value (内层映射的值) |
|---|---|---|
address |
address |
bool |
示例代码:
1 | |
在这个映射中:
- 外层的
address代表某个选民(或投票者)的地址。 - 内层的
address代表投票的目标地址(如提案的地址或其他合约地址)。 bool值表示该选民是否已经对目标地址(如某个提案)投过票,true表示已经投过,false表示没有投过。
示例赋值:
1 | |
解释:
- 外层
address表示投票者的地址(如某个选民)。 - 内层
address表示该选民投票的目标地址(如提案的地址)。 bool表示该选民是否对这个目标地址投过票,true表示已投票,false表示没有投票。
总结:
- 两层映射 用于跟踪每个选民对多个目标的投票状态,可以在任何场景中使用,比如提案投票、权限控制等。
这两个address可以不可以替换位置?
是的,这两个 address 是可以交换位置的,但这样做会改变映射的含义。
具体来说,如果你交换外层和内层 address 的位置,你将改变映射的逻辑含义。让我们分别看一下两种情况。
1. 当前的映射(外层 address 是投票者,内层 address 是目标地址):
1 | |
含义:
- 外层
address代表投票者的地址(例如,某个选民)。 - 内层
address代表目标地址,通常是提案或其他对象的地址。 bool值表示该投票者是否对该目标地址(如提案)投过票。
例子:
votes[0x123...][0xabc...] = true;- 选民
0x123...已经对提案0xabc...投了票。
- 选民
2. 交换位置后的映射(外层 address 是目标地址,内层 address 是投票者):
1 | |
含义:
- 外层
address代表目标地址(例如,提案地址)。 - 内层
address代表投票者的地址(例如,选民)。 bool值表示该选民是否已经对该目标地址(如提案)投票。
例子:
votes[0xabc...][0x123...] = true;- 提案
0xabc...已经收到选民0x123...的投票。
- 提案
对比:
- 当前版本:外层
address是投票者,内层address是目标地址,适用于记录每个投票者的投票情况。 - 交换版本:外层
address是目标地址,内层address是投票者,适用于记录每个目标(如提案)的投票情况,能快速查看某个提案收到哪些选民的投票。
总结:
- 交换位置是 可以的,但这样会改变映射的逻辑含义。原本的结构是按“投票者 → 目标地址”来记录的,交换后会变成“目标地址 → 投票者”。
- 具体选择哪种方式取决于你要存储和查询的数据类型及用途。如果你关心的是每个投票者对哪些提案投票,使用原来的结构;如果你关心的是每个提案收到哪些投票,使用交换后的结构。
我记不住用哪种方式,该怎么办
如果你很难记住两种方式的具体用法,这里有一些方法和技巧可以帮助你做出决定并记住正确的方式:
1. 根据需求来选择
如果你关心的是每个投票者的投票情况,例如:你想知道每个选民对哪个提案投了票,那么应该使用 **
mapping(address => mapping(address => bool))**。这种方式能方便地查询某个选民在所有提案中的投票状态。- 示例应用:比如一个选民
0x123...是否已经投票,或者他们是否支持提案0xabc...。
- 示例应用:比如一个选民
如果你关心的是每个提案的投票情况,例如:你想知道每个提案收到哪些选民的投票,那么应该使用 **
mapping(address => mapping(address => bool))**,将外层address设置为提案地址,内层address设置为投票者的地址。这样可以快速查询某个提案得到了哪些选民的支持或反对。- 示例应用:比如查看提案
0xabc...有没有选民0x123...投票。
- 示例应用:比如查看提案
2. 通过函数命名或注释提醒自己
函数命名:通过函数命名来帮助自己记住是哪种方式。
- 例如:如果你是处理每个提案的投票情况,可以使用
castVoteOnProposal(address proposalAddress)。 - 如果你是处理每个投票者的投票情况,可以命名为
hasVoted(address voterAddress)。
- 例如:如果你是处理每个提案的投票情况,可以使用
注释:在代码中加入注释,明确你使用的映射的含义,帮助自己快速理解:
1
2
3// 外层是提案地址,内层是投票者地址
// 记录投票者是否对特定提案投票
mapping(address => mapping(address => bool)) public votesOnProposals;
3. 图示方法
图示帮助记忆:你可以用图示的方式帮助自己记住映射的含义。比如画一个图,标明外层映射和内层映射代表的内容:
- 外层:
address代表选民或提案。 - 内层:
address代表提案或选民。
- 外层:
例如:
- 外层
address→ 投票者,内层address→ 提案:votes[voter][proposal] - 外层
address→ 提案,内层address→ 投票者:votes[proposal][voter]
- 外层
4. 记忆法:使用“谁对谁投票”
谁对谁投票:你可以用这个简单的规则来记忆:
- **”外层是投票者,内层是提案”**:如果你关心的是每个选民对哪个提案投票,就用外层
address为投票者,内层address为提案。
例如:votes[voter][proposal]。 - **”外层是提案,内层是投票者”**:如果你关心的是每个提案的投票情况,外层
address为提案,内层address为投票者。
例如:votes[proposal][voter]。
- **”外层是投票者,内层是提案”**:如果你关心的是每个选民对哪个提案投票,就用外层
5. 总结规则:
- 外层
address是谁?如果是投票者,外层就是投票者的地址;如果是提案,外层就是提案的地址。 - 内层
address是谁?如果是投票者,内层就是投票者的地址;如果是提案,内层就是提案的地址。
最后,最重要的提示:
无论你选择哪种方式,最关键的是 明确你需要追踪的数据,然后根据需求选择适合的映射结构。你不需要记住所有细节,只需要记住 “你要存什么数据” 和 **”你希望查询什么数据”**,然后根据这些来选择映射的结构。
希望这些技巧能帮你更好地理解和记住两层映射的使用!
Your Goal: Proposal Created & Vote Cast Events
你的目标:提案创建和投票事件
Create an event ProposalCreated which takes a single argument: a uint proposal ID. Emit this event whenever a new Proposal struct is created.
创建一个事件ProposalCreated,它接受一个参数:一个uint类型的提案ID。每当创建新的Proposal结构体时,就触发该事件。
Create an event VoteCast which takes two arguments: a uint proposal ID and an address for the voter’s address. Emit this event any time a new vote is cast.
创建一个名为VoteCast的事件,它接受两个参数:一个uint类型的提案ID和一个address类型的投票者地址。每当有新的投票被投出时,就触发此事件。
正确答案
1 | |
知识点
以下是对 emit ProposalCreated(proposals.length - 1); 语句的详细解释:
语句解析:
**
emit**:emit是 Solidity 中用来触发事件的关键字。当使用emit时,它表示事件已经发生,并将事件数据记录到区块链的交易日志中。这些日志对外部消费者(如前端应用程序)是可访问的,可以用于追踪和通知。**
ProposalCreated**:
这是正在触发的事件。在你的合约中,ProposalCreated是一个自定义事件,它在创建新提案时被触发,并且接收一个proposalId(类型为uint)作为参数:1
event ProposalCreated(uint proposalId);这个事件用于在创建新的提案时触发,并记录提案的 ID(即
proposalId)。**
proposals.length - 1**:proposals是一个数组,用于存储所有已创建的提案。proposals.length返回数组中当前的提案数量,即已经创建的提案数量。由于 Solidity 中的数组是零索引的,因此数组中的最后一个元素的索引是
proposals.length - 1。因此,proposals.length - 1返回的是最新添加的提案的索引,也就是当前交易中新创建的提案的proposalId。
发生了什么:
- 每当创建一个新的提案时,它会被添加到
proposals数组中。 - 在提案添加后,
proposals.length会反映数组的新长度(比之前的长度大 1)。 - 为了获取新创建提案的 索引(即
proposalId),我们将数组的长度减去 1,因为 Solidity 中的数组是从 0 开始的。
例如:
- 如果数组中已有 2 个提案(
proposals.length == 2),那么添加一个新的提案后,数组的长度将变为 3,但新添加的提案的索引是 2(proposals.length - 1 = 2)。
因此,emit ProposalCreated(proposals.length - 1) 会触发一个包含新提案索引的事件。
示例工作流程:
创建提案前:
假设proposals数组中没有任何提案:1
proposals.length == 0创建提案:
在调用newProposal函数后,新的提案会被添加到proposals数组中:1
proposals.push(newProposalInstance);现在,
proposals.length == 1(数组中有一个提案)。触发事件:
- 语句
emit ProposalCreated(proposals.length - 1)会触发事件,并将proposalId作为参数传递。在这种情况下,proposalId将是 0,因为新创建的提案的索引是 0。 - 触发的事件将是:
1
ProposalCreated(0);- 语句
下一个提案:
如果再添加一个提案,proposals.length将变为 2,触发的事件将有proposalId为 1(即proposals.length - 1 = 1)。
为什么这很重要?
触发的事件对外部交互非常有用。通过发出 proposalId,外部消费者(如前端应用程序或其他智能合约)可以轻松追踪哪个提案被创建,并基于这些信息采取进一步的操作。
总结:
emit ProposalCreated(proposals.length - 1)触发了ProposalCreated事件,并将proposalId(即新创建提案的索引)作为参数传递。- 索引是
proposals.length - 1,因为数组是从 0 开始的。
Your Goal: Members 你的目标:成员
Create a public constructor which takes an array of address. These addresses, plus the deployer of the function, should all be allowed to create new proposals and vote on those proposals.
创建一个公共的constructor,该函数接受一个address数组作为参数。这些地址以及该函数的部署者都应被允许创建新提案并对这些提案进行投票。
If anyone else attempts to create a proposal or vote, the call should be reverted.
如果有其他人试图创建提案或进行投票,调用应被回滚。
是先写还是后写constructor,在solidity里面有顺序要求吗?
在 Solidity 中,constructor 的定义顺序没有强制要求,既可以写在合约代码的开头,也可以写在其他函数/状态变量之后,编译器都能正常识别。
核心结论
Solidity 对 constructor 的位置没有语法限制,以下两种写法均合法且功能完全一致:
写法1:constructor 写在开头(推荐风格)
1 | |
写法2:constructor 写在末尾(语法合法但不推荐)
1 | |
关键补充说明
- 唯一性限制:一个合约只能有一个
constructor(Solidity 0.8.0+ 不支持重载 constructor,早期版本曾允许但已废弃)。 - 可见性关键字(可选):Solidity 0.7.0 之前,
constructor可加public/internal关键字(默认public);0.7.0 及之后,public关键字被废弃,默认即为公共构造函数,internal仍可使用(表示合约仅可被继承,不能直接部署)。 - 最佳实践:虽然语法无限制,但推荐将 constructor 写在状态变量定义之后、普通函数之前。原因是:
- 构造函数是合约初始化逻辑,位置靠前更符合“先初始化、后定义业务逻辑”的代码逻辑;
- 提高可读性,其他开发者可快速找到合约的初始化参数和逻辑。
特殊场景:继承合约的 constructor 顺序
如果涉及合约继承,父合约的 constructor 会在子合约 constructor 执行前自动调用(与代码中定义的顺序无关),但子合约需显式指定父合约的构造参数(或父合约有默认构造函数):
1 | |
总结
- 语法层面:
constructor的定义位置无强制要求,任意顺序都能编译通过; - 工程实践:推荐放在状态变量之后、业务函数之前,提升代码可读性;
- 继承场景:父合约 constructor 优先执行,子合约需显式传递父合约构造参数。
为什么写memory不写calldata?
1 | |
在 Solidity 中,constructor 的引用类型参数(如数组、结构体、字符串) 选择 memory 而非 calldata,核心原因是:构造函数的参数存储位置规则与普通外部函数不同,calldata 对构造函数是非法的,只有 memory 是合法选择。
先明确核心结论
对于 constructor(address[] ...) 这类参数:
- ✅ 必须用
memory(Solidity 0.5+ 语法要求); - ❌ 不能用
calldata(编译报错,calldata不适用于构造函数)。
关键原因:calldata 的适用场景限制
calldata 是 Solidity 中只读、临时的存储位置,专门用于:
- 普通
external函数的引用类型参数(如function f(address[] calldata) external); - 普通
public函数的引用类型参数(0.8.19+ 版本支持,之前版本仅external支持)。
它的设计目的是直接读取交易的输入数据(calldata 区域),不复制到内存,节省gas,但有一个硬性限制:calldata 不能用于构造函数、内部函数(internal)、私有函数(private)的参数—— 因为这些场景的参数传递逻辑不经过“外部交易的 calldata 区域”,编译器不允许指定 calldata 存储位置。
为什么 memory 对构造函数是合法的?
memory 是 Solidity 中临时、可修改的存储位置,适用于:
- 构造函数的引用类型参数(唯一合法选择);
- 函数内部临时创建的引用类型变量;
- 普通函数(
internal/private)的引用类型参数。
构造函数的参数传递逻辑:
- 部署合约时,你传入的地址数组(如
["0x123", "0x456"])会被编码为交易的输入数据; - 编译器会自动将这些输入数据复制到
memory中,供构造函数读取; - 你在构造函数中对数组的操作(如循环遍历、读取元素),本质是操作
memory中的副本,语法合法且高效。
对比:普通外部函数 vs 构造函数
为了更清晰,用表格对比两者的参数存储位置选择:
| 场景 | 引用类型参数(如 address[])允许的存储位置 |
推荐选择 | 原因 |
|---|---|---|---|
构造函数 constructor |
仅 memory |
memory |
calldata 对构造函数非法 |
普通 external 函数 |
calldata / memory |
calldata |
只读场景下更省gas(无需复制到内存) |
普通 internal 函数 |
仅 memory |
memory |
calldata 不适用于内部函数 |
示例:普通外部函数可用 calldata,但构造函数不行
1 | |
补充:为什么不用 storage?
storage 是持久化存储位置(对应合约的状态变量,存储在链上),不能用于函数/构造函数的参数—— 因为参数是临时输入数据,不是合约的持久化状态,指定 storage 会编译报错。
总结
构造函数中写 address[] memory proposals 而非 calldata 的核心原因:
calldata仅适用于普通external(或 0.8.19+ 的public)函数,对构造函数是非法语法;memory是构造函数引用类型参数的唯一合法选择,编译器会自动处理输入数据到内存的复制;- 这种写法完全符合 Solidity 语法规范,且不会额外增加gas(构造函数的参数无论用什么存储位置,都会经过一次复制,
memory是最优解)。
简单记:构造函数的引用类型参数,直接用 memory 准没错。