僵尸攻击人类

映射(Mapping)和地址(Address)

我们通过给数据库中的僵尸指定“主人”, 来支持“多玩家”模式
如此一来,我们需要引入 2 个新的数据类型:mapping(映射) 和 address(地址)

Addresses (地址)

以太坊区块链由 _ account _ (账户)组成,你可以把它想象成银行账户。一个帐户的余额是以太(在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样

每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:

0x0cE446255506E92DF41614C46F1d6df9Cc969183

(这是 CryptoZombies 团队的地址,如果你喜欢 CryptoZombies 的话,请打赏我们一些以太币!😉)
我们将在后面的课程中介绍地址的细节,现在你只需要了解地址属于特定用户(或智能合约)的

所以我们可以指定“地址”作为僵尸主人的 ID。当用户通过与我们的应用程序交互来创建新的僵尸时,新僵尸的所有权被设置到调用者的以太坊地址下

Mapping(映射)

在第一课中,我们看到了 结构体 和 数组。映射是另一种在 Solidity 中存储有组织数据的方法
映射是这样定义的:

1
2
3
4
//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:
mapping (address => uint) public accountBalance;
//或者可以用来通过userId 存储/查找的用户名
mapping (uint => string) userIdToName;

映射本质上是存储和查找数据所用的键-值对。在第一个例子中,键是一个 address,值是一个 uint,在第二个例子中,键是一个 uint,值是一个 string

为了存储僵尸的所有权,我们会使用到两个映射:一个记录僵尸拥有者的地址,另一个记录某地址所拥有僵尸的数量

  • 创建一个叫做 zombieToOwner 的映射。其键是一个 uint(我们将根据它的 id 存储和查找僵尸),值为 address。映射属性为 public
  • 创建一个名为 ownerZombieCount 的映射,其中键是 address,值是 uint
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
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
// 在这里定义映射
mapping(uint => address) public zombieToOwner;
mapping(address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}

Msg.sender

现在有了一套映射来记录僵尸的所有权了,我们可以修改 _createZombie 方法来运用它们
为了做到这一点,我们要用到 msg.sender

在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address

注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender 总是存在的

以下是使用 msg.sender 来更新 mapping 的例子:

1
2
3
4
5
6
7
8
9
10
11
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// 更新我们的 `favoriteNumber` 映射来将 `_myNumber`存储在 `msg.sender`名下
favoriteNumber[msg.sender] = _myNumber;
// 存储数据至映射的方法和将数据存储在数组相似
}
function whatIsMyNumber() public view returns (uint) {
// 拿到存储在调用者地址名下的值
// 若调用者还没调用 setMyNumber, 则值为 `0`
return favoriteNumber[msg.sender];
}

在这个小小的例子中,任何人都可以调用 setMyNumber 在我们的合约中存下一个 uint 并且与他们的地址相绑定。 然后,他们调用 whatIsMyNumber 就会返回他们存储的 uint
使用 msg.sender 很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的

我们来修改第 1 课的 _createZombie 方法,将僵尸分配给函数调用者吧。

  • 首先,在得到新的僵尸 id 后,更新 zombieToOwner 映射,在 id 下面存入 msg.sender。
  • 然后,我们为这个 msg.sender 名下的 ownerZombieCount 加 1。

跟在 JavaScript 中一样, 在 Solidity 中你也可以用 ++ 使 uint 递增

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
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
//我们得到这个id之后让id与msg.sender对应起来
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}

Require

在第一课中,我们成功让用户通过调用 createRandomZombie 函数并输入一个名字来创建新的僵尸。 但是,如果用户能持续调用这个函数来创建出无限多个僵尸加入他们的军团,这游戏就太没意思了!

于是,我们作出限定:每个玩家只能调用一次这个函数。 这样一来,新玩家可以在刚开始玩游戏时通过调用它,为其军团创建初始僵尸。

我们怎样才能限定每个玩家只调用一次这个函数呢?
答案是使用 require。 require 使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行

1
2
3
4
5
6
7
8
function sayHiToVitalik(string _name) public returns (string) {
// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 运行如下语句
return "Hi!";
}

如果你这样调用函数 sayHiToVitalik(“Vitalik”) ,它会返回“Hi!”。而如果调用的时候使用了其他参数,它则会抛出错误并停止执行。
因此,在调用一个函数之前,用 require 验证前置条件是非常有必要的

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
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
//检查条件
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}

继承

我们的游戏代码越来越长。 当代码过于冗长的时候,最好将代码和逻辑分拆到多个不同的合约中,以便于管理
有个让 Solidity 的代码易于管理的功能,就是合约 inheritance (继承):

