首页
论坛
专栏
课程

[转帖][推荐]目前见到的学习solidity语法最好的一个案例

roysue
3
2018-8-10 16:50 863

原文地址: 一步步构建自己的智能合约 https://segmentfault.com/a/1190000014797663

一、合约

所有的 Solidity 源码都必须冠以 "version pragma" — 标明 Solidity 编译器的版本. 以避免将来新的编译器可能破坏你的代码。

例如:pragma solidity ^0.4.19;(当前 Solidity 的最新版本是 0.4.19).

综上所述, 下面就是一个最基本的合约 — 每次建立一个新的项目时的第一段代码:

pragma solidity ^0.4.19;

contract HelloWorld {

}

为了建立我们的僵尸部队, 让我们先建立一个基础合约,称为 ZombieFactory。

pragma solidity ^0.4.19;

contract ZombieFactory {
    
}

!我们已经为我们的合约做了一个外壳, 下面学习 Solidity 中如何使用变量。

状态变量是被永久地保存在合约中。也就是说它们被写入以太币区块链中. 想象成写入一个数据库。

无符号整数:uint

uint无符号数据类型, 指其值不能是负数,对于有符号的整数存在名为int的数据类型。

注: Solidity中,uint实际上是uint256代名词, 一个256位的无符号整数。你也可以定义位数少的uints —uint8,uint16,uint32, 等

我们的僵尸DNA将由一个十六位数字组成。

pragma solidity ^0.4.19;

contract ZombieFactory {
    uint dnaDigits = 16;
}

1.1 数值运算

在 Solidity 中,数学运算很直观明了,与其它程序设计语言相同:

加法:x + y

减法:x - y,

乘法:x * y

除法:x / y

取模 / 求余:x % y(例如, 13 % 5 余 3, 因为13除以5,余3)

Solidity 还支持乘方操作(如:x 的 y次方) // 例如: 5 ** 2 = 25

uint x =5**2;// equal to 5^2 = 25

为了保证我们的僵尸的DNA只含有16个字符,我们先造一个uint数据,让它等于10^16。这样一来以后我们可以用模运算符 % 把一个整数变成16位。

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
}

1.2 结构体

有时你需要更复杂的数据类型,Solidity 提供了结构体:

struct Person {
  uint age;
  string name;
}

结构体允许你生成一个更复杂的数据类型,它有多个属性。

注:我们刚刚引进了一个新类型,string。 字符串用于保存任意长度的 UTF-8 编码数据。 如:string greeting = "Hello world!"。

在我们的程序中,我们将创建一些僵尸!每个僵尸将拥有多个属性,所以这是一个展示结构体的完美例子。 solidity中的结构体要声明在contract内部

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    // 这里开始
    struct Zombie {
        string name;
        uint dna;
    }
}

1.3 数组

如果你想建立一个集合,可以用数组这样的数据类型. Solidity 支持两种数组:静态数组和动态数组:

// 固定长度为2的静态数组:
uint[2] fixedArray;
// 固定长度为5的string类型的静态数组:
string[5] stringArray;
// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;

你也可以建立一个结构体类型的数组 例如,上一章提到的Person:

Person[]people; // dynamic Array, we can keep adding to it

记住:状态变量被永久保存在区块链中。所以在你的合约中创建动态数组来保存成结构的数据是非常有意义的。

公共数组

你可以定义public数组, Solidity 会自动创建getter方法. 语法如下:

Person[]public people;

其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。

为了把一个僵尸部队保存在我们的APP里,并且能够让其它APP看到这些僵尸,我们需要一个公共数组。

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;
}

1.4 定义函数

在 Solidity 中函数定义的句法如下:

function eatHamburgers(string _name, uint _amount) {

}

这是一个名为eatHamburgers的函数,它接受两个参数:一个string类型的 和 一个uint类型的。现在函数内部还是空的。

注:: 习惯上函数里的变量都是以(_)开头 (但不是硬性规定) 以区别全局变量。我们整个教程都会沿用这个习惯。

我们的函数定义如下:

eatHamburgers("vitalik",100);

在我们的应用里,我们要能创建一些僵尸,让我们写一个函数做这件事吧!

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        uint dna;
        string name;
    }

    Zombie[] public zombies;

    function createZombie(string _name, uint _dna) {
        
    }
}

1.5 创建新的结构体

还记得上个例子中的Person结构吗?

struct Person {
  uint age;
  string name;
}

Person[] public people;

现在我们学习创建新的Person结构,然后把它加入到名为people的数组中.

// 创建一个新的Person:
Person satoshi = Person(172, "Satoshi");

// 将新创建的satoshi添加进people数组:
people.push(satoshi);

你也可以两步并一步,用一行代码更简洁:

people.push(Person(16,"Vitalik"));
注:array.push()在数组的尾部加入新元素 ,所以元素在数组中的顺序就是我们添加的顺序, 如:
uint[] numbers;
numbers.push(5);
numbers.push(10);
numbers.push(15);
// numbers is now equal to [5, 10, 15]

让我们创建名为createZombie的函数来做点儿什么吧。

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function createZombie(string _name, uint _dna) {
        // 这里开始
        Zombie z = Zombie(_name, _dna);
        zombies.push(z);
    }

}

1.6 私有 / 公共函数

Solidity 定义的函数的属性默认为公共。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。

显然,不是什么时候都需要这样,而且这样的合约易于受到攻击。 所以将自己的函数定义为私有是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共。

