【一步步一起学DApp开发】(三)Solidity语言讲解 | 用Solidity编写智能合约

版权声明:转载请注明出处!喜欢就关注一下or点赞鼓励一下呗 https://blog.csdn.net/ImagineCode/article/details/82431073

有好几种语言可以用于编写以太坊智能合约,不过Solidity是最热门的语言。在本章中,我们将首先学习Solidity编程语言。然后创建一个DApp,用于证明在特定时间的存在、真实性和所有权,即证明一个文件在一个特定时间属于一个特定所有者。

要点:

  • Solidity源文件的布局
  • 理解Solidity的数据类型
  • 合约的特殊变量和函数
  • 控制结构
  • 合约的结构和功能
  • 编译和部署合约

Solidity源文件

Solidity源文件使用的扩展名为.sol。这里面我们使用的是0.4.2版本。

智能合约的结构

合约就像一个类(class),其中包含:

  • 状态变量(state variable)
  • 函数(function)
  • 函数修改器(function modifier)
  • 事件(event)
  • 结构(structure)
  • 枚举(enum)

同时,合约还支持继承与多态。

示例:

contract Sample
{
    //状态变量
    uint256 data;
    address owner;

    //定义事件
    event logData(uint256 dataToLog);
    //函数修改器
    modifier onlyOwner() {
        if(msg.sender!=owner) throw;

    }
    //构造器,名字与合约名一致
    function Sample(uint256 initData,address initOwner) {
        data = initData;
        owner = initOwner;
    }
    //函数
    function getData() returns (uint256 returnedData) {
    return data;    
    }
    function setData() returns (uint256 newData) onlyOwner {
        logData(newData);
        data = newData;
    }

}

代码注释:

  • contract 关键字:用于声明一个合约
  • data和owner:是两个状态变量。data包含一些数据,owner包含所有者的以太坊钱包地址,即部署合约者的以太坊地址
  • event logData 定义事件logData,用于通知客户端:一旦data发生变化,将触发这个事件。所有事件都保存在区块链中。
  • 函数修改器:onlyOwner。修改器用于在执行一个函数之前自动检测文件。这里的修改器用于检测合约所有者是否在调用函数。如果没有,则会抛出异常。

  • 合约函数构造器constructor:在部署合约时,构造器用于初始化状态变量。

  • function,getData()用于得到data状态变量的值,setData()用于改变data的值。

数据位置(较难理解)

通常,变量会存储在内存中。但是,在Solidity中,会根据不同的情况,变量可能会不存储在内存和文件系统中。

通常,在Solidity中,数据有一个默认位置。通常,存储有一个storage位置和一个memory位置,即本地存储与内存存储。

函数参数,包括其返还参数,默认用memory,本地变量默认用storage,例如状态变量,其数据位置强制使用storage。

注意:

  • 不能把memory中存储的复杂类型分配给storage;

什么是不同的数据类型

首先明白3点;

  • Solidity是一种静态类型语言,变量存储的数据类型需要预先定义。
  • 所有变量默认值都是0。
  • 在Solidity中,变量是有函数作用范围的,也就是说,在函数中任何地方声明的变量将对整个函数存在适用范围。

那么Solidity提供了哪些数据类型——》

基本类型

除了数组类型、字符串类型、结构类型、枚举类型和map类型外,
其他类型均称为基本类型。

  • 无符号型:例如uint8,uint16,uint24,…,uint256分别用于存储无符号的8位,16
    位,24位,…,256位整数
  • 有符号型:例如,int8,int16,…,int256分别用于存储8位,16位,24位,…,256位整数
  • address类型:用于存储以太坊地址,用16进制表示。address类型有两个属性:balance和send。balance用于检测地址余额,send用于向地址发送以太币。send方法拿出需要转账那
    些数量的wei,并根据转账是否成功返回true或者false。

注意:

  • uint和int是uint256和int256的别名。
  • 如果一个数字超过256位,则使用256位数据类型存储该数字的近似值。

  • 数组:Solidity支持generic和byte两种数组类型。

数组有length属性,用于发现数组的长度。

注意:不可以在内存中改变数组大小,也不可以改变非动态数组大小。

字符串类型

有两种方法创建字符串:使用bytes和string。
bytes用于创建原始字符串,而string用于创建UTF-8字符串

