主页 > imtoken钱包国际版下载 > 以太坊开发实战学习——高等Solidity理论(五)
以太坊开发实战学习——高等Solidity理论(五)
在数组后面加上memory关键字表示数组只在内存中创建,不需要写入外存,在函数调用结束时解散。 与在程序结束时将数据存入存储相比,内存操作可以大大节省gas开销——把这个数组放在视图中根本不花钱。
下面是一个声明内存数组的例子:
function getArray() external pure returns(uint[]) {
// 初始化一个长度为3的内存数组
uint[] memory values = new uint[](3);
// 赋值
values.push(1);
values.push(2);
values.push(3);
// 返回数组
return values;
}
这个小例子展示了一些语法规则。 在下一章中,我们将使用一个实际用例来展示它如何与 for 循环结合使用。
注意:必须使用长度参数(在本例中为 3)创建内存数组。 目前不支持 array.push() 等方法调整数组大小,未来版本可能支持修改长度。
实践练习
我们将创建一个名为 getZombiesByOwner 的函数,它以 uint[] 数组的形式返回特定用户拥有的所有僵尸。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
// 在这里开始
uint[] memory result = new uint[](ownerZombieCount[_ owner]);
return result;
}
}
4.for循环
在上一篇博文中,我们提到函数中使用的数组是在运行时通过for循环实时构建在内存中的,而不是预先构建在存储中。
你为什么要这样做?
为了实现getZombiesByOwner函数,一个“无脑”的方案是在ZombieFactory中存储“owner”和“zombie army”的映射。
mapping (address => uint[]) public ownerToZombies
然后每次我们创建一个新的僵尸时,执行ownerToZombies[owner].push(zombieId) 将其添加到所有者的僵尸数组中。 而 getZombiesByOwner 函数也很简单:
function getZombiesByOwner(address _owner) external view returns (uint[]) {
return ownerToZombies[_owner];
}
这种方法有问题
方法很简单。 但是如果我们需要一个将僵尸转移到另一个主人的函数(我们肯定会在后面的课程中实现),会发生什么?
这个“改变主人”功能应该做:
但是第三步太贵了! 因为每移动一个僵尸,我们都要进行一次写操作。 如果一个 master 有 20 个僵尸并且第一个被移除,我们必须执行 19 次写入以保持数组的顺序。
由于写入存储是 Solidity 中最消耗 gas 的操作之一,因此每次调用 remaster 函数都非常昂贵。 更糟糕的是,每次调用的 gas 成本都不同! 它还取决于用户在原主力部队中的丧尸数量,以及被移除的丧尸所在的位置。 让用户不知道他们应该支付多少gas。
注:当然,我们也可以将数组中最后一个僵尸向前移动,填补空位,将数组长度减一。 但是这样进行的每一次交易,都会改变丧尸大军的秩序。
由于从外部可以自由调用视图函数,我们也可以在getZombiesByOwner函数中使用for循环遍历整个僵尸数组,挑选出属于某个所有者的僵尸来构建僵尸数组。 那么我们的传输函数会便宜很多,因为我们不需要在 shuffle store 中对僵尸数组重新排序,这样总体上会更便宜,虽然有点违反直觉。
使用 for 循环
for 循环的语法在 Solidity 和 JavaScript 中是相似的。
让我们看一个创建偶数数组的例子:
function getEvens() pure external returns(uint[]) {
uint[] memory evens = new uint[](5);
// 在新数组中记录序列号
uint counter = 0;
// 在循环从1迭代到10:
for (uint i = 1; i <= 10; i++) {
// 如果 `i` 是偶数...
if (i % 2 == 0) {
// 把它加入偶数数组
evens[counter] = i;
//索引加一, 指向下一个空的‘even’
counter++;
}
}
return evens;
}
此函数将返回形状为 [2,4,6,8,10] 的数组。
实践练习
我们回到 getZombiesByOwner 函数并使用 for 循环遍历 DApp 中的所有僵尸,将给定的“用户 ID”与每个僵尸的“所有者”进行比较,并在函数返回中间之前将它们推送到我们的结果数组。
就是这样——这个函数返回 _owner 拥有的僵尸数组,无需支付一分钱的 gas。
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
// 在这里开始
uint counter = 0;
for(uint i = 0; i < zombies.length; i++) {
if(zombieToOwner[i] == _owner)
{
result[counter] = i;
counter ++;
}
}
return result;
}
}
5、应付款
到目前为止,我们只接触了几个函数修饰符。 很难记住所有内容,所以这里有一个概述:
这些修饰符可以同时应用于函数定义:
function test() external view onlyOwner anotherModifier { /* ... */ }
在本章中,我们将了解一种新的应付修饰符。
应付修饰符
支付方法是让 Solidity 和以太坊如此酷的部分原因——它们是接受以太币的特殊功能。
先放手吧。 当您在普通 Web 服务器上调用 API 函数时,您不能使用您的函数发送美元——也不能发送比特币。
但在以太坊中遍历以太坊节点,因为钱(Ether)、数据(交易有效载荷)、合约代码本身都存在于以太坊中。 您可以调用一个函数并同时支付另一个合约。
这允许很多有趣的逻辑,比如要求合约支付一定数量的钱来运行一个功能。
例子
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}
在这里,msg.value 是一种查看向合约发送了多少以太币的方法,以太币是一个内置单位。
这里发生的是有人会从 web3.js(从 DApp 的前端)调用这个函数,就像这样:
// 假设 `OnlineStore` 在以太坊上指向你的合约:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
请注意值字段,JavaScript 调用指定发送多少 (0.001) 以太币。 如果您将交易想象成一个信封,那么您发送给函数的参数就是信件的内容。 增加价值很像把钱装进信封——信件的内容和钱会同时寄给收件人。
注意:如果一个函数没有被标记为 payable 而你尝试使用上述方法发送以太币,该函数将拒绝你的交易。
实践练习
让我们在僵尸游戏中创建一个支付函数。
假设在我们的游戏中,玩家可以通过支付 ETH 来升级他们的僵尸。 ETH 将存储在您拥有的合约中 - 一个简单明了的示例,向您展示您可以通过自己的游戏赚钱。
然后它应该增加僵尸的级别: zombies[_zombieId].level++ 。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
// 1\. 在这里定义 levelUpFee
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 2\. 在这里插入 levelUp 函数
function levelUp(uint _zombieId) external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
6.提现
在上一节中,我们学习了如何向合约发送以太币,那么发送之后会发生什么?
在你发送以太币之后,它将被存储在合约的以太坊账户中,在那里它会被冻结——除非你添加一个从合约中提取以太币的功能。
您可以编写一个函数从合约中提取以太币,如下所示:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
请注意,我们在 Ownable 合约中使用了 owner 和 onlyOwner,假设它已经被导入。
你可以通过 transfer 函数将以太币发送到一个地址,然后 this.balance 将返回当前合约中存储了多少以太币。 因此,如果 100 个用户每人支付给我们 1 个以太币,则 this.balance 将为 100 个以太币。
您可以向任何以太坊地址转账。 例如,你可以有一个函数在 msg.sender 多付时退款:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
或者在一个卖家和卖家的合约中,你可以存储卖家的地址,当有人从它那里购买东西时遍历以太坊节点,将买家支付的钱发送给它的seller.transfer(msg.value)。
有很多例子可以说明是什么让以太坊上的编程如此酷——你可以拥有一个不受任何人控制的去中心化市场。
实践练习
一种。 创建一个名为 setLevelUpFee 的函数,它接收一个参数 uint _fee,它是外部的,使用修饰符 onlyOwner。
b. 此函数应将 levelUpFee 设置为等于 _fee。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 1\. 在这里创建 withdraw 函数
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
// 2\. 在这里创建 setLevelUpFee 函数
function setLevelUpFee(uint _fee) external onlyOwner {
levelUpFee = _fee;
}
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
七、综合应用
我们创建一个新的攻击函数合约,将代码放入一个新文件中,并导入之前的合约。
让我们创建一个新合约。 熟能生巧。
如果您不记得如何操作,请查看 zombiehelper.sol - 但最好先尝试一下以检查您有什么。
zombiebattle.sol
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
}
八、随机数
优秀的游戏需要一些随机元素,那么我们如何在Solidity中生成随机数呢?
真正的答案是你不能,或者至少,你不能安全地做到这一点。
让我们看看为什么
使用keccak256制作随机数
Solidity 中最好的随机数生成器是 keccak256 哈希函数。
我们可以像这样生成一些随机数
// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
此方法首先获取现在的时间戳、msg.sender 和一个自动递增的随机数(一个只使用一次的数字,因此我们不会为相同的输入值多次调用哈希函数)。
然后用keccak把输入的值转成hash值,再把hash值转成uint,再用0取最后两位,生成一个0到100之间的随机数。
这种方法很容易被不诚实的节点攻击
在以太坊上,当您调用合约上的函数时,您会将其广播到网络上的一个或多个交易节点。 网络上的节点会收集很多交易,争取最先解决计算密集型数学问题作为“工作量证明”,然后将“工作量证明”(Proof of Work,PoW)连同交易一起公布为在互联网上屏蔽。
一旦一个节点解决了一个PoW,其他节点就停止尝试解决这个PoW,并验证其他节点的交易列表有效,然后接受这个节点并尝试解决下一个节点。
这使得我们的随机数函数可以被利用
假设我们有一个抛硬币合同——正面朝上你赢双倍钱,反面朝上你输掉所有的钱。 如果用上面的方法判断是正面还是反面(随机>=50算正面,随机<50算反面)。
如果我正在运行一个节点,我可以只将交易发布到我自己的节点而不共享它。 我可以运行抛硬币的方法来查看我的得失——如果我输了,我不会将这笔交易包含在我要解决的下一个区块中。 我可以继续运行此方法,直到我赢得一次抛硬币并解决下一个区块,然后获利。
那么我们如何在以太坊上安全地生成随机数呢?
由于区块链的全部内容对所有参与者都是透明的,这使得这个问题变得困难,而且它的解决方案超出了本课程的范围,你可以阅读这个 StackOverflow 讨论以获得一些想法。 一种方法是使用预言机访问以太坊区块链之外的随机数函数。
当然,由于网络上成千上万的以太坊节点都在竞争解决下一个区块,所以我成功解决下一个区块的机会很低。 开发这种货币化方法需要我们巨大的计算资源——但如果奖励异常高(比如我可以在抛硬币功能中赢得 1 亿),那是非常值得的。
所以尽管这种方法在以太坊上并不安全,但在实践中,除非我们的随机函数上有很多钱,否则你游戏的用户通常没有足够的资源来攻击。
由于在本教程中我们只是为了演示目的而编写一个简单的游戏并且没有投入真金白银,所以我们决定接受这个不足并使用这个简单的随机数生成器。 但请记住,这并不安全。
实践练习
让我们实现一个随机数生成器函数来计算战斗的结果。 虽然这个功能一点都不安全。
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
// 在这里开始
uint randNonce = 0;
function randMod(uint _modulus) internal returns (uint) {
randNonce ++;
return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}
}
9.游戏对战
我们的合约已经有了一些随机性来源,我们可以用它来计算僵尸大战的结果。
我们的僵尸大战看起来像这样:
有很多逻辑要处理,我们将在下一课中分解这些步骤。
实践练习
zombiehelper.sol
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
uint randNonce = 0;
// 在这里创建 attackVictoryProbability
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}
// 在这里创建新函数
function attack(uint _zombieId, uint _targetId) external {
}
}