cryptozombies2

定义与使用interface

Continuing our previous example with NumberInterface, once we’ve defined the interface as:
继续我们之前使用 NumberInterface 的例子,一旦我们定义了如下接口:

1
2
3
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}

We can use it in a contract as follows:
我们可以在合约中这样使用它:

1
2
3
4
5
6
7
8
9
10
11
12
contract MyContract {
address NumberInterfaceAddress = 0xab38...
// ^ The address of the FavoriteNumber contract on Ethereum
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// Now `numberContract` is pointing to the other contract

function someFunction() public {
// Now we can call `getNum` from that contract:
uint num = numberContract.getNum(msg.sender);
// ...and do something with `num` here
}
}

In this way, your contract can interact with any other contract on the Ethereum blockchain, as long they expose those functions as public or external.
通过这种方式,只要其他合约将这些函数暴露为 public 或 external,你的合约就可以与以太坊区块链上的任何其他合约进行交互。

Put it to the test
Let’s set up our contract to read from the CryptoKitties smart contract!

I’ve saved the address of the CryptoKitties contract in the code for you, under a variable named ckAddress. In the next line, create a KittyInterface named kittyContract, and initialize it with ckAddress — just like we did with numberContract above.

实践练习
让我们将合约设置为能够读取 CryptoKitties 智能合约!

我已经在代码中为你保存了 CryptoKitties 合约的地址,放在名为 ckAddress 的变量中。在下一行,创建一个名为 kittyContract 的 KittyInterface,并用 ckAddress 初始化它——就像我们上面使用 numberContract 那样。

处理多个返回值

这个 getKitty 函数是我们看到的第一个返回多个值的例子。让我们来看看如何处理它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}

function processMultipleReturns() external {
uint a;
uint b;
uint c;
// This is how you do multiple assignment:
(a, b, c) = multipleReturns();
}

// Or if we only cared about one of the values:
function getLastReturnValue() external {
uint c;
// We can just leave the other fields blank:
(,,c) = multipleReturns();
}

实践练习
是时候与 CryptoKitties 合约进行交互了!

让我们创建一个从合约中获取猫咪基因的函数:

创建一个名为 feedOnKitty 的函数。该函数应接受 2 个 uint 参数 _zombieId_kittyId,并且应该是一个 public 函数。

该函数应首先声明一个名为 kittyDna 的 uint 变量。

注意:在我们的 KittyInterface 中,genes 是 uint256 类型——但如果你回想一下第 1 课,uint 是 uint256 的别名——它们是一样的。

然后该函数应使用 _kittyId 调用 kittyContract.getKitty 函数,并将 genes 存储在 kittyDna 中。记住——getKitty 会返回很多变量。(确切地说是 10 个——我很贴心,已经帮你数过了!)。但我们只关心最后一个变量 genes。要仔细数好逗号的位置!

最后,该函数应调用 feedAndMultiply,并向它传递 _zombieIdkittyDna 两个参数。

代码解析:

这段代码是 Solidity 中处理函数多个返回值的特殊语法。让我详细解释:

1
2
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);

具体解释:

  • kittyContract.getKitty(_kittyId) 返回 10个值
  • 但我们只需要第10个值(genes)
  • 使用逗号占位符来忽略前9个返回值
  • 只有最后一个位置写了 kittyDna,表示只接收第10个返回值

相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// getKitty 返回的10个值:
(
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes // ← 这是第10个,我们想要的kittyDna
) = kittyContract.getKitty(_kittyId);

// 但我们用逗号语法简化了,只取最后一个

逗号计数:

1
2
( , , , , , , , , , kittyDna)
1 2 3 4 5 6 7 8 9 10
  • 前9个逗号对应前9个返回值(被忽略)
  • 第10个位置是 kittyDna,接收第10个返回值

更清晰的写法(新版本Solidity):

1
2
(,,,,,,,,,uint256 kittyDna) = kittyContract.getKitty(_kittyId);
// 可以直接在括号内声明变量

这种语法让你可以选择性接收多返回值函数的特定值,而不需要处理所有返回值。

第13章:额外功能:猫咪基因

我们的函数逻辑现在已经完成了……但让我们添加一个额外功能。

让我们让由猫咪变成的僵尸拥有一些独特特征,以显示它们是猫僵尸。