示例:

contract sample {
    string myString = "";// string
    bytes myRawString;

    function sample(string initString,bytes rawStringInit) {
        myString = initString;

        string storage myString2 = myString;
        string memory myString3 = "ABCDE";
        myString3 = "imaginecode";
        myRawString = rawStringInit;
        myRawString.length++;
    }
}

结构类型struct

示例

contract sample {
    struct myStruct {
        bool myBool;
        string myString;
    }
    myStruct s1;
    myStruct s2 = myStruct{true,""}; 

    function sample(bool initBool,string initString){
        s1 = myStruct(initBool,initString);

        myStruct memory s3 = myStruct(initBool,initString);
    }
}

注意:函数参数不可以是结构类型,且函数不可以返回结构类型。

枚举类型 enum

示例

contract sample {
    enum OS {OSX, Linux,Unix,windows }

    OS choice;

    function sample(OS chosen) {
        choice = chosen;
    }
    function setLinux() {
        choice = OS.Linux;
    }
    function getChoice return (OS chosenOS) {
        return choice;
    }
}

mapping 类型

  • mapping类型只可以存在于storage中,不存在于memory中,因此它们是作为状态变量声明的。

  • mapping类型包含key/value对,不是实际存储key,而是存储key的keccak256哈希,用于查询value。

  • mapping不可以被分配给另一个mapping。

constract sample {
    mapping (int => string) myMap;
    function sample(){
        myMap[key] = value;
        mapping (int => string) myMap2 = myMap;
    }
}

注意:如果想访问mapping中不存在的key,返回的value为0。

delete 操作符

可用于操作任何类型的变量。

  • 对动态数组使用delete操作符,则删除所有元素,其长度变为0。

  • 对静态数组使用delete操作符,则重置所有索引

  • 对map类型使用delete操作符,什么都不会发生,但是,对map类型的一个键使用delete操作符,则会删除与该键相关的值

示例

contract sample {
    struct Struct {
        mapping (int => int) myMap;
        int myNumber;
    }
    int[] myArray;
    Struct myStruct;

    function sample(int key,int value,int number,int[] array) {
        myStruct = Struct(number);

        myStruct = Struct(number);
        myStruct.myMap[key] = value;//对某个键赋值
        myArray = array;
    }
    function reset() {
        delete myArray;//myArray数组长度为0
        delete myStruct;//myNumber为0,myMap不变
    }
    function deleteKey(int key) {
            delete myStruct.myMap[key];//删除myMap中的某个键的值
    }
}

基本类型之间的转换

  • 隐式转换:常用。通常来说,如果没有语义信息丢失,值和类型之间可以进行隐式转换:uint8可转换为uint16,int128可转换为int256,但是int8不可转换为uint256(因为uint256不能存储,例如-1)
  • Solidity也支持显式转换,如果编译器不允许在两种数据类型之间隐式转换,则可以进行显式转换。建议尽量避免显式转换,因为可能返回难以预料的结果。

示例:

uint32 a = 0x12345678;
uint16 b = uint16(a); // b = 0x5678,将uint32类型显式转换为uint16,也就是说,把较大类型转换为较小类型,因此高位被截掉了

var

使用关键字var声明的变量,其变量类型根据分配给它的第一个值来动态确定。一旦分配了值,类型就固定了,所以如果给它指定另一个类型,将引起类型转换。

int256 x= 12;
var y = x;//此时y的类型是int256

uint256 z = 9;
y = z;//此时,报出异常,因为uint256不能转换为int256类型

但要注意的是:

  • 在定义数组array和map时不能使用var。var也不能用于定义函数参数和状态变量

控制结构

  • if-else
  • while
  • for
  • break\continue
  • return
  • ?:
  • 等等
//结构上和其他语法没有什么差异
contract sample {
    int a = 12;
    int[] b;

    function sample() {
        if(a == 12) {}
        else if(a==34){}
        else {}
        var temp = 10;
        while(temp<20)
        {
            if(temp==17){break;}
            else {continue;}
        }
        temp++;
    }
    for(var m=0;m<b.length;m++){

    }
}

用new 操作符创建合约

一个合约可以使用new关键字来创建一个新合约。

例如:

contract sample1 {
    int a;
    function assign(int b){
        a = b;
    }
}

