第06课:Solidity

第06课:Solidity 详解

本文将重点讲解以太坊智能合约开发语言:Solidity。本篇内容较多,希望大家耐心看完。

大家先看下面这段代码。我将结合这段代码讲解 Solidity 相关知识点。

pragma solidity ^0.4.17;
//学校合约
contract School{

    //班级信息结构体
    struct Class {
        string name;  //班名
        address classAddress;  //班费钱包地址
        bool exist;   //是否存在。方便require()方法判断
    }

    //班号/班级信息对象映射关系
    mapping (uint => Class) classList;

    //班级数量
    uint classNumber = 0;

    //学生个人信息结构体
    struct Student {
        uint classId; //班号
        string name; //姓名
        uint height; //身高
        uint weight; //体重
        uint age;  //年龄
        string homeAddress; //学生家庭地址
        bool exist;   //是否存在。方便require()方法判断
    }

    //班级/学号/学生个人信息对象映射关系
    mapping (uint => mapping (uint => Student)) studentsList;

    //班级/学生数量
    mapping (uint => uint) classStudentsNumber;

    //写入班级信息
    function setClass(uint classId, string className, address classAddress) public {
        //校验班号是否已存在
        require(classList[classId].exist == false, '无效参数');
        Class memory class = Class({
            name: className,
            classAddress: classAddress,
            exist: true
        });
        classList[classId] = class;
        //自增班级数量
        classNumber++;
    }

    //读取班级信息
    function getClass(uint classId) public view returns (uint, string, address) {
        //校验classId是否大于0
        require(classId > 0, '无效参数');
        //校验班号是否存在
        require(classList[classId].exist == true, '无效参数');
        Class memory class = classList[classId];
        //返回班号 班级名称 班费地址
        return (classId, class.name, class.classAddress);
    }

    //写入学生个人信息
    function setStudent(uint classId, uint studentId, string studentName, uint height, uint weight, uint age, string homeAddress) public {
        //校验classId是否存在
        require(classList[classId].exist == true, '无效参数');
        //校验studentId是否已存在
        require(studentsList[classId][studentId].exist == false, '无效参数');
        Student memory student = Student({
            classId: classId,
            name: studentName,
            height: height,
            weight: weight,
            age: age,
            homeAddress: homeAddress,
            exist: true
        });
        studentsList[classId][studentId] = student;
        //自增对应班级的学生数量
        classStudentsNumber[classId]++;
    }

    //读取学生个人信息
    function getStudent(uint classId, uint studentId) public view returns (uint, string, uint, uint, uint, string) {
        //校验classId是否大于0
        require(classId > 0, '无效参数');
        //校验classId是否存在
        require(classList[classId].exist == true, '无效参数');
        //校验studentId是否大于0
        require(studentId > 0, '无效参数');
        //校验studentId是否存在
        require(studentsList[classId][studentId].exist == true, '无效参数');
        Student memory student = studentsList[classId][studentId];
        //返回学生所属的班号  学生姓名  身高  体重  年龄  家庭地址
        return (student.classId, student.name, student.height, student.weight, student.age, student.homeAddress);
    }

    //读取班级数量
    function getClassNumber() public view returns (uint) {
        return classNumber;
    }

    //读取某个班级的学生总数
    function getStudentNumber(uint classId) public view returns (uint) {
        //校验classId是否大于0
        require(classId > 0, '无效参数');
        //校验classId是否存在
        require(classList[classId].exist == true, '无效参数');
        return classStudentsNumber[classId];
    }

}


//函数类型  internal / external   默认是internal
//在当前合约中,有两种方式可以访问一个方法:直接使用方法名 f(内部调用),或使用 this.f(外部调用)

Solidity 是一种静态类型语言,每个变量在定义时都需要指定变量类型。 上面这份代码中用到了几种基本类型,也有较复杂的组合类型。

类型

布尔类型

bool exist;   //是否存在。方便require()方法判断

