cryptozombies

网站: https://cryptozombies.io/
是一个学习solidity的教学网站,英语的。
可以一边学英语一边学solidity开发。

状态变量state 变量将会永久存储在以太坊链上
Unsigned Integers无符号整数

英语中,如何表达次方

to the power of (最正式、最标准)

10 to the power of 3 (10的3次方)

数组

数组分为固定数组和动态数组

1
2
3
4
5
6
// Array with a fixed length of 2 elements:
uint[2] fixedArray;
// another fixed Array, can contain 5 strings:
string[5] stringArray;
// a dynamic Array - has no fixed size, can keep growing:
uint[] dynamicArray;

你可以创建一个公开public数组,solidity编译器会自动创建一个getter方法

变量声明解析

1
2
3
4
5
6
7
8
// 定义结构体
struct Person {
string name;
uint age;
}

// 声明数组
Person[] public people; // people 就是数组的名字

Person[] public people;
Person[]: 这是一个类型声明,表示这是一个 Person 类型的数组
public: 可见性修饰符,表示这个变量可以被外部访问
people: 这是变量名,也就是你给这个数组起的名字

数组不一定非要和结构体有关,它们可以存储任何数据类型。

1
2
3
4
5
// 基本类型的数组
uint[] public numbers; // 整数数组
string[] public names; // 字符串数组
address[] public wallets; // 地址数组
bool[] public flags; // 布尔值数组

第11章:Keccak256 与类型转换