contract sample2 {
    function sample2(){
        sample1 s = new sample1(); //注意写法
        s.assign(12);
    }
}

异常

异常的抛出分为自动和手动。
若你想手动抛出异常,可以使用throw手动抛出。
注意,异常抛出后,会撤销对状态和余额的所有改变。

contract sample {
    function myFunction () {
        throw;
    }
}

函数调用

  • 内部函数调用:一个函数在同一个合约中调用另一个函数
  • 外部函数调用:一个函数调用另一个合约的函数。

外部函数调用–this关键字

合约sample1

contract sample1 {
    int a;
    function sample1(int b) payable {
        a = b;
    }
    function assign(int c){
        a = c;
    }
    function makePayment(int d) payable {
        a = d;
    }
}

合约sample2

contract sample2 {
    function hello() {}
    function sample2(address addressOfContract){
        sample1 s = (new sample1).value(12)(23);
        s.makePayment(22);
        s.makePayment.value(45)(12);
        s.makePayment.value(4).gas(900)(12);
        this.hello(); //利用this调用外部合约函数
        sample1 s2 = sample1(addressOfContract);
        s2.makePayment(112);
    }
}

注意:使用this关键字进行的调用称为外部调用。在函数中,this关键字代表当前合约实例

合约功能——深入理解合约

可见性

可见性定义了谁可以看到它,函数和状态变量有四种可见性:external、public、internal和private

  • 函数可见性,默认为 public
  • 状态变量可见性,默认为 internal

  • external:外部函数只能由其他合约调用,或者通过交易调用——this.f()

  • public:公共函数和状态变量可以用所有可行办法访问
  • internal:内部函数和状态变量只可以内部访问,即从当前合约内和继承它的合约访问。不可以使用this访问它
  • private:私有函数和状态变量类似于内部函数,但是继承合约不可以访问它们

示例

contract sample1 {
    int public b = 78;
    int internal c = 90;

    function sample1() {
        this.a();//外部访问
        b = 21;//内部访问

    }
    function a() external {}
}

contract sample2 {
    int internal d = 9;
    int private e = 90;
}
//sample3 继承 sample2
contract sample3 is sample2 {
    sample1 s;
    function sample3() {
        s = new sample1();
        s.a();//外部访问
    }
}

函数修改器(较难理解)

先看一个修改器的例子:

contract sample {
    int a = 90;
    modifier myModifier1(int b) {
        int c = b;
        _;
        c = a;
        a = 1;
    }
    modifier myModifier2 {
        int c = a;
        _;

    }
    modifier myModifier3 {
        a = 96;
        return;
        _;
        a = 99;
    }
    modifier myModifier4 {
        int c = a;
        _;
    }
    function myFunction() myModifier1(a) myModifier2 myModifier3 returns (int d) {
        a = 2;
        return a;
    }

}

注:

  • 在修改器中,无论下一个修改器体或者函数体二者哪个先到达,都会被插入到“_;”出现的地方。

回退函数

即一个合约中唯一一个未命名函数。

  • 不能有实参
  • 不能有返回值
  • 如果其他函数都不能匹配给定的函数标识符,那么就执行回退函数
  • 如果你想让你的合约接收以太币,就必须实现回退函数
contract sample {
    function() payable {

    }
}

继承

即使一个合约继承自其他多个合约,在区块链上也只会创建一个合约。
父合约的代码总是会被复制到最终合约里。

  • 关键字 is

示例

contract sample1 {
    function a(){}
    function b() {}
}
//合约2继承自合约1
contract sample2 is sample1{
    function b() {}
}
contract sample3{
    function sample3(int b){
    }
}
//合约4继承自合约1与合约2
contract sample4 is sample1,sample2 {
    function a(){}
    function c() {
        a();
        //执行合约1中的a方法
        sample1.a();
        //执行合约2中的b方法
        b();
    }
}

关键字super

  • 用于引用最终继承链中的下一个合约
    示例
contract sample1{
}
contract sample2{
}
contract sample3 is sample2{
}
contract sample4 is sample2{
}
contract sample5 is sample4{
    function myFunc(){}
}
contract sample6 is sample1,sample2,sample3,sample4,sample5 {
    function myFunc() {
    //执行sample5中的myFunc方法
        super.myFunc();
    }
}