为了实现这一点,我们可以在僵尸的DNA中添加一些特殊的猫咪代码。

如果你回想一下第1课,我们目前只使用16位DNA中的前12位来确定僵尸的外观。所以让我们使用最后2位未使用的数字来处理”特殊”特征。

我们将规定猫僵尸的DNA最后两位是99(因为猫有9条命)。所以在我们的代码中,我们会说如果僵尸来自猫咪,那么将DNA的最后两位数字设置为99。

If 条件语句

Solidity 中的 if 语句看起来和 JavaScript 中的一样:

1
2
3
4
5
6
7
function eatBLT(string memory sandwich) public {
// Remember with strings, we have to compare their keccak256 hashes
// to check equality
if (keccak256(abi.encodePacked(sandwich)) == keccak256(abi.encodePacked("BLT"))) {
eat();
}
}

实践练习
让我们在僵尸代码中实现猫咪基因。

首先,我们修改 feedAndMultiply 函数的定义,使其接受第三个参数:一个名为 _species 的字符串,我们将把它存储在内存中。

接下来,在计算出新僵尸的 DNA 后,我们添加一个 if 语句来比较 _species 和字符串 “kitty” 的 keccak256 哈希值。我们不能直接将字符串传递给 keccak256。相反,我们将在左侧传递 abi.encodePacked(_species) 作为参数,在右侧传递 abi.encodePacked("kitty") 作为参数。

在 if 语句内部,我们要将 DNA 的最后两位数字替换为 99。一种实现方法是使用以下逻辑:newDna = newDna - newDna % 100 + 99;

解释:假设 newDna 是 334455。那么 newDna % 100 是 55,所以 newDna - newDna % 100 是 334400。最后加上 99 得到 334499。

最后,我们需要修改 feedOnKitty 内部的函数调用。当它调用 feedAndMultiply 时,在末尾添加参数 “kitty”。

大功告成,你已经完成了第2课!

你可以在右侧查看演示,看看它的实际效果。去吧,我知道你已经等不及翻到页面底部了😉。点击一只小猫进行攻击,看看你得到的新猫僵尸!

JavaScript 实现

当我们准备将这个合约部署到以太坊时,我们只需要编译和部署 ZombieFeeding——因为该合约是我们继承自 ZombieFactory 的最终合约,并且可以访问两个合约中的所有公共方法。

让我们看一个使用 JavaScript 和 web3.js 与我们部署的合约进行交互的示例:

试试看吧!
选择你想要捕食的猫咪。你的僵尸DNA和猫咪的DNA将会结合,你将在你的军队中获得一个新的僵尸!

注意到你新僵尸身上那些可爱的猫腿了吗?这就是最后两位DNA设置为99在起作用 😉

如果你愿意,可以重新开始再试一次。当你得到一个满意的猫僵尸时(你只能保留一个),请继续下一章来完成第2课!

https://cryptozombies.io/en/lesson/2/chapter/15

合约不可变

一旦合约在以太坊中部署,就是不可变的。 所以这是巨大的安全隐患。
如果你的合约代码中存在缺陷,那你就再也没有办法修复了。
你必须告诉你的用户使用一个不同的已修复的合约地址。

外部依赖

在第2课中,我们将 CryptoKitties 合约地址硬编码到我们的 DApp 中。但是如果 CryptoKitties 合约出现漏洞,有人销毁了所有的猫咪,会发生什么?

这种情况不太可能发生,但如果真的发生,它将使我们的 DApp 完全失效——我们的 DApp 指向一个硬编码的地址,而该地址不再返回任何猫咪。我们的僵尸将无法捕食猫咪,我们也无法修改合约来修复这个问题。

因此,拥有允许你更新 DApp 关键部分的函数通常是有意义的。

例如,与其将 CryptoKitties 合约地址硬编码到我们的 DApp 中,不如设置一个 setKittyContractAddress 函数,以便在未来 CryptoKitties 合约发生问题时,我们可以更改这个地址。

实践练习
让我们更新第2课中的代码,使其能够更改 CryptoKitties 合约地址。

删除我们硬编码 ckAddress 的那行代码。

将我们创建 kittyContract 的那行代码改为仅声明变量——即不要将其设置为任何值。

