智能合约编写之Solidity的基础特性

区块链
智能合约编写阶段将从 Solidity 基础特性、高级特性、设计模式以及编程攻略分别展开,带读者认识 Solidity 并掌握其运用,更好地进行智能合约开发。本篇将围绕 Solidity 的基础特性,带大家上手开发一个最基本的智能合约。

[[324359]]

前篇介绍,目前大部分的联盟链平台,包括 FISCO BCOS,都采用 Solidity 作为智能合约开发语言,因此熟悉并上手 Solidity 十分必要。

作为一门面向区块链平台设计的图灵完备的编程语言,Solidity 支持函数调用、修饰符、重载、事件、继承等多种特性,在区块链社区中,拥有广泛的影响力和踊跃的社区支持。但对于刚接触区块链的人而言,Solidity 是一门陌生的语言。

智能合约编写阶段将从 Solidity 基础特性、高级特性、设计模式以及编程攻略分别展开,带读者认识 Solidity 并掌握其运用,更好地进行智能合约开发。 

本篇将围绕 Solidity 的基础特性,带大家上手开发一个最基本的智能合约。

智能合约代码结构

任何编程语言都有其规范的代码结构,用于表达在一个代码文件中如何组织和编写代码,Solidity 也一样。

本节,我们将通过一个简单的合约示例,来了解智能合约的代码结构。

  1. pragma solidity ^0.4.25;
  2. contract Sample{
  3.  
  4. //State variables
  5. address private _admin;
  6. uint private _state;
  7.  
  8. //Modifier
  9. modifier onlyAdmin(){
  10. require(msg.sender == _admin, "You are not admin");
  11. _;
  12. }
  13.  
  14. //Events
  15. event SetState(uint value);
  16.  
  17. //Constructor
  18. constructor() public{
  19. _admin = msg.sender;
  20. }
  21.  
  22. //Functions
  23. function setState(uint value) public onlyAdmin{
  24. _state = value;
  25. emit SetState(value);
  26. }
  27.  
  28. function getValue() public view returns (uint){
  29. return _state;
  30. }
  31.  
  32. }

上面这段程序包括了以下功能:

  • 通过构造函数来部署合约

  • 通过 setValue 函数设置合约状态

  • 通过 getValue 函数查询合约状态

整个合约主要分为以下几个构成部分:

  • 状态变量 - _admin_state,这些变量会被永久保存,也可以被函数修改

  • 构造函数 - 用于部署并初始化合约

  • 事件 - SetState, 功能类似日志,记录了一个事件的发生

  • 修饰符 - onlyAdmin, 用于给函数加一层“外衣”

  • 函数 - setStategetState,用于读写状态变量

下面将逐一介绍上述构成部分。

状态变量

状态变量是合约的骨髓,它记录了合约的业务信息。用户可以通过函数来修改这些状态变量,这些修改也会被包含到交易中;交易经过区块链网络确认后,修改即为生效。

  1. uint private _state;

状态变量的声明方式为:[类型]  [访问修饰符-可选] [字段名]

构造函数

构造函数用于初始化合约,它允许用户传入一些基本的数据,写入到状态变量中。 

在上述例子中,设置了 _admin 字段,作为后面演示其他功能的前提。

  1. constructor() public{
  2. _admin = msg.sender;
  3. }

 和 Java 不同的是,构造函数不支持重载,只能指定一个构造函数。

函数

函数被用来读写状态变量。对变量的修改将会被包含在交易中,经区块链网络确认后才生效。生效后,修改会被永久的保存在区块链账本中。

函数签名定义了函数名、输入输出参数、访问修饰符、自定义修饰符。

  1. function setState(uint value) public onlyAdmin;

函数还可以返回多个返回值:

  1. function functionSample() public view returns(uint, uint){
  2. return (1,2);
  3. } 

在本合约中,还有一个配备了 view 修饰符的函数。这个 view 表示了该函数不会修改任何状态变量。 

view 类似的还有修饰符 pure,其表明该函数是纯函数,连状态变量都不用读,函数的运行仅仅依赖于参数。

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

如果在 view 函数中尝试修改状态变量,或者在 pure 函数中访问状态变量,编译器均会报错。

事件 

事件类似于日志,会被记录到区块链中,客户端可以通过 web3 订阅这些事件。

定义事件:

event SetState(uint value);

构造事件:

emit SetState(value);