如何定义一个私有的函数呢?

uint[] numbers;

function _addToArray(uint _number) private {
  numbers.push(_number);
}

这意味着只有我们合约中的其它函数才能够调用这个函数,给numbers数组添加新成员。

可以看到,在函数名字后面使用关键字private即可。和函数的参数类似,私有函数的名字用(_)起始。

我们合约的函数 createZombie 的默认属性是公共的,这意味着任何一方都可以调用它去创建一个僵尸。 咱们来把它变成私有吧!

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

}

1.7 函数的更多属性

本章中我们将学习函数的返回值和修饰符。

返回值

要想函数返回一个数值,按如下定义:

string greeting = "What's up dog";

function sayHello() public returns (string) {
  return greeting;
}

Solidity 里,函数的定义里可包含返回值的数据类型(如本例中string)。

函数的修饰符

上面的函数实际上没有改变 Solidity 里的状态,即,它没有改变任何值或者写任何东西。

这种情况下我们可以把函数定义为view, 意味着它只能读取数据不能更改数据:

functionsayHello()publicviewreturns(string){

Solidity 还支持pure函数, 表明这个函数甚至都不访问应用里的数据,例如:

function _multiply(uint a, uint b) private pure returns (uint) {
  return a * b;
}

这个函数甚至都不读取应用里的状态 — 它的返回值完全取决于它的输入参数,在这种情况下我们把函数定义为pure.

注:可能很难记住何时把函数标记为 pure/view。 幸运的是, Solidity 编辑器会给出提示,提醒你使用这些修饰符。

我们想建立一个帮助函数,它根据一个字符串随机生成一个DNA数据。

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }
    
    function _generateRandomDna(string _str) private view returns (uint) {
        
    }
}

1.8 Keccak256 和 类型转换

如何让_generateRandomDna函数返回一个全(半) 随机的uint?

Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。

这在 Ethereum 中有很多应用,但是现在我们只是用它造一个伪随机数。

例子:

//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256("aaaab");
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256("aaaac");

显而易见,输入字符串只改变了一个字母,输出就已经天壤之别了。

注: 在区块链中安全地产生一个随机数是一个很难的问题, 本例的方法不安全,但是在我们的Zombie DNA算法里不是那么重要,已经很好地满足我们的需要了。

类型转换

有时你需要变换数据类型。例如:

uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);

上面,a * b返回类型是uint, 但是当我们尝试用uint8类型接收时, 就会造成潜在的错误。如果把它的数据类型转换为uint8, 就可以了,编译器也不会出错。

给_generateRandomDna函数添加代码! 它应该完成如下功能:

第一行代码取_str的keccak256散列值生成一个伪随机十六进制数,类型转换为uint, 最后保存在类型为uint名为rand的变量中。

我们只想让我们的DNA的长度为16位 (还记得dnaModulus?)。所以第二行代码应该return上面计算的数值对dnaModulus求余数(%)。

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

}

1.9 放在一起

我们就快完成我们的随机僵尸制造器了,来写一个公共的函数把所有的部件连接起来。

写一个公共函数,它有一个参数,用来接收僵尸的名字,之后用它生成僵尸的DNA。

创建一个public函数,命名为createRandomZombie. 它将被传入一个变量_name(数据类型是string)。(注: 定义公共函数 public 和定义一个私有 private 函数的做法一样)。

函数的第一行应该调用_generateRandomDna函数,传入_name参数, 结果保存在一个类型为uint的变量里,命名为randDna。

第二行调用_createZombie函数, 传入参数:_name和randDna。

整个函数应该是4行代码 (包括函数的结束符号})。

pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    // 事件
    pragma solidity ^0.4.19;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string _name, uint _dna) private {
        zombies.push(Zombie(_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);
    }   
}

1.10 : 事件

我们的合约几乎就要完成了!让我们加上一个事件.

事件是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。

例子:

// 这里建立事件
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
  uint result = _x + _y;
  //触发事件,通知app
  IntegersAdded(_x, _y, result);
  return result;
}

你的 app 前端可以监听这个事件。JavaScript 实现如下:

YourContract.IntegersAdded(function(error, result) { 
  // 干些事
}

我们想每当一个僵尸创造出来时,我们的前端都能监听到这个事件,并将它显示出来。

1。 定义一个事件叫做NewZombie。 它有3个参数:zombieId(uint),name(string), 和dna(uint)。

2。 修改_createZombie函数使得当新僵尸造出来并加入zombies数组后,生成事件NewZombie。

3。 需要定义僵尸id。array.push()返回数组的长度类型是uint- 因为数组的第一个元素的索引是 0,array.push() - 1将是我们加入的僵尸的索引。zombies.push() - 1就是id,数据类型是uint。在下一行中你可以把它用到NewZombie事件中。

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;

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

二、支持多用户啦!

2.1 映射(Mapping)和地址(Address)

我们通过给数据库中的僵尸指定“主人”, 来支持“多玩家”模式。

如此一来,我们需要引入2个新的数据类型:mapping(映射) 和address(地址)。

Addresses (地址)

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

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

0x21110f92fd4e5b09d1237e6d30a2a8d733cd154b

(如果你喜欢本课程的话,请打赏我一些以太币!

[防守篇]2018看雪.TSRC CTF 挑战赛(团队赛)11月1日征题开启!

最新回复 (1)
duolaa朗 2018-10-23 11:18
2

0

https://cryptozombies.io/
返回