抽象合约

  • 仅包含函数原型而不包含函数实现的合约
  • 抽象合约不能被编译
  • 如果一个合约继承自抽象合约且不重写,那么它自己也是抽象合约

示例

contract sample1{
    function a() returns (int b);
}
contract sample2{
    function myFunc(){
        sample1 s = sample();
        s.a();
    }
}

库的目的是在一个特定地址中只部署一次,其其代码可供复用。

示例-使用solidity的math库:

library math
{
    function addInt(int a,int b) return (int c){
        return a+b;
    }
}
contract sample {
    function data() returns (int d){
        return math.addInt(1,2);//调用math库中的addInt方法
    }
}

使用场景

  • 如果有许多合约,这些合约有一些共同的代码,那么可以把它们共同的代码部署成一个库。这么做的好处是这样能节省gas。因为gas的大小依赖于合约的规模。

返回多个值

示例:

contract sample{
    function a() returns (int a,string c){
            return (1,"m");
    }
    function b(){
        int A;
        string memory B;

        (A,B) = a();// A =1,B = "m"
        (A,)  = a();// A =1
        (,B) = a(); //B = "m"
    }
}

全局变量

  • 特殊变量
  • 特殊函数

1、区块和交易属性

  • block.blockhash(uint blockNumber) returns (bytes32) //区块哈希值
  • block.coinbase(address) //当前区块矿工的地址
  • block.difficulty(uint) //当前区块的难度值
  • block.gaslimit(uint) //当前区块的gas上限,定义了整个区块中的所有交易最多能消耗多少gas
  • block.number(uint) //当前区块的序号
  • block.timestamp(uint) //当前区块的时间戳
  • msg.gas(uint) //当前剩余的gas
  • msg.sender(address) //当前调用发起人的地址
  • msg.sig(bytes4) //调用数据的前四个字节
  • msg.value(uint) //这个消息所附带的货币量,单位为wei
  • now(uint) //当前区块的时间戳,等同于block.timestamp
  • tx.gasprice(uint) //交易的gas价格
  • tx.origin(address) //交易的发起人

2、地址类型相关

  • .balance(uint256) //地址余额,单位为wei
  • .send(uint256 amount) returns(bool) //发送指定数量的wei到地址

3、合约相关

  • this //当前合约
  • selfdestruct(address recipient) //销毁当前合约,把其中的资金发送到指定地址

以太币单位

一个数字可以用wei、finney、szabo、Ether等单位转换为不同面值的以太币。默认使用wei为单位。

存在、真实性和所有权合约的证明

下面我们要实现一个“证明文件所有权”的合约。

分以下几步来进行:

  • 1、成对存储文件的哈希和文件所有者的名字,用以实现所有权证明(PoO)
  • 2、成对存储文件的哈希和区块的时间戳,用以实现文件在某个特定时间存在的证明(PoE)
  • 3、存储哈希自身,用以证明文件的真实性。如果文件被修改,其哈希也会被改变。更改过的文件的哈希会使得合约无法发现文件,从而证明文件被修改过。

代码如下:

contract Proof{
    struct FileDetails {
        uint timestamp;
        string owner;
    }
    mapping (string => FileDetails) files;

    event logFileAddedStatus(bool status,uint timestamp,string owner,string fileHash);

    //存储文件所有者
    function set(string owner,string fileHash){
        if(files[fileHash].timestamp==0){
            files[fileHash] = FileDetails(block.timestamp,owner);
            //触发一个事件以至于前端应用知道文件的存在
            logFileAddedStatus(true,block.timestamp,owner,fileHash);
        }else {
        //告诉前端文件存在,但是不能存储
            logFileAddedStatus(false,block.timestamp,owner,fileHash);
        }
    }
    //获取文件信息
    function get(string fileHash) returns (uint timestamp,string owner){
        return (files[fileHash].timestamp,files[fileHash].owner);
    }
}

编译和部署合约

这里,我们使用solcjs和Browser Solidity ,其中solcjs允许在node.js中以编程方式编译Solidity,而Browser Solidity是一个适用于小型合约的IDE。

至此,我们将Solidity语言进行了基本的讲解,下一节中我们将介绍如何使用web3.js开发DApp前端。

猜你喜欢

转载自blog.csdn.net/ImagineCode/article/details/82431073