Solidity学习记录——第二章

Posted 龙行龘龘龘龘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solidity学习记录——第二章相关的知识,希望对你有一定的参考价值。

Solidity学习记录

第一章 创建生产僵尸的工厂
第二章 设置僵尸的攻击功能



前言

本人平时比较忙,只能在周末自学Solidity,尽量在周日更新学习记录,这份学习记录不仅仅是记录我的学习过程,也是我继续按时学习的监督。


一、本章主要目的

我们在之前的课程建立了僵尸工厂,可以生成僵尸,但只有僵尸这个类,没有为僵尸添加任何动作和功能(就类似于之前只是做了一个手办一样的僵尸),这节课是对僵尸建立起动作相关的功能(让手办一样的僵尸变成可以吃东西,可以感染其他生物的僵尸)。这里要添加僵尸的食物来源和如何生成新僵尸,同时注意僵尸要有进食间隔,不能不停地吃东西。同时,由于这个教程的目的是编写一个游戏,要支持多玩家模式,所以要记录好僵尸的所属,不能让一个用户可以随意的使用他人的僵尸。

二、学习过程

1.本节课程知识点

1、Solidity中,一个十分重要的数据类型:Addresses (地址)。
以太坊区块链由 _ account _ (账户)组成,你可以把它想象成银行账户。一个帐户的余额是 Ether (以太)(在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。
每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:

0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 (这是一个随机生成的虚拟地址)
注意:地址属于特定用户(或智能合约)的。
所以我们可以指定“地址”作为僵尸主人的 ID----因为地址具有唯一表示。当用户通过与我们的应用程序交互来创建新的僵尸时,新僵尸的所有权被设置到调用者的以太坊地址下。

2、我们看到了 _ struct _ 和 _ array _ 。 _ mapping _ 是另一种在 Solidity 中存储有组织数据的方法。mapping是这样定义的:

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

mapping本质上是存储和查找数据所用的key–value(键-值对)。在第一个例子中,键是一个 address,值是一个 uint,在第二个例子中,键是一个uint,值是一个 string。

3、我们在设置mapping的时候,有两个问题要考虑到:
(1)僵尸和拥有者是多对一的关系,即一个僵尸只能有一个拥有者,但一个拥有者可以拥有多个僵尸;

(2)键-值对是一对一关系,多个值组成的一个集合可以看成一个值,但想要查找僵尸的拥有者的时间则未定。(如果一个用户拥有大量的僵尸,用拥有者作为键,在修改僵尸的所有权时会花费大量资源或者打乱僵尸的顺序。前者会花费gas,这是需要真金白银的;后者会改变僵尸军团的秩序。)

我们在上一课建立僵尸时,给僵尸设置了一个身份ID,这个对于每个僵尸来说是唯一的身份标识。僵尸ID加上用户的地址,我们有了两个独一无二的标志符。这样我们既可以用僵尸ID作为键,也可以用用户地址作为键。所以建立两个mapping,一个用僵尸ID作为键,用户地址作为值,标志僵尸的归属;一个用用户地址作为键,拥有的僵尸数量作为值,标志用户拥有的僵尸数量。

4、在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address。使用 msg.sender 很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。
注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender总是存在的。(在智能合约的众筹项目中,msg.sender是十分重要的,它代表着参加众筹项目的支持者的地址和支付账户(因为涉及到从哪个账户转账、扣钱,所以这个最为关键!!!))

5、require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行。在调用一个函数之前,用 require 验证前置条件是非常有必要的。(注意:在 Solidity 中,关键词放置的顺序并不重要,参数的两个位置是等效的。)
例如:我们设置每个用户只能调用一次 createRandomZombie函数来生成一个僵尸,则可以在createRandomZombie中首先添加:
require(ownerZombieCount[msg.sender] == 0); // 只有用户没有僵尸时才能调用

6、Solidity同样有继承功能,支持合约之间的继承。现有一个合约A、合约B,合约B想要继承合约A,则要写为contract B is A,由于 B 是从 A 那里 inherits (继承)过来的。 这意味着当你编译和部署了 B,它将可以访问我们定义在B和A中定义的所有公共函数。这可以用于逻辑继承(比如表达子类的时候,Cat 是一种 Animal)。 但也可以简单地将类似的逻辑组合到不同的合约中以组织代码。

7、在 Solidity 中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用 import 语句。若两个文件在同一个文件夹中,可以用如下的代码:
例如想要将同文件夹的sol文件A导入,则应该写为:import “./A.sol”;
这样当我们在这个合约目录下有一个名为 A.sol 的文件( ./ 就是同一目录的意思),它就会被编译器导入。

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

9、除 public 和 private 属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部) 和 external(外部)。internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。external 与public 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。声明函数 internal 或 external 类型的语法,与声明 private 和 public类 型相同。

10、如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 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 代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。

11、在 Solidity中,可以让一个函数返回多个值。

12、创建一个接口:(1)定义一个名为 XXX 的接口。 请注意,因为我们使用了 contract 关键字, 这过程看起来就像创建一个新的合约一样。(2)在interface里定义了 YYY 函数(不过是复制/粘贴上面的函数,但在 returns 语句之后用分号,而不是大括号内的所有内容)。

13、返回多个值的函数可以这样处理:

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();   //按返回值的顺序依次给a、b、c赋值
}

// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
  uint c;
  // 可以对其他字段留空:
  (,,c) = multipleReturns();  //只对c赋值,将c赋值为返回的众多值中的最后一个返回值
}

2.最终代码

代码如下:

zombiefeeding.sol

//this is zombiefeeding.sol

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 {

  // 1. 移除这一行:
  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  // 2. 只声明变量:
  KittyInterface kittyContract = KittyInterface(ckAddress);

  // 3. 增加 setKittyContractAddress 方法

  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 (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");
  }

}

zombiefactory.sol

//this is 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) 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);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}



总结

这个章节重点内容为:

  1. 每个人的地址address是独一无二的,可以通过msg.sender获得调用者的地址。
  2. mapping在Solidity语言中有着重要作用,是类似于字典类的、独属于Solidity的函数。
  3. require有着十分便利的功能,如果在编写众筹智能合约时,可以用来检测众筹资金是否达到目标金额等一系列问题。
  4. 继承和导入方法使得代码不再过于冗长,让代码编写更加规范,更加易于检查。internal则使得函数可以在同一合约的不同文件之间可以互相使用,而不能被外部使用。
  5. storage指全局变量,多为指针类型;memory指局部变量,不限类型。在编译器remix中可以自动检查,若将memory错用为storage,会反馈错误,同时写明此处应该为memory。
  6. interface让我们的合约可以和其他合约会话,也方便我们获得其他合约的返回值。

以上是关于Solidity学习记录——第二章的主要内容,如果未能解决你的问题,请参考以下文章

Solidity学习记录——第四章

智能合约实战 solidity 语法学习 13 [ 事件event emit日志logs 异常throw revertrequireassert] 附代码

智能合约实战 solidity 语法学习 13 [ 事件event emit日志logs 异常throw revertrequireassert] 附代码

智能合约实战 solidity 语法学习 13 [ 事件event emit日志logs 异常throw revertrequireassert] 附代码

智能合约实战 solidity 语法学习 13 [ 事件event emit日志logs 异常throw revertrequireassert] 附代码

Solidity内嵌汇编学习