1
2
uint rand = unit(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;

我来解释这段Solidity代码:

代码分析

1
2
uint rand = uint(keccak256(abi.encodePacked("_str")));
return rand % dnaModulus;

1. 随机数生成过程

abi.encodePacked("_str")

  • 将字符串 “_str” 进行紧凑编码(不添加填充字节)
  • 返回 bytes 类型的数据

keccak256(...)

  • 对编码后的数据进行Keccak-256哈希计算
  • 返回32字节的哈希值(bytes32类型)

uint(...)

  • 将哈希结果转换为无符号整数(uint256)
  • 将32字节的哈希值解释为一个非常大的整数

% dnaModulus

  • 对生成的随机数取模,限制在特定范围内
  • dnaModulus 通常是一个定义范围的除数

这种模式常用于:

  • 生成伪随机DNA或特征值
  • 创建基于输入的确定性但看似随机的标识符
  • 游戏中的随机元素生成

注意: 在区块链上无法获得真正的随机数,所有”随机”都是伪随机且可预测的。

对生成的随机数取模是什么意思

好的,这是一个非常核心的编程和数学概念。我用一个简单的方式来解释“对生成的随机数取模”是什么意思。

一句话概括

取模运算就是求余数。 对一个随机数取模,就是为了限制这个随机数的范围,让它最终落在一个你想要的区间内。


详细解释

1. 什么是取模运算?

符号是 %。它的作用是计算一个数除以另一个数后的余数

  • 例子:
    • 7 % 3 = 1 (因为7除以3等于2,余1)
    • 10 % 4 = 2 (因为10除以4等于2,余2)
    • 15 % 5 = 0 (因为15除以5等于3,余0)

2. 为什么在随机数生成中要用取模?

在你提供的代码中:

1
2
uint rand = uint(keccak256(abi.encodePacked("_str"))); // 这会产生一个巨大的随机数,比如 123456789...
return rand % dnaModulus; // 我们用取模来限制范围
  • 第一步产生的问题keccak256 会生成一个非常非常大的随机数(范围从 0 到 2²⁵⁶ - 1)。这个范围太大了,通常不是我们想要的。
  • 第二步(取模)的目的:我们使用 % dnaModulus 把这个巨大的数字“压缩”到一个可控的范围内。

取模运算的关键特性:
X % N 的结果永远在 [0, N-1] 这个区间内

3. 具体例子

假设:

  • keccak256(...) 生成了一个巨大的随机数:**123456**
  • 你设定的 dnaModulus = 100

那么:

1
123456 % 100 = 56

最终返回的随机数就是 56

为什么是56?

  • 因为 123456 除以 100 等于 1234 余 56
  • 由于除数是100,余数不可能大于等于100,所以结果被牢牢地限制在了 0 到 99 之间。

在您代码中的具体应用

1
2
uint rand = uint(keccak256(abi.encodePacked("_str")));
return rand % dnaModulus;
  • 目的:生成一个在 0dnaModulus - 1 之间的伪随机数。
  • **假设 dnaModulus = 10**,那么无论 rand 多大,最终结果一定是 0, 1, 2, …, 9 中的一个。
  • **假设 dnaModulus = 100**,那么最终结果就是 0 到 99 之间的一个数。

总结

“对生成的随机数取模”就是一个数学上的“缩放”过程:

  1. 输入:一个巨大的、范围不可控的随机数。
  2. 操作:使用 %(取模)运算。
  3. 输出:一个范围被精确控制在 [0, 模数-1] 之间的、更实用的随机数。

这是一种非常常见且重要的技巧,用于将哈希函数生成的大随机数映射到我们需要的特定范围内。

事件event

Modify the first line of the _createZombie function. You’re going to need the zombie’s id. The array.push() function returns a uint of the new length of the array - and since the first item in an array has index 0, array.push() - 1 will be the index of the zombie we just added. Store the result of zombies.push() - 1 in a uint called id, so you can use this in the NewZombie event in the next line.
修改 _createZombie 函数的第一行代码。您需要获取僵尸的id。array.push() 函数会返回一个表示数组新长度的 uint 值——由于数组中的第一项索引为 0,所以 array.push() - 1 将是我们刚刚添加的僵尸的索引。将 zombies.push() - 1 的结果存储在一个名为 id 的 uint 变量中,这样您就可以在下一行的 NewZombie 事件中使用它。

(该翻译保持了技术文档的准确性,同时将英文的被动语态和长句结构转换为中文常见的主动语态和分句表达,使内容更符合中文技术文档的阅读习惯。关键编程概念如”array.push()”、”uint”、”index”等均使用标准译法,确保专业术语的准确性。)

fire the NewZombie event. 这个fire怎么理解?
“触发 NewZombie 事件”

web3.js

我们的 Solidity 合约已经完成!现在我们需要编写一个与合约交互的 JavaScript 前端。

以太坊有一个名为 Web3.js 的 JavaScript 库。

在后续课程中,我们将深入讲解如何部署合约和设置 Web3.js。但现在,让我们先看看 Web3.js 如何与我们已部署合约交互的示例代码。

如果现在还有些不明白,不用担心。我们的 JavaScript 代码会获取上面 zombieDetails 中生成的值,并利用基于浏览器的 JavaScript 技巧(我们使用 Vue.js)来替换图片和应用 CSS 滤镜。您将在后续课程中获得所有这些代码。

试试看吧!
在右侧输入框中输入您的名字,看看会得到什么样的僵尸!

当您获得满意的僵尸后,请点击下方的”下一章”来保存您的僵尸并完成第 1 课!

这个链接证明我完成了该solidity课程: https://share.cryptozombies.io/en/lesson/1/share/111111111111111111111111?id=Y3p8NjY1NTA4

地址和映射

地址
以太坊区块链由账户组成,您可以将其类比为银行账户。每个账户都持有一定余额的以太币(Ethereum区块链上使用的货币),您可以向其他账户发送或接收以太币,就像银行账户可以电汇资金到其他银行账户一样。

每个账户都有一个地址,这类似于银行账号。它是指向该账户的唯一标识符

我们将在后续课程详细讨论地址的细节,目前您只需理解:每个地址都由特定用户(或智能合约)拥有。

因此,我们可以将地址作为僵尸所有权的唯一标识符。当用户通过与我们的应用交互创建新僵尸时,我们会将这些僵尸的所有权设置为调用该函数的以太坊地址。

映射
在第一课中,我们学习了结构体和数组。映射是Solidity中另一种存储组织化数据的方式。

映射本质上是一个键值对存储结构,用于存储和查询数据。在第一个例子中,键是 address 类型,值是 uint 类型;在第二个例子中,键是 uint 类型,值是 string 类型。

实践练习
为了存储僵尸的所有权信息,我们将使用两个映射:一个用于记录每个僵尸所属的地址,另一个用于记录每个地址拥有的僵尸数量。

创建一个名为 zombieToOwner 的映射。键的类型为 uint(我们将根据僵尸的 id 来存储和查找),值的类型为 address。请将此映射设为 public。

创建一个名为 ownerZombieCount 的映射,其中键的类型为 address,值的类型为 uint。


mapping (address => uint) public zombieToOwner; 为什么这个写法是错误的?mapping (uint => address) public zombieToOwner;这个写法却是正确的?

好的,这是一个非常关键的理解点。这两个写法的区别在于映射中键和值的逻辑关系

核心原因

zombieToOwner 这个名字的语义是“通过僵尸ID找到其主人”。

让我们来分析一下:


1. 错误写法:mapping (address => uint) public zombieToOwner;

  • 键 (Key): address (一个所有者地址)
  • 值 (Value): uint (一个僵尸ID)

这行代码的实际含义是:
“给定一个所有者地址,返回他拥有的某一个僵尸的ID。”

这为什么是错的?

  • 一个所有者(地址)可以拥有多个僵尸。
  • 这个映射只能存储 “一个地址 => 一个僵尸ID” 的关系。
  • 如果用户拥有第二个僵尸,你存储新ID时就会覆盖掉第一个僵尸的ID
  • 你无法查询一个地址拥有的所有僵尸。

这更像是 ownerToSingleZombie 的映射,而不是 zombieToOwner


2. 正确写法:mapping (uint => address) public zombieToOwner;

  • 键 (Key): uint (一个僵尸ID)
  • 值 (Value): address (该僵尸的所有者地址)

这行代码的实际含义是:
“给定一个僵尸ID,返回拥有它的所有者地址。”

这为什么是正确的?

  • 每个僵尸的ID是全局唯一的。
  • 一个僵尸在任意时刻只能有一个所有者。
  • 这个映射完美地建立了 “一个僵尸 => 一个所有者” 的关系。
  • 这正是 zombieToOwner 这个名字想表达的逻辑。

直观类比

把它想象成一个电话簿:

  • 错误写法 (address => uint)

    • 就像你想通过“人名”去查“电话号码”,但规定每个人只能有一个电话号码。这不符合需求,因为一个人可能有多个僵尸。
  • 正确写法 (uint => address)

    • 就像你想通过“僵尸ID”这个唯一编号去查它的“主人”是谁。这是一个一对一的完美映射。

如何解决“一个用户拥有多个僵尸”的问题?

这就是为什么你的代码中还需要另一个映射:

1
mapping (address => uint) public ownerZombieCount;

这个映射用来记录每个地址拥有的僵尸数量,它和 zombieToOwner 映射协同工作来完整地管理所有权关系。

总结:映射的键值对设计必须符合你想要查询的逻辑方向。zombieToOwner 的查询起点是僵尸ID,目标是找到其所有者,所以必须是 (uint => address)

代码解析

numbers[msg.sender] = _num; 这行代码的意思是,把msg.sender指向_num还是把_num指向msg.sender?
这是一个很常见的困惑点!让我来帮你彻底搞清楚:

正确答案是:把 _num 的值赋给 msg.sendernumbers 映射中的位置。

更准确地说:_num 指向(赋值给)msg.sender 对应的存储位置。

详细解释:

1
numbers[msg.sender] = _num;

这行代码的执行顺序是从右向左

  1. _num - 这是数据源(要存储的值)
  2. = - 赋值操作符,表示”将右边的值赋给左边”
  3. numbers[msg.sender] - 这是目标位置(要存储的位置)

类比理解:

1
2
3
// 类似于其他编程语言中的:
userScores["Alice"] = 95;
// 意思是:把95赋给Alice的分数

在映射中的具体含义:

  • numbers 是一个映射(mapping)
  • msg.sender 是键(key),表示一个地址
  • _num 是值(value),要存储的数字
  • 整体意思:在 numbers 映射中,为 msg.sender 这个地址设置对应的值为 _num

举例说明:

1
2
3
4
5
6
7
8
mapping(address => uint256) public numbers;

function setNumber(uint256 _num) public {
numbers[msg.sender] = _num; // 把 _num 存储到调用者的位置
}

// 如果 Alice 调用 setNumber(100)
// 那么 numbers[Alice的地址] 就等于 100

简单记法: 总是 右边赋值给左边,就像数学中的 x = 5 是把 5 赋给 x。

msg.sender

现在我们已建立了用于追踪僵尸所有权的映射,接下来需要更新 _createZombie 方法以使用这些映射。

为此,我们需要使用一个名为 msg.sender 的全局变量。

msg.sender
在 Solidity 中,存在一些对所有函数可用的全局变量。其中之一就是 msg.sender,它指向当前函数调用者(个人或智能合约)的地址。

注意:在 Solidity 中,函数的执行始终需要由外部调用者发起。合约只会静静地部署在区块链上,直到有人调用其某个函数时才会执行操作。因此,系统中始终会存在一个 msg.sender。

以下是一个使用 msg.sender 并更新映射的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
// Update our `favoriteNumber` mapping to store `_myNumber` under `msg.sender`
favoriteNumber[msg.sender] = _myNumber;
// ^ The syntax for storing data in a mapping is just like with arrays
}