bool 取值范围为常量 true 和 false 。布尔类型常见于表达式运算结果,这里顺便介绍下常见的运算符。

  • !:逻辑非
  • &&:逻辑与,and
  • ||:逻辑或,or
  • ==:等于
  • !=:不等于

运算符 ||&& 都遵循短路原则,即在表达式 f(x) || g(y) 中,如果左边 f(x) 的值为 true,那么右边 g(y) 就不会执行,无论其值是 true 还是 false。

整型

uint classNumber = 0;

uint 是无符号整型,int 为有符号整型。两者都支持关键字 uint8/int8uint256/int256 位数的整型变量(以 8 位为步长递增)。

需要注意的是,uint 和 int 分别是 uint256 和 int256 的别名。

地址类型

address classAddress;  //班费钱包地址

address 地址类型存储了一个 20 字节的值(和以太坊地址大小相当)。

address 类型有两个成员变量:balance 和 transfer。可以通过 <address>.balance 来查询某个地址的余额(以 wei 为单位),也可以通过 <address>.transfer 函数向某个地址发送 ETH(以 wei 为单位),示例代码如下。

address owner=0x616874DC6cc2810CdC930DEA26496FcF217D58cA;

function getOwnerBalance() public view returns (uint){
    return owner.balance; //获取owner地址余额
}

function transfer(address receiver) public {
    receiver.transfer(100000); //合约向receiver转账 100000 wei的以太币
}

数组

数组可以在声明时指定长度,也可以动态调整大小。假设类型为 uint,固定长度为 10 的一维数组可以声明为 uint[10],动态一维数组声明为 uint[]

同时,也支持多维数组的定义。假设类型为 uint,第一维长度不固定,第二维长度固定为 10 的多维数组可以声明为 uint[10][] 。需要注意的是,多维数组的定义方式与绝大多数编程语言刚好相反,但使用下标访问元素时与其它编程语言是一致的。假设我们需要访问第 2 个数组的第 3 个元素,可通过 variable[1][2]

不论是定长数组,还是动态数组,都可以通过 .length 属性来获取数组长度:

uint[5] test=[1,2,3,4,5];  //定长数组
for(uint i=0; i<test.length; i++){
    //do something
}

以上代码中的 uint[5] test=[1,2,3,4,5]; 是 Storage 数组。对于申明为 Storage 的数组,元素可以是任意类型;对于申明为 Memory 的数组,元素不能是 Mapping 类型(Storage 和 Memory 类型下文会介绍)。

申明为 Storage 的动态数组可以通过 .length 属性重新赋值改变其长度,也可以通过 .push 方法将新元素添加到数组末尾:

uint[] test=[1,2,3,4,5]; //动态数组

function setArrLength() public {
    test.length = 10;
}

function getArrLength() public view returns (uint) {
    return test.length;  //10
}

function pushArr() public {
    test.push(6);
}

对于 Memory 数组,我们可以通过如下方式创建:

function createMemoryArray() {
    //a.length等于5
    uint[] memory a = new uint[](5);
}

字节数组类型

字节数组类型分为定长字节数组和变长字节数组。关键字为 bytes。

定长字节数组

定长字节数组是指关键字为 bytes1 至 bytes32 的字节数组变量(以 1 位为步长递增)。需要注意的是,byte 是 bytes1 的别名。

变长字节数组

变长字节数组和定长字节数组相比,只是没有确定的位数。

不论是定长字节数组还是变长字节数组,都可以通过 .length 属性获取数组长度:

bytes memory b = new bytes(7);
b.length; //7

字符串类型

String 类型可以理解为特殊的变长字节数组类型,采用的编码为 UTF-8。

映射类型

//班级/学生数量
mapping (uint => uint) classStudentsNumber;

映射类型可以理解为哈希表,其声明形式为 mapping(_keyType => _valueType)_keyType 可以是除了 Mapping 、Bytes、Enum、Struct 以外的几乎所有类型。_valueType 可以是包含 Mapping 类型在内的任何类型。示例如下:

//班号/班级信息对象映射关系
mapping (uint => Class) classList;
//班级/学号/学生个人信息对象映射关系
mapping (uint => mapping (uint => Student)) studentsList;