这里有几点需要注意:

  • 事件的名称可以任意指定,不一定要和函数名挂钩,但推荐两者挂钩,以便清晰地表达发生的事情。

  • 构造事件时,也可不写 emit,但因为事件和函数无论是名称还是参数都高度相关,这样操作很容易笔误将事件写成函数调用,因此不推荐不写。 

    1. function setState(uint value) public onlyAdmin{
    2. _state = value;
    3. emit SetState(value);
    4. // 下面这样写也可以,但不推荐,因为很容易笔误写成 setState
    5. // SetState(value);
    6. }
  • Solidity 编程风格应采用一定的规范。关于编程风格,建议参考:https://learnblockchain.cn/docs/solidity/style-guide.html#id16 

修饰符

修饰符是合约中非常重要的一环。它挂在函数声明上,为函数提供一些额外的功能,例如检查、清理等工作。

在本例中,修饰符 onlyAdmin 要求函数调用前,需要先检测函数的调用者是否为函数部署时设定的那个管理员(即合约的部署人)。

  1. //Modifer
  2. modifier onlyAdmin(){
  3. require(msg.sender == _admin, "You are not admin");
  4. _;
  5. }
  6.  
  7. ...
  8. //Functions
  9. function setState(uint value) public onlyAdmin{
  10. ...
  11. }

值得注意的是,定义在修饰符中的下划线 “_”,表示函数的调用,指代的是开发者用修饰符修饰的函数。在本例中,表达的是 setState 函数调用的意思。

智能合约的运行 

了解了上述的智能合约示例的结构,就可以直接上手运行,运行合约的方式有多种,大家可以任意采取其中一种:

  • 方法二:使用 FISCO BCOS 开源项目 WeBASE 提供的在线 ide WEBASE-front 运行 

  • 方法三:通过在线 ide remix 来进行合约的部署与运行,remix 的地址为:http://remix.ethereum.org/

本例中使用 remix 作为运行示例。

编译

首先,在 remix 的在线 ide 中键入代码后,通过编译按钮来编译。成功后会在按钮上出现一个绿色对勾: 

 

部署

编译成功后就可进行部署环节,部署成功后会出现合约实例。 

 

setState

合约部署后,我们来调用 setState(4)。在执行成功后,会产生一条交易收据,里面包含了交易的执行信息。

在这里,用户可以看到交易执行状态(status)、交易执行人(from)、交易输入输出(decoded input、decoded output)、交易开销(execution cost)以及交易日志(logs)。

在交易日志中,我们看到 SetState 事件被抛出,里面的参数也记录了事件传入的值 4

如果我们换一个账户来执行,那么调用会失败,因为 onlyAdmin 修饰符会阻止用户调用。

 

getState 

调用 getState 后,可以直接看到所得到的值为 4,正好是我们先前 setState 所传入的值:

 

Solidity 数据类型

在前文的示例中,我们用到了 uint 等数据类型。由于 Solidity 类型设计比较特殊,这里也会简单介绍一下 Solidity 的数据类型。

整型系列

Solidity 提供了一组数据类型来表示整数, 包含无符号整数与有符号整数。每类整数还可根据长度细分,具体细分类型如下。

类型

长度(位)

有符号

uint

256

uint8

8

uint16

16

...

...

uint256

256

int

256

int8

8

int16

16

...

...

int256

256

定长字节系列

Solidity 提供了 bytes1bytes32 的类型,它们是固定长度的字节数组。

用户可以读取定长字节的内容。 

  1. function bytesSample() public{
  2. bytes32 barray;
  3. //Initialize baarray
  4. //read brray[0]
  5. byte b = barray[0];
  6. }

并且,可以将整数类型转换为字节。 

  1. uint256 s = 1;
  2. bytes32 b = bytes32(s);

这里有一个关键细节,Solidity 采取大端序编码,高地址存的是整数的小端。例如,b[0] 是低地址端,它存整数的高端,所以值为 0;取 b[31] 才是 1。 

  1. function bytesSample() public pure returns(byte, byte){
  2. uint256 value = 1;
  3. bytes32 b = bytes32(value);
  4. //Should be (0, 1)
  5. return (b[0], b[31]);
  6. }

变长字节

从上文中,读者可了解定长字节数组。此外,Solidity 还提供了一个变长字节数组:bytes。使用方式类似数组,后文会有介绍。 

字符串 

Solidity 提供的字符串,本质是一串经 UTF-8 编码的字节数组,它兼容于变长字节类型。

目前 Solidity 对字符串的支持不佳,也没有字符的概念。用户可以将字符串转成字节。

  1. function stringSample() public view returns(bytes){
  2. string memory str = "abc";
  3. bytes memory b = bytes(str);
  4. //0x616263
  5. return b;
  6. }

要注意的是,当将 string 转换成 bytes 时,数据内容本身不会被拷贝,如上文中,strb 变量指向的都是同一个字符串 "abc"