function whatIsMyNumber() public view returns (uint) {
// Retrieve the value stored in the sender's address
// Will be `0` if the sender hasn't called `setMyNumber` yet
return favoriteNumber[msg.sender];
}

为什么这么写是错误的:zombieToOwner[msg.sender] = id;
这样写是正确的:zombieToOwner[id] = msg.sender;

这是一个非常好的问题,它触及了映射核心逻辑的理解。

核心原因

这两种写法的根本区别在于 “谁作为查询的键(Key)”。这直接由映射 mapping (uint => address) public zombieToOwner; 的定义决定。


错误写法分析:zombieToOwner[msg.sender] = id;

  • 你的意图:想把僵尸 id 分配给主人 msg.sender
  • 代码的实际含义msg.sender 这个地址当作键,去存储一个僵尸ID

这为什么是错的?

  1. 类型不匹配:你的映射定义为 mapping (uint => address)。这意味着键必须是 uint 类型。而 msg.sender 是一个 address 类型,不能用作键。Solidity编译器会直接报类型错误。

  2. 逻辑错误(即使类型匹配)

    • 假设你定义的是 mapping (address => uint) zombieToOwner
    • 那么 zombieToOwner[msg.sender] = id; 的含义是:“地址 msg.sender 现在只拥有僵尸 id
    • 如果同一个用户(msg.sender)创建了第二个僵尸(比如 id=2),执行这行代码就会变成:zombieToOwner[msg.sender] = 2;
    • 后果:用户对僵尸 id=1 的所有权记录被覆盖了!现在查询 zombieToOwner[msg.sender] 只会返回 2,僵尸1变得“无主”。这完全违背了我们的设计。