仔细阅读上面的完整代码,会发现我们在统计班级数量时,单独用了一个统计变量来计数:

//班级数量
uint classNumber = 0;

这是因为 Mapping 映射类型没有 .length 属性,所以无法像 Bytes 数组类型一样直接获取长度。

结构体

    //班级信息结构体
    struct Class {
        string name;  //班名
        address classAddress;  //班费钱包地址
        bool exist;   //是否存在。方便require()方法判断
    }

我们可以使用关键字 struct 来定义结构体。结构体内可以包含 String、Uint/Int、Bool、Mapping 等数据类型。

值类型和引用类型

有其他语言开发经验的小伙伴对于值类型和引用类型一定不陌生,这里不再赘述。

上面简单介绍了 Solidity 中的值类型,包含布尔、整型、地址、定长字节数组和枚举;引用类型包含:变长字节数组、字符串、数组和结构体。

存储类型

在 Solidity 中,有两个地方可以存储变量: Storage 或 Memory。

Storage 是指永久存储在区块链上的变量,Memory 则是临时变量,当外部函数对某个合约调用完成时,内存型变量即被移除。可以把它们俩想像成存储在电脑硬盘或是内存中的数据。在函数外声明的状态变量默认为 Storage ,并永久写入区块链:

//班级信息结构体
struct Class {   
    string name;  //班名
    address classAddress;  //班费钱包地址
    bool exist;   //是否存在。方便require()方法判断
}

//班号/班级信息对象映射关系
mapping (uint => Class) classList;

在函数内部声明的变量默认是 Memory,在函数调用结束后立即回收:

//读取班级信息
function getClass(uint classId) public view returns (uint, string, address) {
    //校验classId是否大于0
    require(classId > 0, '无效参数');
    //校验班号是否存在
    require(classList[classId].exist == true, '无效参数');
    Class memory class = classList[classId];
    //返回班号 班级名称 班费地址
    return (classId, class.name, class.classAddress);
}

大多数时候我们都用不到这些关键字,默认情况下 Solidity 会自动处理它们。然而也有一些情况下,需要我们手动声明存储类型,主要用于处理函数内的结构体数组时。

require()assert()

细心的读者可能会发现上面代码中在条件校验的地方使用了 require() 函数:

//校验classId是否大于0
require(classId > 0, '无效参数');

require() 函数用于检查条件,如果条件不满足则抛出异常,并停止执行。以上面的代码为例,我们的本意是想判断 classId 小于等于 0 时,则抛出“无效参数”的异常。但我们在 require() 函数中写条件表达式的时候,需要取反。

solidity 还提供了另一个和 require() 类似的函数 assert()assert() 函数通常用来检查内部错误,若结果为否它就会抛出错误。比如数组下标越界或数值溢出等等;而 require() 函数则用来检查输入变量或合约状态变量是否满足条件,以及验证调用外部合约的返回值等等。assert()require() 两者的区别在于,require() 若失败则会返还给用户剩下的 Gas,assert() 则不会。所以大部分情况下,我们写代码时比较喜欢用 require()assert() 只在代码可能出现严重错误的时候使用,比如 Uint 溢出。

function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
}

函数调用方式

Solidity 中有两种函数调用方式: Internal 和 External,即内部调用和外部调用。

内部调用(Internal)

内部调用是指在合约的函数内直接调用其它函数的方式,示例代码如下:

//读取班级信息
function getClass(uint classId) public view returns (uint, string, address) {
    //校验classId是否大于0
    require(classId > 0, '无效参数');
    //校验班号是否存在
    require(classList[classId].exist == true, '无效参数');
    Class memory class = classList[classId];
    //返回班号 班级名称 班费地址
    return (classId, class.name, class.classAddress);
}

function showClass(uint classId) public view {
    getClass(classId);  //内部调用的方式
}

外部调用(External)

外部调用是指被合约的外部消息所调用(向目标合约发送了一个消息调用),所以在合约初始化的时候,不能以此方式调用自身合约内的函数,因为自身合约尚未初始化完成,示例代码如下:

pragma solidity ^0.4.0;

contract School {
    function getSchool() {}
}

contract Class {
    function getSchoolInfo(School school) {
        school.getSchool(); //外部调用的方式
    }
}

我们也可以在合约内通过 this. 来强制以外部调用的方式调用函数。需要注意的是,这里的 this. 含义与绝大多数语言都不一样,示例代码如下:

//读取班级信息
function getClass(uint classId) public view returns (uint, string, address) {
    //校验classId是否大于0
    require(classId > 0, '无效参数');
    //校验班号是否存在
    require(classList[classId].exist == true, '无效参数');
    Class memory class = classList[classId];
    //返回班号 班级名称 班费地址
    return (classId, class.name, class.classAddress);
}

function showClass(uint classId) public view {
    this.getClass(classId);  //强制以外部调用的方式
}

可见性类型

Solidity 中的可见性类型和绝大多数面向对象编程语言里的可见性类型类似,可按此理解并举一反三。

状态变量可见性类型

下面先来介绍下状态变量的可见性类型。Solidity 中状态变量有三种可见性类型:public、internal 和 private,默认情况下是 internal。

public

public 类型的状态变量访问权限最大,可被合约内部、子合约以及外部直接访问:

//父合约School
contract School {
    //状态变量age声明为public
    uint public age=16;

    function getAge() public view returns (uint) {
        return age;  //16
    }
}
//Class子合约继承自School父合约
contract Class is School{
    //返回School父合约中的状态变量age
    function getParentAge() public view returns (uint) {
        return age;   //16
    }
}

需要注意的是,被声明为 public 的状态变量,编译器会自动为此状态变量生成一个同名的 getter 函数:

//班级数量
uint public classNumber = 0;
//编译器自动生成同名函数
function classNumber() returns (uint) {
    return classNumber;
}
internal

默认情况下,状态变量的可见性类型是 internal 。被声明为 internal 的状态变量,可供合约内部以及子合约访问:

//父合约School
contract School {
    //状态变量age声明为internal
    uint internal age=16;

    function getAge() public view returns (uint) {
        return age;  //16
    }
}
//Class子合约继承自School父合约
contract Class is School{
    //返回School父合约中的状态变量age
    function getParentAge() public view returns (uint) {
        return age;   //16
    }
}
private

声明为 private 的状态变量,只能在合约内部访问:

//父合约School
contract School {
    //状态变量age声明为private
    uint private age=16;

    function getAge() public view returns (uint) {
        return age;  //16
    }
}
//Class子合约继承自School父合约
contract Class is School{
    function getParentAge() public view returns (uint) {
        return age;  //报错 DeclarationError: Undeclared identifier.
    }
}

函数可见性类型

函数有四种可见性类型:external、public、internal 和 private,默认情况下函数类型为 public。

external

声明为 external 的函数可以理解为合约对外的接口,必须以外部调用的方式进行调用:

//读取班级信息
//函数已被声明为 external
function getClass(uint classId) external view returns (uint, string, address) {
    //校验classId是否大于0
    require(classId > 0, '无效参数');
    //校验班号是否存在
    require(classList[classId].exist == true, '无效参数');
    Class memory class = classList[classId];
    //返回班号 班级名称 班费地址
    return (classId, class.name, class.classAddress);
}

function showClass(uint classId) public view{
    this.getClass(classId); //外部调用的方式
}
public

默认情况下,函数的可见性类型是 public。被声明为 public 的函数和被声明为 public 的状态变量类似,其访问权限最大,可被合约内部、子合约以及外部直接访问。不仅能以内部调用的方式访问,也能以外部调用的方式访问:

//读取班级信息
//函数已被声明为 public
function getClass(uint classId) public view returns (uint, string, address) {
    //校验classId是否大于0
    require(classId > 0, '无效参数');
    //校验班号是否存在
    require(classList[classId].exist == true, '无效参数');
    Class memory class = classList[classId];
    //返回班号 班级名称 班费地址
    return (classId, class.name, class.classAddress);
}