创建一个名为 setKittyContractAddress 的函数。它接受一个参数 _address(一个地址),并且应该是一个 external 函数。

在函数内部,添加一行代码,将 kittyContract 设置为 KittyInterface(_address)

第2章:可拥有合约(Ownable Contracts)

你是否发现了上一章中的安全漏洞?

setKittyContractAddress 是 external 函数,这意味着任何人都可以调用它!也就是说,任何调用该函数的人都能够更改 CryptoKitties 合约的地址,从而破坏我们应用程序对所有用户的服务。

我们确实希望在合约中保留更新这个地址的能力,但我们不希望所有人都能进行更新。

为了处理这种情况,一个常见的做法是让合约成为可拥有的(Ownable)——这意味着它们有一个拥有特殊权限的所有者(也就是你)。

OpenZeppelin 的 Ownable 合约

下面是从 OpenZeppelin Solidity 库中提取的 Ownable 合约。OpenZeppelin 是一个包含安全且经过社区审查的智能合约库,你可以在自己的 DApp 中使用。学完这一课后,我们强烈建议你查看他们的网站以进一步学习!

请仔细阅读下面的合约。你会看到一些我们尚未学习的内容,但别担心,我们稍后会讨论它们。

这里有几个我们之前没见过的新的概念:

构造函数constructor() 是一个构造函数,这是一个可选的特殊函数。它只在合约首次创建时执行一次。

函数修饰符modifier onlyOwner()。修饰符有点像半函数,用于修改其他函数,通常在执行前检查某些要求。在这个例子中,onlyOwner 可用于限制访问,只有合约的所有者才能运行此函数。我们将在下一章详细讨论函数修饰符,以及那个奇怪的 _; 是做什么的。

indexed 关键字:暂时不用担心这个,我们还不需要它。

所以 Ownable 合约基本上做了以下事情:

当合约创建时,它的构造函数将所有者设置为 msg.sender(部署合约的人)

它添加了一个 onlyOwner 修饰符,可以限制某些函数只能由所有者访问

它允许你将合约转让给新的所有者

onlyOwner 是合约中如此常见的要求,以至于大多数 Solidity DApps 都是从复制/粘贴这个 Ownable 合约开始的,然后他们的第一个合约会继承它。

由于我们想将 setKittyContractAddress 限制为只有所有者能调用,我们将在我们的合约中做同样的处理。

实践练习
我们已经提前将 Ownable 合约的代码复制到了新文件 ownable.sol 中。让我们继续让 ZombieFactory 继承它。

修改我们的代码以导入 ownable.sol 的内容。如果你不记得怎么做,可以查看 zombiefeeding.sol 文件。

修改 ZombieFactory 合约以继承自 Ownable。同样,如果你不记得怎么做,可以查看 zombiefeeding.sol 文件。

onlyOwner函数修饰符

现在我们的基础合约 ZombieFactory 继承自 Ownable,我们也可以在 ZombieFeeding 中使用 onlyOwner 函数修饰符。

这是由于合约继承的工作方式。记住:

1
2
ZombieFeeding 是 ZombieFactory
ZombieFactory 是 Ownable

因此 ZombieFeeding 也是 Ownable,并且可以访问 Ownable 合约中的函数/事件/修饰符。这也适用于未来继承自 ZombieFeeding 的任何合约。

函数修饰符

函数修饰符看起来很像函数,但使用关键字 modifier 而不是 function。而且它不能像函数那样直接调用——相反,我们可以在函数定义的末尾附加修饰符的名称来改变该函数的行为。

让我们通过仔细研究 onlyOwner 来更深入地了解:

注意到 renounceOwnership 函数上的 onlyOwner 修饰符。当你调用 renounceOwnership 时,onlyOwner 中的代码会首先执行。然后当它遇到 onlyOwner 中的 _; 语句时,它会返回并执行 renounceOwnership 中的代码。

虽然修饰符还有其他用法,但最常见的用例之一是在函数执行前添加一个快速的 require 检查。

onlyOwner 而言,将这个修饰符添加到函数上,使得只有合约的所有者(也就是部署合约的你)能够调用该函数。

注意:像这样赋予所有者对合约的特殊权限通常是必要的,但也可能被恶意使用。例如,所有者可以添加一个后门函数,允许他将任何人的僵尸转移给自己!