正确写法分析:zombieToOwner[id] = msg.sender;

  • 你的意图:想把僵尸 id 分配给主人 msg.sender
  • 代码的实际含义id 这个数字当作键,去存储一个所有者地址

这为什么是正确的?

  1. 类型匹配:映射 mapping (uint => address) 的键是 uint,值 addressiduintmsg.senderaddress,完美匹配。

  2. 逻辑正确

    • 每个僵尸的 id全局唯一永不改变的。
    • 这行代码建立了一个从僵尸到主人的单向关系
    • 查询方式:当我想知道“僵尸5是谁的?”时,我就可以去查 zombieToOwner[5],它会返回其主人的地址。
    • 即使用户创建了多个僵尸,每个僵尸ID都会在映射中有一个独立的条目,互不覆盖
      • zombieToOwner[1] = 0x123... (用户0x123…拥有僵尸1)
      • zombieToOwner[2] = 0x123... (用户0x123…也拥有僵尸2)
      • zombieToOwner[3] = 0x456... (用户0x456…拥有僵尸3)

直观类比

把它想象成一个学校的储物柜系统

  • 每个僵尸ID 对应一个储物柜编号
  • 每个所有者地址 对应一个学生

zombieToOwner[id] = msg.sender 就像是:

“把 储物柜编号 id 分配给 **学生 msg.sender**。”

  • 你想知道103号柜子是谁的?查一下 zombieToOwner[103] 就知道了。
  • 一个学生可以拥有多个柜子(比如103和105),但一个柜子只能分配给一个学生。

而错误的写法 zombieToOwner[msg.sender] = id 就像是:

“规定 学生 msg.sender 只能使用 **储物柜 id**。”

  • 如果这个学生之前用过5号柜,现在分配他10号柜,那他就失去了5号柜的使用权。

结论:映射的设计决定了查询方向。zombieToOwner 的设计目的是“通过僵尸找主人”,所以必须用僵尸ID作为键。

storage vs Memory

如果说storage是硬盘存储,那么memory就是RAM
状态变量就是在函数外声明的变量。 状态变量会永久存储在区块链上。

为什么solidity里有时候用两个==,有时候用一个=

好的,这是一个关于 Solidity(以及大多数编程语言)基础语法的核心问题。简单来说:

  • 一个 = 是赋值
  • 两个 == 是比较