function showClass(uint classId) public view{
    this.getClass(classId); //外部调用的方式
    getClass(classId);  //内部调用的方式
}
internal

被声明为 internal 的函数,可被当前合约及子合约访问,但只能以内部调用的方式访问:

//父合约School
contract School {
    //状态变量age声明为private
    uint private age=16;

    function getAge() internal view returns (uint) {
        return age;  //16
    }

    function getAge2() public view returns (uint) {
        //外部调用方式
        this.getAge(); //报错 TypeError: Member "getAge" not found or not visible after argument-dependent lookup in contract School
    }
}
//Class子合约继承自School父合约
contract Class is School{
    function getParentAge() public view returns (uint) {
       return getAge();
    }

    function getParentAge2() public view returns (uint) {
        //外部调用方式
        this.getAge(); //报错 TypeError: Member "getAge" not found or not visible after argument-dependent lookup in contract School
    }
}
private

private 的访问权限最小,只能在当前合约内以内部调用的方式访问:

//父合约School
contract School {
    //状态变量age声明为private
    uint private age=16;

    function getAge() private view returns (uint) {
        return age;  //16
    }

    function getAge2() public view returns (uint) {
        //外部调用方式
        this.getAge(); //报错 TypeError: Member "getAge" not found or not visible after argument-dependent lookup in contract School
    }
}
//Class子合约继承自School父合约
contract Class is School{
    function getParentAge() public view returns (uint) {
       return getAge(); //报错 DeclarationError: Undeclared identifier.
    }
}

需要注意的是,即使是被声明为 private 的状态变量和函数,仍能被所有人查看到里面的数据。访问权限只是阻止了其它合约访问函数或修改数据

用一句话总结合约函数可见性类型,即是:修饰符 private 意味着它只能被合约内部调用;internal 就像 private,但是也能被继承的合约调用;external 只能从合约外部调用;最后 public 可以在任何地方调用,不管是内部还是外部。

合约继承

细心的读者可能已经发现在介绍状态变量可见性类型的示例代码中,有实现合约的继承功能。Solidity 的合约继承和绝大多数面向对象编程语言类似,支持多重继承,此处不再赘述。

这里简单介绍下合约继承里的方法重写。被声明为 external、public、internal 的函数都可以实现方法重写,这里以 external 为例:

//父合约School
contract School {
    //状态变量age声明为private
    uint private age=16;

    function getAge() external view returns (uint) {
        return age;  //返回16
    }
}
//Class子合约继承自School父合约
contract Class is School{
    uint internal age = 20;

    function getAge() external view returns (uint) {
        return age;  //返回20
    }

    function showAge() public view returns (uint) {
        return this.getAge();  //返回20
    }
}

函数修饰器

函数修饰器 modifier 和函数类似,不过它是用来修饰其它已有的函数,在函数内其他语句执行之前,率先为其检查相应条件。函数修饰器也是合约的可继承属性,可被子合约调用:

pragma solidity ^0.4.17;
//父合约School
contract School {
    //状态变量schoolAge
    uint schoolAge=16;
    //状态变量owner。存储合约拥有者地址,即合约部署者的地址
    address owner;
    //构造函数
    constructor() public {
        //将合约部署者地址赋值给owner变量
        owner = msg.sender;
    }
    //函数修饰器onlyOwner
    modifier onlyOwner {
        //检查下调用者,确保只有合约拥有者才能运行
        require(msg.sender == owner, '必须是合约拥有者');
        //修饰器的最后一行为 _;
        //表示修饰器所修饰的函数体 会被插入到此位置
        _;
    }
    //onlyOwner函数修饰器应用于setSchoolAge函数
    //只有合约拥有者才能调用此函数
    function setSchoolAge(uint age) public onlyOwner {
        require(age > 0, '校龄必须大于0');
        schoolAge = age;
    }

    function getSchoolAge() public view returns (uint) {
        return schoolAge;  
    }
}
//Class子合约继承自School父合约
contract Class is School{
    //状态变量classAge
    uint classAge;
    //父合约中的onlyOwner函数修饰器用于setClassAge函数
    //只有合约拥有者才能调用此函数
    function setClassAge(uint age) public onlyOwner {
        require(age > 0, '班龄必须大于0');
        require(classAge > schoolAge, '班龄必须小于等于校龄');
        classAge = age;
    }

    function getClassAge() public view returns (uint) {
        return classAge;  
    }
}