1
2
3
4
5
6
7
8
9
10
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}

由于 BabyDoge 是从 Doge 那里 inherits (继承)过来的。 这意味着当你编译和部署了 BabyDoge,它将可以访问 catchphrase() 和 anotherCatchphrase()和其他我们在 Doge 中定义的其他公共函数
这可以用于逻辑继承(比如表达子类的时候,Cat 是一种 Animal)。 但也可以简单地将类似的逻辑组合到不同的合约中以组织代码

在接下来的章节中,我们将要为僵尸实现各种功能,让它可以“猎食”和“繁殖”。通过将这些运算放到父类 ZombieFactory 中,使得所有 ZombieFactory 的继承者合约都可以使用这些方法

  • 在 ZombieFactory 下创建一个叫 ZombieFeeding 的合约,它是继承自 `ZombieFactory 合约的
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
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}

contract ZombieFeeding is ZombieFactory {

}

引入(Import)

代码已经够长了,我们把它分成多个文件以便于管理。 通常情况下,当 Solidity 项目中的代码太长的时候我们就是这么做的。
在 Solidity 中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用 import 语句:

1
2
3
4
import "./someothercontract.sol";
contract newContract is SomeOtherContract {

}

这样当我们在合约(contract)目录下有一个名为 someothercontract.sol 的文件( ./ 就是同一目录的意思),它就会被编译器导入

现在我们已经建立了一个多文件架构,并用 import 来读取来自另一个文件中合约的内容:

  • 将 zombiefactory.sol 导入到我们的新文件 zombiefeeding.sol 中
1
2
3
4
5
6
pragma solidity ^0.4.19;
import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {

}

zombiefactory.sol 的内容,如果没更新不再附上

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
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}

Storage 与 Memory

在 Solidity 中,有两个地方可以存储变量 —— storage 或 memory
Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是 RAM 中数据的关系
**
大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失
然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 结构体 和 数组 时:

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
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ 看上去很直接,不过 Solidity 将会给出警告
// 告诉你应该明确在这里定义 `storage` 或者 `memory`。
// 所以你应该明确定义 `storage`:
Sandwich storage mySandwich = sandwiches[_index];
// ...这样 `mySandwich` 是指向 `sandwiches[_index]`的指针
// 在存储里,另外...
mySandwich.status = "Eaten!";
// ...这将永久把 `sandwiches[_index]` 变为区块链上的存储
// 如果你只想要一个副本,可以使用`memory`:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// ...这样 `anotherSandwich` 就仅仅是一个内存里的副本了
// 另外
anotherSandwich.status = "Eaten!";
// ...将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
// 不过你可以这样做:
sandwiches[_index + 1] = anotherSandwich;
// ...如果你想把副本的改动保存回区块链存储
}
}

如果你还没有完全理解究竟应该使用哪一个,也不用担心,在本教程中,我们将告诉你何时使用 storage 或是 memory,并且当你不得不使用到这些关键字的时候,Solidity 编译器也发警示提醒你的
现在,只要知道在某些场合下也需要你显式地声明 storage 或 memory 就够了!

是时候给我们的僵尸增加“猎食”和“繁殖”功能了!
当一个僵尸猎食其他生物体时,它自身的 DNA 将与猎物生物的 DNA 结合在一起,形成一个新的僵尸 DNA

  • 创建一个名为 feedAndMultiply 的函数。 使用两个参数:_zombieId( uint 类型 )和_targetDna (也是 uint 类型)。 设置属性为 public 的

  • 我们不希望别人用我们的僵尸去捕猎。 首先,我们确保对自己僵尸的所有权。 通过添加一个 require 语句来确保 msg.sender 只能是这个僵尸的主人(类似于我们在 createRandomZombie 函数中做过的那样)

  • 为了获取这个僵尸的 DNA,我们的函数需要声明一个名为 myZombie 数据类型为 Zombie 的本地变量(这是一个 storage 型的指针)。 将其值设定为在 zombies 数组中索引为 _zombieId 所指向的值

1
2
3
4
5
6
7
8
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
}
}

僵尸的 DNA

我们来把 feedAndMultiply 函数写完吧。
获取新的僵尸 DNA 的公式很简单:计算猎食僵尸 DNA 和被猎僵尸 DNA 之间的平均值。
例如:

1
2
3
4
5
6
function testDnaSplicing() public {
uint zombieDna = 2222222222222222;
uint targetDna = 4444444444444444;
uint newZombieDna = (zombieDna + targetDna) / 2;
// newZombieDna 将等于 3333333333333333
}
  • 首先我们确保 _targetDna 不长于 16 位。要做到这一点,我们可以设置 _targetDna 为 _targetDna % dnaModulus ,并且只取其最后 16 位数字
  • 接下来为我们的函数声明一个名叫 newDna 的 uint 类型的变量,并将其值设置为 myZombie 的 DNA 和 _targetDna 的平均值
  • 一旦我们计算出新的 DNA,再调用 _createZombie 就可以生成新的僵尸了。如果你忘了调用这个函数所需要的参数,可以查看 zombiefactory.sol 选项卡。请注意,需要先给它命名,所以现在我们把新的僵尸的名字设为 NoName - 我们回头可以编写一个函数来更改僵尸的名字
1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
}

更多关于函数可见性

我们上一课的代码有问题!编译的时候编译器就会报错,错误在于,我们尝试从 ZombieFeeding 中调用 _createZombie 函数,但 _createZombie 却是 ZombieFactory 的 private (私有)函数。这意味着任何继承自 ZombieFactory 的子合约都不能访问它

除 public 和 private 属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部) 和 external(外部)
internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。(嘿,这听起来正是我们想要的那样!)
external 与 public 类似,只不过这些函数只能在合约之外调用,它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用 external 和 public

声明函数 internal 或 external 类型的语法,与声明 private 和 public 类 型相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Sandwich {
uint private sandwichesEaten = 0;
function eat() internal {
sandwichesEaten++;
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string) {
baconSandwichesEaten++;
// 因为eat() 是internal 的,所以我们能在这里调用
eat();
}
}
  • 将 _createZombie() 函数的属性从 private 改为 internal , 使得其他的合约也能访问到它

注意,这次又回到了最早写的那一个文件

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
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
//修改
function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}

僵尸吃什么?

是时候让我们的僵尸去捕猎! 那僵尸最喜欢的食物是什么呢?
Crypto 僵尸喜欢吃的是…
CryptoKitties! 😱😱😱
(正经点,我可不是开玩笑 😆)
(补充一下,这是那个迷恋猫)

image.png
image.png

为了做到这一点,我们要读出 CryptoKitties 智能合约中的 kittyDna。这些数据是公开存储在区块链上的。区块链是不是很酷?
别担心,我们的游戏并不会伤害到任何真正的 CryptoKitty。 我们只读取 CryptoKitties 数据,但却无法在物理上删除它

如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口)
先举一个简单的栗子。 假设在区块链上有这么一个合约:

1
2
3
4
5
6
7
8
9
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}

这是个很简单的合约,您可以用它存储自己的幸运号码,并将其与您的以太坊地址关联。 这样其他人就可以通过您的地址查找您的幸运号码了

现在假设我们有一个外部合约,使用 getNum 函数可读取其中的数据
首先,我们定义 LuckyNumber 合约的 interface:

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

请注意,这个过程虽然看起来像在定义一个合约,但其实内里不同:
首先,我们只声明了要与之交互的函数,在本例中为 getNum 在其中我们没有使用到任何其他的函数或状态变量
其次,我们并没有使用大括号 { 和 } 定义函数体,我们单单用分号 ; 结束了函数声明。这使它看起来像一个合约框架
编译器就是靠这些特征认出它是一个接口的

在我们的 app 代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值
在下一课中,我们将真正调用其他合约的函数。目前我们只要声明一个接口,用于调用 CryptoKitties 合约就行了

我们已经为你查看过了 CryptoKitties 的源代码,并且找到了一个名为 getKitty 的函数,它返回所有的加密猫的数据,包括它的“基因”(我们的僵尸游戏要用它生成新的僵尸)。
该函数如下所示:

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
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
) {
Kitty storage kit = kitties[_id];
// if this variable is 0 then it's not gestating
isGestating = (kit.siringWithId != 0);
isReady = (kit.cooldownEndBlock <= block.number);
cooldownIndex = uint256(kit.cooldownIndex);
nextActionAt = uint256(kit.cooldownEndBlock);
siringWithId = uint256(kit.siringWithId);
birthTime = uint256(kit.birthTime);
matronId = uint256(kit.matronId);
sireId = uint256(kit.sireId);
generation = uint256(kit.generation);
genes = kit.genes;
}

这个函数看起来跟我们习惯的函数不太一样。 它竟然返回了…一堆不同的值! 如果您用过 JavaScript 之类的编程语言,一定会感到奇怪 —— 在 Solidity 中,您可以让一个函数返回多个值
现在我们知道这个函数长什么样的了,就可以用它来创建一个接口:

  • 定义一个名为 KittyInterface 的接口。 请注意,因为我们使用了 contract 关键字, 这过程看起来就像创建一个新的合约一样
  • 在 interface 里定义了 getKitty 函数(不过是复制/粘贴上面的函数,但在 returns 语句之后用分号,而不是大括号内的所有内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes);
}
contract ZombieFeeding is ZombieFactory {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
}

使用接口

继续前面 NumberInterface 的例子,我们既然将接口定义为:

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

我们可以在合约中这样使用:

1
2
3
4
5
6
7
8
9
10
11
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// ^ 这是FavoriteNumber合约在以太坊上的地址
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 现在变量 `numberContract` 指向另一个合约对象
function someFunction() public {
// 现在我们可以调用在那个合约中声明的 `getNum`函数:
uint num = numberContract.getNum(msg.sender);
// ...在这儿使用 `num`变量做些什么
}
}

通过这种方式,只要将您合约的可见性设置为 public (公共)或 external (外部),它们就可以与以太坊区块链上的任何其他合约进行交互

  • 我已经将代码中 CryptoKitties 合约的地址保存在一个名为 ckAddress 的变量中。在下一行中,请创建一个名为 kittyContract 的 KittyInterface,并用 ckAddress 为它初始化 —— 就像我们为 numberContract 所做的一样
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
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
}

处理多返回值

getKitty 是我们所看到的第一个返回多个值的函数。我们来看看是如何处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// 这样来做批量赋值:
(a, b, c) = multipleReturns();
}
// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
uint c;
// 可以对其他字段留空:
(,,c) = multipleReturns();
}

我们来定义一个函数,从 kitty 合约中获取它的基因:

  • 创建一个名为 feedOnKitty 的函数。它需要 2 个 uint 类型的参数,_zombieId 和 _kittyId ,这是一个 public 类型的函数

  • 函数首先要声明一个名为 kittyDna 的 uint

    注意:在我们的 KittyInterface 中,genes 是一个 uint256 类型的变量,但是如果你记得,我们在第一课中提到过,uint 是 uint256 的别名,也就是说它们是一回事

  • 这个函数接下来调用 kittyContract.getKitty 函数, 传入 _kittyId ,将返回的 genes 存储在 kittyDna 中。记住 getKitty 会返回一大堆变量。 (确切地说 10 个,我已经为你数过了,不错吧!)。但是我们只关心最后一个 genes。数逗号的时候小心点哦!

  • 最后,函数调用了 feedAndMultiply ,并传入了 _zombieId 和 kittyDna 两个参数

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
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna);
}
}

奖励: Kitty 基因

给从小猫制造出的僵尸添加些特征,以显示他们是猫僵尸,要做到这一点,咱们在新僵尸的 DNA 中添加一些特殊的小猫代码
还记得吗,第一课中我们提到,我们目前只使用 16 位 DNA 的前 12 位数来指定僵尸的外观。所以现在我们可以使用最后 2 个数字来处理“特殊”的特征

这样吧,把猫僵尸 DNA 的最后两个数字设定为 99。所以在我们这么来写代码:如果这个僵尸是一只猫变来的,就将它 DNA 的最后两位数字设置为 99。

if 语句

if 语句的语法在 Solidity 中,与在 JavaScript 中差不多:

1
2
3
4
5
6
function eatBLT(string sandwich) public {
// 看清楚了,当我们比较字符串的时候,需要比较他们的 keccak256 哈希码
if (keccak256(sandwich) == keccak256("BLT")) {
eat();
}
}

让我们在我们的僵尸代码中实现小猫的基因:

  • 首先,我们修改下 feedAndMultiply 函数的定义,给它传入第三个参数:一条名为 _species 的字符串

  • 接下来,在我们计算出新的僵尸的 DNA 之后,添加一个 if 语句来比较 _species 和字符串 “kitty” 的 keccak256 哈希值

  • 在 if 语句中,我们用 99 替换了新僵尸 DNA 的最后两位数字。可以这么做:newDna = newDna - newDna % 100 + 99

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

  • 最后,我们修改了 feedOnKitty 中的函数调用。当它调用 feedAndMultiply 时,增加 “kitty” 作为最后一个参数

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
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
//这里修改函数定义
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
// 这里增加一个 if 语句
if (keccak256(_species) == keccak256("kitty")){
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
//修改函数调用
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
image.png
image.png