以太坊开发高级语言学习。
咱们经过给数据库中的僵尸指定“主人”, 来支持“多玩家”模式。git
如此一来,咱们须要引入2个新的数据类型:mapping
(映射) 和 address
(地址)。数据库
以太坊区块链由 account
(帐户)组成,你能够把它想象成银行帐户。一个账户的余额是 以太
(在以太坊区块链上使用的币种),你能够和其余账户之间支付和接受以太币,就像你的银行账户能够电汇资金到其余银行账户同样。编程
每一个账户都有一个“地址”,你能够把它想象成银行帐号。这是帐户惟一的标识符,它看起来长这样:数组
0x0cE446255506E92DF41614C46F1d6df9Cc969183
咱们将在后面的课程中介绍地址的细节,如今你只须要了解地址属于特定用户(或智能合约)的。安全
因此咱们能够指定“地址”做为僵尸主人的 ID。当用户经过与咱们的应用程序交互来建立新的僵尸时,新僵尸的全部权被设置到调用者的以太坊地址下。架构
在上一篇博文中,咱们看到了 结构体
和 数组
。 映射
是另外一种在 Solidity 中存储有组织数据的方法。app
映射是这样定义的:框架
//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中: mapping (address => uint) public accountBalance; //或者能够用来经过userId 存储/查找的用户名 mapping (uint => string) userIdToName;
映射本质上是存储和查找数据所用的键-值
对。在第一个例子中,键是一个 address
,值是一个 uint
,在第二个例子中,键是一个uint
,值是一个 string
。dom
为了存储僵尸的全部权,咱们会使用到两个映射:一个记录僵尸拥有者的地址,另外一个记录某地址所拥有僵尸的数量。编程语言
zombieToOwner
的映射。其键是一个uint
(咱们将根据它的 id
存储和查找僵尸),值为 address
。映射属性为public
。ownerZombieCount
的映射,其中键是 address
,值是 uint
。Contract.sol
// 1. 这里写版本指令 pragma solidity ^0.4.19; // 2. 这里创建智能合约 contract ZombieFactory { // 12.这里创建事件 event NewZombie(uint zombieId, string name, uint dna); // 3. 定义 dnaDigits 为 uint 数据类型, 并赋值 16 uint dnaDigits = 16; // 4. 10 的 dnaDigits 次方 uint dnaModulus = 10 ** dnaDigits; // 5.结构体定义 struct Zombie { string name; uint dna; } // 6.数组类型为结构体的公共数组 Zombie[] public zombies; // 13.在这里定义映射 mapping(uint => address) public zombieToOwner; mapping(address => uint) ownerZombieCount; /* // 7.建立函数 function createZombie(string _name, uint _dna){ // 8.使用结构体和数组(初始化全局数组) zombies.push(Zombie(_name, _dna)); } */ // 7.建立函数(改成私有方法) function _createZombie(string _name, uint _dna) private { // 8.使用结构体和数组(初始化全局数组) // zombies.push(Zombie(_name, _dna)); // 十二、数组长度减一就是当前的数组ID uint id = zombies.push(Zombie(_name, _dna)) - 1; // 十二、这里触发事件 NewZombie(id, _name, _dna); } // 9.函数修饰符 private, view, returns 返回值 function _generateRandomDna(string _str) private view returns (uint){ // 10.散列并取模 uint rand = uint(keccak256(_str)); // 注意这里须要将string类型转为uint类型 return rand % dnaModulus; } // 十一、综合函数 function createRandomZombie(string _name) public { uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }
如今有了一套映射来记录僵尸的全部权了,咱们能够修改 _createZombie
方法来运用它们。
为了作到这一点,咱们要用到 msg.sender
。
在 Solidity 中,有一些全局变量能够被全部函数调用。 其中一个就是 msg.sender
,它指的是当前调用者(或智能合约)的 address
。
注意:在 Solidity 中,功能执行始终须要从外部调用者开始。 一个合约只会在区块链上什么也不作,除非有人调用其中的函数。因此
msg.sender
老是存在的。
如下是使用 msg.sender
来更新 mapping
的例子:
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
很安全,由于它具备以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,不然是没有办法修改其余人的数据的。
咱们来修改前边的_createZombie
方法,将僵尸分配给函数调用者吧。
id
后,更新 zombieToOwner
映射,在 id
下面存入 msg.sender
。msg.sender
名下的 ownerZombieCount
加 1。跟在 JavaScript 中同样, 在 Solidity 中你也能够用 ++ 使 uint 递增。
uint number = 0; number++; // `number` 如今是 `1`了
修改两行代码便可。
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; // 从这里开始,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); } }
咱们成功让用户经过调用 createRandomZombie
函数 并输入一个名字来建立新的僵尸。 可是,若是用户能持续调用这个函数来建立出无限多个僵尸加入他们的军团,这游戏就太没意思了!
因而,咱们做出限定:每一个玩家只能调用一次这个函数
。 这样一来,新玩家能够在刚开始玩游戏时经过调用它,为其军团建立初始僵尸。
咱们怎样才能限定每一个玩家只调用一次这个函数呢?
答案是使用require
。 require
使得函数在执行过程当中,当不知足某些条件时抛出错误,并中止执行:
function sayHiToVitalik(string _name) public returns (string) { // 比较 _name 是否等于 "Vitalik". 若是不成立,抛出异常并终止程序 // (敲黑板: Solidity 并不支持原生的字符串比较, 咱们只能经过比较 // 两字符串的 keccak256 哈希值来进行判断) require(keccak256(_name) == keccak256("Vitalik")); // 若是返回 true, 运行以下语句 return "Hi!"; }
若是你这样调用函数 sayHiToVitalik(“Vitalik”)
,它会返回“Hi!”。而若是调用的时候使用了其余参数,它则会抛出错误并中止执行。
所以,在调用一个函数以前,用 require
验证前置条件是很是有必要的。
在咱们的僵尸游戏中,咱们不但愿用户经过反复调用 createRandomZombie
来給他们的军队建立无限多个僵尸 —— 这将使得游戏很是无聊。
咱们使用了 require
来确保这个函数只有在每一个用户第一次调用它的时候执行,用以建立初始僵尸。
createRandomZombie
的前面放置 require
语句。 使得函数先检查 ownerZombieCount [msg.sender]
的值为 0 ,否则就抛出一个错误。注意:在 Solidity 中,关键词放置的顺序并不重要
ownerZombieCount [msg.sender]
放前面吧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 判断 require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }
咱们的游戏代码愈来愈长。 当代码过于冗长的时候,最好将代码和逻辑分拆到多个不一样的合约中
,以便于管理。
有个让 Solidity
的代码易于管理的功能,就是合约 inheritance
(继承):
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
合约的。
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); } } // Start here (合约继承) contract ZombieFeeding is ZombieFactory { }
在这一节中,咱们将对上边那个很大的合约进行拆分。
上边的代码已经够长了,咱们把它分红多个文件以便于管理。 一般状况下,当 Solidity
项目中的代码太长的时候咱们就是这么作的。
在 Solidity 中,当你有多个文件而且想把一个文件导入另外一个文件时,可使用 import
语句:
import "./someothercontract.sol"; contract newContract is SomeOtherContract { }
这样当咱们在合约(contract)目录下有一个名为 someothercontract.sol
的文件( ./
就是同一目录的意思),它就会被编译器导入。
如今咱们已经创建了一个多文件架构,并用 import 来读取来自另外一个文件中合约的内容:
zombiefactory.sol
导入到咱们的新文件 zombiefeeding.sol
中。zombiefactory.sol
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); } }
zombiefeeding.sol
pragma solidity ^0.4.19; // put import statement here(导入合约) // import './zombiefactory.sol'; // 导入另外一个文件不能用单引号,只能用双引号,不然会报错 import "./zombiefactory.sol"; contract ZombieFeeding is ZombieFactory { }
在 Solidity 中,有两个地方能够存储变量 —— storage
或 memory
。
Storage
变量是指永久存储在区块链
中的变量。 Memory
变量则是临时
的,当外部函数对某合约调用完成时,内存型变量即被移除。 你能够把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
大多数时候你都用不到这些关键字,默认状况下 Solidity 会自动处理它们。 状态变量(在函数以外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
然而也有一些状况下,你须要手动声明存储类型,主要用于处理函数内的 结构体
和 数组
时:
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 函数中作过的那样)。
注意:一样,由于咱们的答案检查器比较呆萌,只认识把
msg.sender
放在前面的答案,若是你切换了参数的顺序,它就不认得了。 但你正常编码时,如何安排参数顺序都是正确的。
myZombie
数据类型为Zombie的本地变量(这是一个 storage 型的指针)。 将其值设定为在 zombies 数组中索引为_zombieId
所指向的值。到目前为止,包括函数结束符 }
的那一行, 总共4行代码。
zombiefeeding.sol
pragma solidity ^0.4.19; import "./zombiefactory.sol"; contract ZombieFeeding is ZombieFactory { // Start here function feedAndMultiply(uint _zombieId, uint _targetDna) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; } }
咱们来把 feedAndMultiply
函数写完吧。
获取新的僵尸DNA的公式很简单:计算猎食僵尸DNA和被猎僵尸DNA之间的平均值。
示例:
function testDnaSplicing() public { uint zombieDna = 2222222222222222; uint targetDna = 4444444444444444; uint newZombieDna = (zombieDna + targetDna) / 2; // newZombieDna 将等于 3333333333333333 }
之后,咱们也可让函数变得更复杂些,比方给新的僵尸的 DNA 增长一些随机性之类的。但如今先从最简单的开始 —— 之后还能够回来完善它嘛。
_targetDna
不长于16位。要作到这一点,咱们能够设置 _targetDna
为 _targetDna%dnaModulus
,而且只取其最后16
位数字。注意:您能够用myZombie.name
或myZombie.dna
访问myZombie
的属性。
_createZombie
就能够生成新的僵尸了。若是你忘了调用这个函数所须要的参数,能够查看 zombiefactory.sol
选项卡。请注意,须要先给它命名,因此如今咱们把新的僵尸的名字设为NoName - 咱们回头能够编写一个函数来更改僵尸的名字。注意:对于 Solidity 高手,你可能会注意到咱们的代码存在一个问题。别担忧,下一章会解决这个问题的 ;)
zombiefeeding.sol
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]; // start here _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类 型相同:
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
, 使得其余的合约也能访问到它。咱们已经成功把你的注意力集中在到zombiefactory.sol
这个基类合约上了。
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; // 在这里修改函数的功能 private => internal 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); } }
是时候让咱们的僵尸去捕猎! 那僵尸最喜欢的食物是什么呢?
为了作到这一点,咱们要读出 CryptoKitties 智能合约中的 kittyDna。这些数据是公开存储在区块链上的。区块链是否是很酷?
别担忧 —— 咱们的游戏并不会伤害到任何真正的CryptoKitty。 咱们只 读取 CryptoKitties 数据,但却没法在物理上删除它。
若是咱们的合约须要和区块链上的其余的合约会话,则需先定义一个 interface
(接口)。
先举一个简单的栗子。 假设在区块链上有这么一个合约:
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
:
contract NumberInterface { function getNum(address _myAddress) public view returns (uint); }
请注意,这个过程虽然看起来像在定义一个合约,但其实内里不一样:
首先,咱们只声明了要与之交互的函数 —— 在本例中为 getNum
—— 在其中咱们没有使用到任何其余的函数或状态变量。
其次,咱们并无使用大括号({
和 }
)定义函数体,咱们单单用分号(;
)结束了函数声明。这使它看起来像一个合约框架。
编译器就是靠这些特征认出它是一个接口的。
在咱们的 app 代码中使用这个接口,合约就知道其余合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。
在下一课中,咱们将真正调用其余合约的函数。目前咱们只要声明一个接口,用于调用 CryptoKitties 合约就好了。
咱们已经为你查看过了 CryptoKitties 的源代码,而且找到了一个名为 getKitty
的函数,它返回全部的加密猫的数据,包括它的“基因”(咱们的僵尸游戏要用它生成新的僵尸)。
该函数以下所示:
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
关键字, 这过程看起来就像建立一个新的合约同样。getKitty
函数(不过是复制/粘贴上面的函数,但在 returns
语句以后用分号,而不是大括号内的全部内容。zombiefeeding.sol
pragma solidity ^0.4.19; import "./zombiefactory.sol"; // Create KittyInterface here 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); } }