函数修饰器看起来跟函数没什么不同,不过关键字 modifier 告诉编译器,这是个 Modifier(修饰器),而不是个 Function(函数)。它不能像函数那样被直接调用,只能被添加到函数定义的末尾,用以改变函数的行为。

注意上面代码中的 setSchoolAge()setClassAge() 函数上的修饰器 onlyOwner。当我们调用 setSchoolAge() 函数时(setClassAge() 同理),首先执行 onlyOwner 中的代码, 执行到 onlyOwner 中的 _; 语句时,程序再返回并执行 setSchoolAge() 中的代码。可见,尽管函数修饰器也可以应用到各种场合,但最常见的还是放在函数执行之前添加快速的 require 检查。因为给函数添加了修饰器 onlyOwner,使得唯有合约的拥有者(也就是部署者)才能调用它。

有读者可能已经注意到上面合约代码中的 msg.sender ,此处顺带提一下。msg 是 Solidity 提供的全局变量,是调用合约时发送的消息对象。msg 对象的主要属性如下:

属性 类型 含义
msg.data bytes 获取调用者传入的数据信息。
msg.sender address 当前调用者的账户地址。
msg.value uint 当前调用发送的 ETH 数量,单位为 wei。

特殊关键字

Solidity 提供了几个特殊关键字,下面来介绍一下。

view

当执行函数不会修改区块链中的数据状态时,那么这个函数就可以声明为 view:

function getSchoolAge() public view returns (uint) {
    return schoolAge;  
}

一般情况下,调用 view 声明的函数是不需要花费 Gas 的。

下列情况被认为是会修改数据状态的:

  • 修改状态变量;
  • 产生事件;
  • 创建其他合约;
  • 通过调用发送 ETH;
  • 调用任何没有声明为 view 或 pure 的函数。

constant

constant 是 view 的别名。但 constant 能用于状态变量。被声明为 constant 的状态变量可以理解为常量,无法修改其值:

uint constant schoolAge=16; //被声明为常量

function setSchoolAge(uint age) public {
    schoolAge = age;  //报错 TypeError: Cannot assign to a constant variable
}

pure

对于没有修改区块链中的数据状态,以及没有读取区块链中数据的函数,可以声明为 pure:

function add(uint a, uint b) public pure returns (uint) {
    return a+b;
}

除了 view 关键字中列出的会修改数据状态的情况外,以下情况会被认为是从区块链中读取数据:

  • 读取状态变量;
  • 访问 this.balance<address>.balance
  • 访问 block、tx 或 msg 中任意成员(msg.data除外);
  • 调用任何没有声明为 pure 的函数。

payable

声明为 payable 的函数,支持支付功能,可以接收 ETH:

uint  schoolAge=16;
address owner = 0x616874DC6cc2810CdC930DEA26496FcF217D58cA;
//假设设置校龄需要支付 0.001 ETH
function setSchoolAge(uint age) public payable {
    require(age > 0, '校龄必须大于0');
    require(msg.sender == owner, '必须是合约拥有者');
    require(msg.value == 0.001 ether, '必须支付0.001ETH');
    schoolAge = age;
}

function getSchoolAge() public view returns (uint) {
    return schoolAge; 
}

以太坊代币单位

以太坊的代币单位可参考这里:以太坊代币单位转换,这里不再详细介绍。

Solidity 语法知识就介绍到这,还有一些更高级的功能与用法,大家在玩转 Solidity 基础知识后便融会贯通,举一反三。

上一篇
下一篇
内容互动
写评论
加载更多
评论文章