1. 一个等号 = :赋值运算符

作用: 将右边的值赋予左边的变量。

可以理解为:“设置为”或“变成”。

示例:

1
2
3
uint x = 5; // 声明变量x,并将其值设置为5
x = 10; // 将变量x的值修改为10
ownerZombieCount[msg.sender] = 1; // 将该地址的僵尸数量设置为1

在这个语境下的例子:
你之前写的 zombieToOwner[id] = msg.sender; 就是一个赋值操作。它的意思是:msg.sender 这个地址“赋予”或“设置”为僵尸 id 的主人。


2. 两个等号 == :比较运算符

作用: 检查左右两边的值是否相等

可以理解为:“等于吗?”或“是否相等”。

它会返回一个 bool 类型(布尔值)的结果:true(真/相等)或 false(假/不相等)。

示例:

1
2
3
4
5
6
7
8
function checkValue(uint a, uint b) public pure returns (bool) {
return a == b; // 如果a等于b,返回true;否则返回false
}

// 使用场景
if (ownerZombieCount[msg.sender] == 0) {
// 如果用户拥有的僵尸数量等于0,则执行这里的代码
}

直观类比

把它想象成一个办理入住的前台:

  • 一个等号 = (赋值)

    前台员工 103号房间的钥匙交给 张三先生。
    roomToGuest[103] = MrZhang;

  • 两个等号 == (比较)

    前台员工检查 想要领取103号房间钥匙的人是不是 张三先生。
    if (msg.sender == MrZhang) { // 才给钥匙 }


为什么不能混淆?

因为编译器无法理解你的意图。

  • 如果你在需要比较的地方错误地使用了 =

    1
    if (x = 5) { ... } // 错误!

    这行代码的意图可能是“如果x等于5”,但它实际做的是“将x的值设为5”,并且整个表达式会返回5。在Solidity中,非0值被视为true,所以这个条件判断永远为真,这很可能是一个严重的逻辑错误。

  • 如果你在需要赋值的地方错误地使用了 ==

    1
    uint x == 5; // 错误!

    编译器会直接报错,因为它期待的是一个赋值操作,而不是一个比较表达式。

总结

运算符 名称 作用 例子 结果
= 赋值 将右边的值存入左边 x = 5 x 变成了 5
== 等于比较 检查两边是否相等 x == 5 如果x5,返回true;否则返回false

记住这个口诀:一个等号是用来“改变”的,两个等号是用来“询问”的。

interface接口

与其他合约的交互
如果我们的合约需要与区块链上其他非自身所有的合约进行交互,首先需要定义一套接口。

让我们来看一个简单示例。假设在区块链上存在这样一个合约:

1
2
3
4
5
6
7
8
9
10
11
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 合约的接口:

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

请注意,接口的定义方式与合约相似,但存在几点关键区别。首先,我们只声明需要交互的函数——本例中的 getNum——而不会涉及合约的其他函数或状态变量。

其次,我们不需要定义函数体。不同于使用花括号({ 和 }),我们直接用分号(;)结束函数声明。

因此,接口看起来像是一个合约的骨架。编译器正是通过这种形式来识别这是一个接口。

通过在我们的去中心化应用中引入此接口,我们的合约就能了解外部合约的函数形式、调用方式以及预期的返回类型。

我们将在下一课详细讲解如何实际调用外部合约的函数,现在让我们先为 CryptoKitties 合约声明接口。

实践练习
我们已为您查阅了 CryptoKitties 的源代码,发现一个名为 getKitty 的函数,它能返回猫咪的所有数据,包括其”基因”(这正是我们的僵尸游戏生成新僵尸所需的素材!)。

该函数的具体定义如下:

(翻译说明:

  1. “contract skeleton” 译为”合约骨架”,形象传达接口的本质特征
  2. “dapp” 采用行业通用译法”去中心化应用”,括号内标注英文缩写
  3. “Put it to the test” 延续之前”实践练习”的译法,保持一致性
  4. “genes” 译为”基因”并添加引号,既准确又体现其特殊游戏属性
  5. 技术细节翻译精准:函数体声明方式、分号使用等关键区别都明确呈现
  6. 语句结构进行合理重组,将英文长句转换为符合中文阅读习惯的短句
  7. 保留所有代码元素 getNumgetKitty 的原貌
  8. 结尾留出代码示例的衔接空间,保持教程的连贯性)