[智能合约] 初识智能合约-语法篇

1,598 阅读9分钟

1. 概念

智能合约(Smart Contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约实质上是一套以数字形式定义的承诺(Promises),包括合约参与方可以在上面执行这些承诺的协议。

本质上来说,智能合约是一段程序,它以计算机指令的方式实现了传统合约的自动化处理。智能合约程序不只是一个可以自动执行的计算机程序,它本身就是一个系统参与者,对接收到的信息进行回应,可以接收和储存价值,也可以向外发送信息和价值。这个程序就像一个可以被信任的人,可以临时保管资产,总是按照事先的规则执行操作。简单讲,智能合约就是双方在区块链资产上交易时,触发执行的一段代码,这段代码就是智能合约。提前规定好合约的内容,当在满足触发合约条件的时候,程序就会自动执行合约内容。

2. 版本指令和contract关键字

pragma solidity ^0.8.0;

contract HelloWorld {}

这一行的意思是源代码使用solidity版本0.8.0写的,并且使用0.8.0以上的版本也没问题(最高到0.9.0,但是不包含0.9.0),和前端同学常见的的npm语义化版本类似。

通过 contract 关键字声明合约代码。

3. 状态变量和整数

状态变量是定义在合约内部的,但是不在函数内部的变量,它会永久地保存在合约存储空间中,也就是说它们会被写入区块链中,这就好比数据写入一个数据库中。

uint类型:无符号整数

uint是Solidity支持的基本数据类型之一,意思是无符号整型(unsigned integer),指其值不能是负数,对于有符号的整数存在名为 int 的数据类型。

string: 字符串,用来保存任意长度的UTF-8的编码数据。

4. 数学运算

加减乘除取余等操作,和JavaScript一样,比如:平方: a ** 2, n次方: a ** n

5. 结构体

有时你需要更复杂的数据类型,Solidity支持通过结构体来定义新的类型,使用 struct 关键字来定义。

// 定义结构体
struct Book {
    string name;
    uint page;
}

// 结构体初始化
Book myBook = Book('blockchain', 200);

// 变量赋值
myBook.page = 300;

6. 函数

通过function关键字声明函数,函数是合约代码的可执行单元,

定义:function buyBook(string _name, uint _count){ }

调用:buyBook('fishBook', 100)

函数返回值:要想函数返回一个数值,可以按如下示例。Solidity里,函数的定义里可包含返回值的数据类型(如本例中 string)。

string message = "hello world";

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

函数类型的构造方式:function (<parameter types>) {public | private | internal | external} [modifier] [pure|constant|view|payable] [returns (<return types>)]

6.1 函数权限关键字

  • public:只有 public 类型的函数才可以供外部访问,当一个状态变量的权限为 public 类型时,它就会自动生成一个可供外部调用的 get 函数。当函数声明时,它默认为是 public 类型,而状态变量声明时,默认为 internal 类型。
  • private:只能在当前类中进行访问,子类无法继承,也无法调用或访问。
  • internal:子类继承父类,子类可以访问父类的 internal 函数,同时,使用 using for 关键字后,本类可以使用被调用类的 internal 函数。
  • external:被声明的函数只能在合约外部调用。

6.2 函数修饰关键字

  • modifier:被 modifier 关键字声明的关键字所修饰的函数只能在满足 modifier 关键字声明的关键字的要求后才会被执行,比如声明某函数只有管理员有权限,则可以参考以下实现:

      modifier onlyAdmin() {      
          require(msg.sender == admin, "Permission denied");
          _;  
      }  
      function set(uint a) public onlyAdmin returns(uint) {
          // .....  
      }
    
  • constant:被声明为 constant 的状态变量只能使用那些在编译时有确定值的表达式来给它们赋值。任何通过访问 内存、区块链数据(例如 now,this.balance 或 block.number)或执行数据(msg.gas)或对外部合约的调用来给它们赋值都是不允许的。不是所有类型的状态变量都支持用 constant 来修饰,当前支持的仅有值类型和字符串。

  • view:被该关键字修饰的状态变量只能读取其值,不能对该状态变量的值进行修改。

  • pure:被该关键字修饰的状态变量既不能读取变量,也不能修改该变量。

  • Storage 变量是指永久存储在区块链中的变量。

  • Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。

7. 数组

Solidity支持两种数组: 静态数组和动态数组

// 固定长度为3的静态数组:
uint[3] fixedArray;

// 固定长度为6的string类型的静态数组:
string[6] stringArray;

// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;

// 也可以建立一个结构体类型的数组,例如上面提到的Book:
// 这是动态数组,我们可以不断添加元素
Book[] books;

// 数组方法
books.push(BOOK)

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

公共数组:你可以定义公共数组,使用 public 关键字。Solidity 会为公共数组自动创建 getter 方法。

定义语法:Book[] public books;

特点:其它的合约可以从这个数组读取数据(但不能写入数据),所以这是一种在合约中保存公共数据的方法。

一行完成数组push:books.push(Book('fishbook', 100))

8. 散列函数 hash

散列函数keccak256是把一个字符串转换为一个256位的16进制数字

uint(keccak256(_name)) 通过 uint() 把 16进制数 转为10进制数。

9. 事件

事件是合约和区块链通讯的一种机制。

你的应用可以“监听”某些事件,并做出反应。我们使用event关键字来定义事件,使用emit关键字在函数中触发事件。

10. Mapping 映射

映射本质上是存储和查找数据所用的键-值对。我们可以直接通过键来查询该键对应的值。

类似与JS中的对象,key-value数据结构,key是唯一的,value可以重复。

举例:mapping (uint => string) userIdToName; 表示从userId到Name的一种映射关系,用来存储或者查找用户名用的。其中:键是一个uint类型,值是一个 string类型。

给map赋值:userIdToName[1] = 'Tom',

取值:userIdToName[1]

11. address 地址

solidity语言中用来表示合约地址或账户地址;contract/account address,address表示的地址是20字节。

12. msg.sender 全局函数

solidity会提供多个全局接口函数可以直接使用,其中一个就是 msg.sender,它指的是交易的发送方,会返回一个identity类型。

注:类似的接口函数还有例如block.number获取当前块高。

13. require

require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行。

用法:require(条件, 报错提示string), 条件为true则通过,false则报错,第二个参数是报错内容。

14. 存储

在Solidity中,有两个地方可以存储变量 —— storage 或 memory。

  • storage变量是指永久存储在区块链中的变量。

  • memory变量则是临时的,当外部函数对某合约调用完成时,memory变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。

  • 大多数时候你都用不到这些关键字,默认情况下Solidity会自动处理它们。 状态变量(在函数之外声明的变量)默认为storage形式,并永久写入区块链;而在函数内部声明的变量是memory型的,它们会在函数调用结束后消失。

  • 然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的结构体数组时:

    contract BookStore {
    
        struct Book {
            string name;
            string status;
        }
    
        Book[] books;
    
        function buyBook(uint _index) public {
    
        // Book myBook = books[_index];
        // 上面的例子看上去很直接,不过Solidity将会给出警告
        // 告诉你在这里应该明确定义 `storage` 或者 `memory`。
        // 所以你应该明确定义 `storage`:
        Book storage myBook = books[_index];
        myBook.status = "solded";
        // 这将永久把 `books[_index]` 变为区块链上的存储
        // 如果你只想要一个副本,可以使用`memory`:
    
        Book memory anotherBook = books[_index + 1];
        // 这样 `anotherBook` 就仅仅是一个内存里的副本了
    
        anotherBook.status = "solded!";
        // 这样仅仅修改临时变量,对 `books[_index + 1]` 没有任何影响
        // 不过你可以这样做:
    
        books[_index + 1] = anotherBook;
        // 如果你想把副本的改动保存回区块链存储
        }
    }
    

15. modifier函数修饰符

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

// 存储蚂蚁级别的映射
mapping (uint => uint) public level;

// 限定蚂蚁等级的修饰符
modifier levelThan(uint _level, uint _antId) {
    require(level[_antId] >= _level);
    _;
}

// 必须年满5级才允许发奖励
// 我们可以用如下参数调用`levelThan` 修饰符:
function prize(uint _antId) levelThan(5, _antId) {
    // 其余的程序逻辑
}

注意:prize 函数上的 levelThan 修饰符。 当你调用 prize 时,首先执行 levelThan 中的代码, 执行到 levelThan 中的 _ ; 语句时,程序再返回并执行 prize 中的代码。

16. 公有/私有函数

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

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

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

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

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

注意:当一个函数同时被 public/privatemodifier 修饰时,先写 public/private