地址类型  

address 表示账户地址,它由私钥间接生成,是一个 20 字节的数据。同样,它也可以被转换为 bytes20

  1. function addressSample() public view returns(bytes20){
  2. address me = msg.sender;
  3. bytes20 b = bytes20(me);
  4. return b;
  5. }

映射 

mapping 表示映射,是极其重要的数据结构。它与 Java 中的映射存在如下几点差别:

  • 它无法迭代键名,因为它只保存键的哈希,而不保存键值,如果想迭代,可以用开源的可迭代哈希类库

  • 如果一个键名未被保存在映射中,一样可以正常读取到对应的键值,只是值是空值(字节全为 0)。所以它也不需要 putget 等操作,用户直接去操作它即可。

  1. contract Sample{
  2. mapping(uint=>string) private values;
  3. function mappingSample() public view returns(bytes20){
  4. //put a key value pair
  5. values[10] = "hello";
  6. //read value
  7. string value = values[10];
  8. }
  9. }

数组 

如果数组是状态变量,那么支持 push 等操作:

  1. contract Sample{
  2. string[] private arr;
  3. function arraySample() public view {
  4. arr.push("Hello");
  5. uint len = arr.length;//should be 1
  6. string value = arr[0];//should be Hello
  7. }
  8. }

数组也可以以局部变量的方式使用,但稍有不同:

  1. function arraySample() public view returns(uint){
  2. //create an empty array of length 2
  3. uint[] memory p = new uint[](2);
  4. p[3] = 1;//THIS WILL THROW EXCEPTION
  5. return p.length;
  6. }

结构

Solidity 允许开发者自定义结构对象。结构体既可以作为状态变量存储,也可以在函数中作为局部变量存在。  

  1. struct Person{
  2. uint age;
  3. string name;
  4. }
  5.  
  6. Person private _person;
  7.  
  8. function structExample() {
  9. Person memory p = Person(1, "alice");
  10. _person = p;
  11. }

本节中只介绍了比较常见的数据类型,更完整的列表可参考 Solidity 官方网站:https://solidity.readthedocs.io/en/v0.6.3/types.html  

全局变量 

示例合约代码的构造函数中,包含 msg.sender。它属于全局变量。在智能合约中,全局变量或全局方法可用于获取和当前区块、交易相关的一些基本信息,如块高、块时间、合约调用者等。

比较常用的全局变量是 msg 变量,表示调用上下文,常见的全局变量有以下几种: 

  • msg.sender:合约的直接调用者。由于是直接调用者,所以当处于“用户 A->合约 1->合约 2”调用链下,若在合约 2内使用 msg.sender,得到的会是合约 1 的地址。如果想获取用户 A,可以用 tx.origin

  • tx.origin:交易的"始作俑者",整个调用链的起点。

  • msg.calldata:包含完整的调用信息,包括函数标识、参数等。calldata 的前 4 字节就是函数标识,与 msg.sig相同。 

  • msg.sigmsg.calldata 的前 4 字节,用于标识函数。 

  • block.number:表示当前所在的区块高度。

  • now:表示当前的时间戳。也可以用 block.timestamp 表示。 

这里只列出了部分常见全局变量,完整版本请参考:https://solidity.readthedocs.io/en/v0.4.24/units-and-global-variables.html

结语 

本文以一个简单的示例合约作为引入,介绍了运用 Solidity 开发智能合约的基本知识。读者可以尝试运行该合约,感受智能合约的开发。 

责任编辑:庞桂玉 来源: Linux中国
相关推荐

2021-03-18 23:04:41

Solidity开发智能

2023-05-12 09:14:34

2023-10-12 09:05:11

2022-10-17 09:15:37

2023-02-24 08:00:00

2009-10-26 12:17:03

linux脚本编写

2021-12-28 14:07:03

人工智能相似问机器人

2020-12-17 08:00:00

区块链数据以太坊

2021-04-23 10:59:07

区块链货币私钥

2021-05-06 09:42:24

Truffle部署智能

2021-11-02 10:25:19

区块链智能合约

2020-04-22 13:23:46

智能合约区块链比特币

2022-05-29 10:19:08

区块链智能合约编程语言

2022-02-13 23:05:23

加密货币比特币货币

2021-03-25 16:34:10

区块链加密资产技术

2023-05-05 08:00:00

2021-04-20 12:53:34

Polkadot部署合约

2019-01-18 05:22:39

区块链智能合约网络安全

2017-03-15 13:42:12

互联网

2022-10-26 08:00:00

点赞
收藏

51CTO技术栈公众号