因此重要的是要记住,仅仅因为一个 DApp 在以太坊上,并不自动意味着它是去中心化的——你必须实际阅读完整的源代码,确保它没有需要你担心的所有者特殊控制。作为开发者,在保持对 DApp 的控制(以便修复潜在错误)与构建用户可信赖的无所有者平台之间,需要谨慎平衡。

实践练习
现在我们可以限制对 setKittyContractAddress 的访问,这样除了我们之外,没有人能在未来修改它。

onlyOwner 修饰符添加到 setKittyContractAddress 函数上。

gas费

第4章:Gas

太好了!现在我们知道如何更新 DApp 的关键部分,同时防止其他用户干扰我们的合约。

让我们看看 Solidity 与其他编程语言另一个很大的不同之处:

Gas——以太坊 DApp 运行的燃料

在 Solidity 中,用户每次在您的 DApp 上执行函数时都需要支付费用,使用的货币称为 gas。用户用以太币(以太坊的货币)购买 gas,因此您的用户必须花费 ETH 才能在您的 DApp 上执行函数。

执行函数需要多少 gas 取决于该函数逻辑的复杂程度。每个单独的操作都有基于执行该操作所需计算资源的 gas 成本(例如,写入存储比两个整数相加要昂贵得多)。函数的总 gas 成本是其所有单独操作 gas 成本的总和。

由于运行函数对用户来说需要花费真金白银,在以太坊中代码优化比其他编程语言中重要得多。如果您的代码写得马虎,用户将不得不支付更高的费用来执行您的函数——这可能会在数千用户中累积成数百万美元的不必要费用。

为什么需要 gas?

以太坊就像一台庞大、缓慢但极其安全的计算机。当您执行函数时,网络上的每个节点都需要运行相同的函数来验证其输出——数千个节点验证每个函数的执行,这正是以太坊去中心化的体现,也使其数据不可变且抗审查。

以太坊的创建者希望确保没有人能够通过无限循环阻塞网络,或者通过非常密集的计算占用所有网络资源。因此他们规定交易不是免费的,用户必须为计算时间和存储空间付费。

注意:对于其他区块链,比如 CryptoZombies 作者在 Loom Network 上构建的区块链,情况不一定如此。直接在以太坊主网上运行像《魔兽世界》这样的游戏可能永远都不合适——gas 成本会高得令人望而却步。但它可以在具有不同共识算法的区块链上运行。我们将在以后的课程中详细讨论哪些类型的 DApp 适合部署在 Loom 上,哪些适合部署在以太坊主网上。

通过结构体打包节省 gas

在第1课中,我们提到还有其他类型的 uint:uint8、uint16、uint32 等。

通常使用这些子类型没有好处,因为无论 uint 大小如何,Solidity 都会保留 256 位的存储空间。例如,使用 uint8 而不是 uint(uint256)不会节省任何 gas。

但有一个例外:在结构体内部。

如果在结构体中有多个 uint,尽可能使用较小尺寸的 uint 将允许 Solidity 将这些变量打包在一起以占用更少的存储空间。例如:

因此,在结构体内部,您会希望使用尽可能小的整数子类型。

您还需要将相同的数据类型聚集在一起(即在结构体中让它们相邻存放),这样 Solidity 才能最小化所需的存储空间。例如,一个包含字段 uint c; uint32 a; uint32 b; 的结构体,会比包含字段 uint32 a; uint c; uint32 b; 的结构体消耗更少的 gas,因为 uint32 字段被聚集在了一起。

实践练习
在本课中,我们将为我们的僵尸添加两个新特性:level(等级)和 readyTime(就绪时间)——后者将用于实现冷却计时器,以限制僵尸捕食的频率。

让我们回到 zombiefactory.sol

为我们的 Zombie 结构体再添加两个属性:level(一个 uint32)和 readyTime(也是一个 uint32)。我们希望将这些数据类型打包在一起,所以把它们放在结构体的末尾。

32 位对于存储僵尸的等级和时间戳来说已经绰绰有余,因此通过比使用常规 uint(256 位)更紧密地打包数据,这将为我们节省一些 gas 成本。

第5章:时间单位

level 属性相当容易理解。之后,当我们创建战斗系统时,赢得更多战斗的僵尸会随着时间升级并获得更多能力。

readyTime 属性需要更多一些解释。目标是添加一个”冷却期”,即僵尸在捕食或攻击后必须等待一段时间,才能再次捕食/攻击。如果没有这个限制,僵尸每天可以攻击和繁殖1000次,这会使游戏变得过于简单。

为了跟踪僵尸需要等待多长时间才能再次攻击,我们可以使用 Solidity 的时间单位。

时间单位

Solidity 提供了一些原生的时间单位来处理时间。

变量 now 将返回最新区块的当前 Unix 时间戳(自1970年1月1日以来经过的秒数)。在我写下这段文字时,Unix 时间是 1515527488。

注意:Unix 时间传统上存储在32位数字中。这将导致”2038年”问题,届时32位 Unix 时间戳将溢出并破坏许多传统系统。因此,如果我们希望我们的 DApp 在20年后仍然运行,我们可以使用64位数字——但与此同时,我们的用户将不得不支付更多的 gas 来使用我们的 DApp。这就是设计决策!

Solidity 还包含时间单位:secondsminuteshoursdaysweeksyears。这些单位会转换为对应时间长度的秒数 uint。所以 1 minutes 是 60,1 hours 是 3600(60秒 × 60分钟),1 days 是 86400(24小时 × 60分钟 × 60秒),等等。

下面是一个展示这些时间单位如何有用的例子:

我们可以将这些时间单位用于我们的僵尸冷却功能。

实践练习
让我们为 DApp 添加一个冷却时间,使得僵尸在攻击或捕食后必须等待1天才能再次攻击。

声明一个名为 cooldownTime 的 uint,并将其设置为 1 days。(请原谅语法问题——如果你设置为 “1 day”,它将无法编译!)

由于我们在上一章向 Zombie 结构体添加了 levelreadyTime,我们需要更新 _createZombie() 函数,在创建新的 Zombie 结构体时使用正确数量的参数。

更新 zombies.push 这行代码,添加2个参数:1(表示等级)和 uint32(now + cooldownTime)(表示就绪时间)。

注意uint32(...) 是必需的,因为 now 默认返回 uint256。所以我们需要显式地将其转换为 uint32。

now + cooldownTime 将等于当前的 Unix 时间戳(以秒为单位)加上1天的秒数——这将等于从现在起1天后的 Unix 时间戳。之后我们可以通过比较这个僵尸的 readyTime 是否大于 now 来判断是否已经过了足够的时间可以再次使用该僵尸。

我们将在下一章实现基于 readyTime 限制行动的功能。

第6章:僵尸冷却时间

既然我们的 Zombie 结构体已经有了 readyTime 属性,让我们转到 zombiefeeding.sol 并实现一个冷却计时器。

我们将修改 feedAndMultiply 函数,使得:

  1. 捕食会触发僵尸的冷却时间
  2. 在冷却期结束前,僵尸不能捕食猫咪

这将确保僵尸不能无限制地捕食猫咪并整天繁殖。将来当我们添加战斗功能时,我们也会让攻击其他僵尸依赖于冷却时间。

首先,我们要定义一些辅助函数来设置和检查僵尸的 readyTime

将结构体作为参数传递

你可以将一个指向结构体的存储指针作为参数传递给 privateinternal 函数。这很有用,例如,可以在函数之间传递我们的 Zombie 结构体。

语法如下所示:

1
2
3
function _doStuff(Zombie storage _zombie) internal {
// do stuff with _zombie
}

这样我们就可以将僵尸的引用传递给函数,而不是传入僵尸ID再进行查找。

实践练习
首先定义一个 _triggerCooldown 函数。它接受1个参数 _zombie,这是一个 Zombie 存储指针。该函数应该是 internal 的。

函数体应将 _zombie.readyTime 设置为 uint32(now + cooldownTime)

接下来,创建一个名为 _isReady 的函数。该函数也将接受一个名为 _zombie 的 Zombie 存储参数。它将是一个 internal view 函数,并返回一个 bool。

函数体应返回 (_zombie.readyTime <= now),这将计算为 true 或 false。这个函数会告诉我们自上次僵尸捕食后是否已经过了足够的时间。

第7章:公共函数与安全

现在让我们修改 feedAndMultiply 函数,将冷却计时器考虑进去。

回顾这个函数,你可以看到我们在上一课中将其设置为 public。一个重要的安全实践是检查所有你的 public 和 external 函数,并思考用户可能如何滥用它们。记住——除非这些函数有像 onlyOwner 这样的修饰符,否则任何用户都可以调用它们并传递任何他们想要的数据。

重新审视这个特定函数,用户可以直接调用该函数并传入任何他们想要的 _targetDna_species。这看起来不太像游戏——我们希望他们遵循我们的规则!

仔细检查后,这个函数只需要被 feedOnKitty() 调用,所以防止这些漏洞利用的最简单方法是将其设置为 internal。

实践练习
目前 feedAndMultiply 是一个 public 函数。让我们将其改为 internal,以使合约更安全。我们不希望用户能够使用任何他们想要的 DNA 来调用此函数。

让我们让 feedAndMultiply 考虑我们的 cooldownTime。首先,在我们查找 myZombie 之后,添加一个 require 语句来检查 _isReady() 并将 myZombie 传递给它。这样,只有当僵尸的冷却时间结束后,用户才能执行此函数。

在函数的最后,让我们调用 _triggerCooldown(myZombie),这样捕食就会触发僵尸的冷却时间。

第8章:函数修饰符详解

很好!我们的僵尸现在有了一个功能正常的冷却计时器。

接下来,我们将添加一些额外的辅助方法。我们为你创建了一个名为 zombiehelper.sol 的新文件,该文件导入了 zombiefeeding.sol。这将有助于保持我们的代码组织有序。

我们将实现让僵尸在达到一定等级后获得特殊能力的功能。但为了做到这一点,首先我们需要学习一些关于函数修饰符的更多知识。

带参数的函数修饰符

之前我们看了 onlyOwner 的简单示例。但函数修饰符也可以接受参数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一个存储用户年龄的映射:
mapping (uint => uint) public age;

// 要求用户年龄必须大于指定值的修饰符:
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}

// 必须年满16岁才能开车(至少在美国是这样)。
// 我们可以像这样带参数调用 `olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
// 一些函数逻辑
}

你可以看到这里的 olderThan 修饰符就像函数一样接受参数。而且 driveCar 函数将其参数传递给了修饰符。

让我们尝试创建自己的修饰符,使用僵尸的 level 属性来限制对特殊能力的访问。

实践练习
ZombieHelper 中,创建一个名为 aboveLevel 的修饰符。它接受两个参数:_level(一个 uint)和 _zombieId(也是一个 uint)。

修饰符的主体应检查确保 zombies[_zombieId].level 大于或等于 _level

记住在修饰符的最后一行使用 _; 来继续执行函数的剩余部分。

第9章:僵尸修饰符

现在让我们使用 aboveLevel 修饰符来创建一些函数。

我们的游戏将为玩家升级僵尸提供一些激励:

对于2级及以上的僵尸,用户将能够更改它们的名字。

对于20级及以上的僵尸,用户将能够给它们定制DNA。

我们将在下面实现这些功能。这是上一课的示例代码供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
// A mapping to store a user's age:
mapping (uint => uint) public age;

// Require that this user be older than a certain age:
modifier olderThan(uint _age, uint _userId) {
require (age[_userId] >= _age);
_;
}

// Must be older than 16 to drive a car (in the US, at least)
function driveCar(uint _userId) public olderThan(16, _userId) {
// Some function logic
}

实践练习
创建一个名为 changeName 的函数。它接受2个参数:_zombieId(一个 uint)和 _newName(一个字符串,数据位置设置为 calldata),并将其设为 external。它应该具有 aboveLevel 修饰符,并且应为 _level 参数传入 2。(不要忘记也要传递 _zombieId)。

注意:calldata 在某种程度上与 memory 类似,但它仅适用于 external 函数。

在这个函数中,首先我们需要验证 msg.sender 是否等于 zombieToOwner[_zombieId]。使用一个 require 语句。

然后该函数应将 zombies[_zombieId].name 设置为 _newName。

在 changeName 下面创建另一个名为 changeDna 的函数。它的定义和内容几乎与 changeName 相同,只是它的第二个参数将是 _newDna(一个 uint),并且它应该为 aboveLevel 的 _level 参数传入 20。当然,它应该将僵尸的 dna 设置为 _newDna,而不是设置僵尸的名字。