本文参考Microsoft了解如何使用Solidity

简介

使用一种称为Solidity的编程语言可以编写供应链、在线市场或其他用例的操作代码。
通过使用Solidity,还可以指定用户操作。通过对网络上允许的这些操作进行编程,你可以创建自己的区块链网络,而这些网络对所有参与者都是安全且透明的。

什么是Solidity

Solidity是一种面向对象的用于编写智能合同的语言。
智能合同是存储在区块链中的程序,它们指定有关数字资产传输的规则和行为。可以使用Solidity为Ethereum区块链平台对智能合同进行编程。
智能合同包含状态和可编程逻辑,通过事务执行函数,因此使用智能合同可以创建业务工作流。

概述

Solidity是Ethereum区块链最常用的编程语言。
Solidity是一种基于其他编程语言(包括 C++、Python和JavaScript)的高级语言,如果你熟悉这些语言中的任何一种,则有助于熟悉Solidity代码。
Solidity是静态类型语言,这意味着类型检查在编译时进行,而不像动态类型语言在运行时进行。
Solidity支持继承,这意味着一个合同中存在的函数、变量和其他属性可以在另一个合同中使用。该语言还支持复杂的用户定义类型(如struct和enum),这使你可以将相关类型的数据组合在一起。
Solidity是一种开源编程语言,协作者社区庞大。若要了解有关 Solidity 项目以及如何参与的详细信息,请参阅GitHub存储库

什么是 Ethereum?

Ethereum是最受欢迎的区块链平台之一,仅次于比特币。这是一种社区构建的技术,有自己的加密货币Ether(ETH),可以进行购买和销售。
Ethereum的独特之处在于它是“全球可编程区块链”。Solidity是用于在Ethereum平台上开发的主要编程语言,由Ethereum开发人员构建和维护。

Ethereum 虚拟机

Solidity合同在Ethereum虚拟机(简称EVM)的虚拟环境上运行。它是一个完全隔离的沙盒环境。
除了执行的合同外,它不会访问网络上的任何其他内容。

Solidity语言基础知识

所有Solidity合同通常都包括:

  • Pragma指令
  • 状态变量
  • 函数
  • 事件

Pragma指令

Pragma是用来指示编译器检查其Solidity版本是否与所需版本匹配的关键字。
如果匹配,则表示源文件可以成功运行。如果不匹配,编译器将发出错误,因此需要始终确保在合同定义中包含Solidity最新版本。
版本pragma指令如下所示:

pragma solidity ^0.7.0;

此行意味着源文件将使用高于0.7.0版本(最高版本为0.7.9)的编译器进行编译。从版本0.8.0开始,可能会引入一些中断性变更,导致源文件无法成功编译。
若要查找Solidity的当前版本,可访问Solidity网站,使用源文件中的最新版本。

状态变量

状态变量是任何Solidity源文件的关键,状态变量永久存储在合同存储中。

pragma solidity >0.7.0 <0.8.0;

contract Marketplace {
    uint price; // State variable

备注:合同源文件始终以“contract ContractName”开头。

在本例中,状态变量名为price,类型为“uint”。整数类型uint表示此变量是256位的无符号整数,这意味着它可以存储00225612^{256}-1范围内的正数。
对于所有变量定义,必须指定类型和变量名。
此外,可以将状态变量的可见性指定为:

  • public:合同接口的一部分,可以从其他合同访问。
  • internal:仅限从当前合同内部访问。
  • private:仅对定义它的合同可见。

internal和private类似,不过如果某个合同继承自其父合同,这个合同即可以访问父合同中定义的“内部”函数。
external与public类似,只不过这些函数只能在合同之外调用 -它们不能被合同内的其他函数调用。

函数

在合同中,可执行代码单元称为函数。
函数描述实现一个任务的单个操作,它们是可重用的,也可以从其他源文件(如库)进行调用。 Solidity中函数的行为类似于其他编程语言中的函数。

pragma solidity >0.7.0 <0.8.0;

contract Marketplace {
    function buy() public {
        // ...
    }
}

这段代码显示了一个名为buy的函数,它具有公共可见性,这意味着它可以由其他合同访问。
函数可以使用以下可见性说明符之一:public、private、internal和external。
函数可以在内部合同进行调用,也可以从外部另一个合同进行调用。
下面是一个函数示例,该函数接受一个参数(一个称为price的整数),并返回一个整数:

pragma solidity >0.7.0 <0.8.0;

contract Marketplace {
    function buy(uint price) public returns (uint) {
        // ...
    }
}

函数修饰符

函数修饰符可用于更改函数的行为。它们的工作原理是在函数执行前检查条件。
例如,函数可以检查只有指定为卖方的用户才能列出要出售的商品。
pragma solidity >0.7.0 <0.8.0;

contract Marketplace {
    address public seller;

    modifier onlySeller() {
        require(
            msg.sender == seller,
            "Only seller can put an item up for sale."
        );
        _;
    }

    function listItem() public view onlySeller {
        // ...
    }
}

此示例介绍以下各项:

  • 类型为address的变量,用于存储卖方用户的20字节Ethereum地址。(地址变量)
  • 名为onlySeller的修饰符,用于说明只有卖方才能列出商品。
  • 特殊符号_;,表示函数体插入的位置。
  • 使用修饰符onlySeller可以定义函数。

可在函数定义中使用的其他函数修饰符包括:

  • pure,用于描述不允许修改或不可访问状态的函数。(不能读写)
  • view,用于描述不允许修改状态的函数。(只读不写)
  • payable,用于描述可以接收Ether的函数。(调用此函数可以附加发送一些ETH)

事件

事件描述了合同中采取的操作。与函数类似,事件具有在调用事件时需要指定的参数。
若要调用事件,必须将关键字“emit”与事件名称及其参数一起使用。

pragma solidity >0.7.0 <0.8.0;

contract Marketplace {
    event PurchasedItem(address buyer, uint price);

    function buy() public {
        // ...
        emit PurchasedItem(msg.sender, msg.value);
    }
}

调用事件时,事件会被捕获为事务日志中的事务,事务日志是区块链中的一种特殊数据结构。
这些日志与合同的地址相关联,已合并到区块链中,并且始终保持不变。从合同中是无法访问日志及其事件数据,并且无法修改它。

Solidity值类型

值类型通过值传递,并在使用时进行复制。编写合同时将使用的主要值类型包括“整数”、“布尔”、“字符串”、“地址”和“枚举”。

整型

每个Solidity源文件中都使用整数。它们可以表示有符号整数也可以表示无符号整数。存储的整数大小介于8位到256位之间。

  • signed:包括负数和正数。可以表示为int。
  • unigned:仅包含正数。可以表示为uint。
  • 如果未指定位数,则默认值为256位。

以下运算符可应用于整数:

  • 比较:<=、<、==、!=、>=、>
  • 位运算符:&、| 、^ 、~
  • 算术运算符:+ 、- 、* 、/ 、% 、)、**

以下是整数定义的一些示例:

int32 price = 25; // signed 32 bit integer
uint256 balance = 1000; // unsigned 256 bit integer
balance - price; // 975
2 * price; // 50
price % 2; // 1

布尔型

布尔使用关键字bool进行定义。它们的值始终是true或false。
下面提供了定义方法:

bool forSale; //true if an item is for sale
bool purchased; //true if an item has been purchased

布尔值通常用于比较语句中。例如:

if(balance > 0 & balance > price) {
    return true;
}

if(price > balance) {
    return false;
}

布尔值也可以用在函数参数和返回类型中。

function buy(int price) returns (bool success) {
    // ...
}

字符串

大多数合同文件中也使用字符串,它们是用双引号或单引号括起来的字符或字词。

String shipped = "shipped"; // shipped
String delivered = 'delivered'; // delivered
String newItem = "newItem"; // newItem

此外,以下转义字符可以与字符串一起使用:

  • <newline> 转义为换行
  • \n 换行
  • \r 回车
  • \t Tab

地址

地址是一种具有20字节值的类型,它表示Ethereum用户帐户。此类型可以是常规“address”,也可以是“address payable”。
两者之间的区别在于,“address payable”类型是Ether发送到的地址,它包含额外的成员transfer和send。

address payable public seller; // account for the seller
address payable public buyer; // account for the user

function transfer(address buyer, uint price) {
    buyer.transfer(price); // the transfer member transfers the price of the item
}

枚举

在Solidity中,可使用枚举创建用户定义类型。之所以称之为用户定义,是因为创建合同的人员才能决定要包含哪些值。
枚举可用于显示许多可选择的选项,其中有一项是必需的。
例如,可以使用enum来表示项目的不同状态,将枚举视为代表多项选择答案,其中所有值都是预定义的,你必须选择一个。
可以在合同或库定义中声明枚举。

enum Status { 
    Pending,
    Shipped,
    Delivered 
}

Status public status;

constructor() public {
    status = Status.Pending;
}

Solidity引用类型

在编写合同时,还应了解引用类型。
与总是传递值的独立副本的值类型不同,引用类型为值提供了一个已知位置。三种引用类型为:结构、数组和映射。

数据位置

使用引用类型时,必须显式提供该类型的数据存储位置。以下选项可用于指定存储类型的数据位置:

  • memory:(内存,即栈中数据,生存期与调用函数相同)
    • 存储函数参数的位置。
    • 生存期限制为外部函数调用的生存期。
  • storage:(存储,即写入本地区块链副本中,开销最高)
    • 存储状态变量的位置。
    • 生命期仅限于合同有效期。
  • calldata:(函数参数数据,行为类似内存数据,因其不可修改因此赋值时完全不需要数据拷贝,开销最低)
    • 存储函数参数的位置。
    • 本位置对于外部函数的参数是必需的,但也可用于其他变量。
    • 生存期限制为外部函数调用的生存期。

引用类型总是创建数据的独立副本。
下面举例说明如何使用引用类型:

contract C {

  uint[] x;
  
  // the data location of values is memory
  function buy(uint[] memory values) public {
      x = value; // copies array to storage
      uint[] storage y = x; //data location of y is storage
      g(x); // calls g, handing over reference to x
      h(x); // calls h, and creates a temporary copy in memory
  }

  function g(uint[] storage) internal pure {}
  function h(uint[] memory) public pure {}
}

(注意h(x)传入memory参数后要创建一个临时拷贝)

数组

数组是一种在集合数据结构中存储相似数据的方法。
数组可以是固定大小,也可以是动态大小。它们的索引从 0 开始。
若要创建固定大小k和元素类型T的数组,则需要编写T[k]。 对于动态大小数组,应编写T[]。(不指定长度即默认为动态数组,类似vector)
数组元素可以是任何类型。例如,它们可以包含“uint”、“memory”或“bytes”,也可以包含“映射”或“结构”。
以下示例演示如何创建数组:

uint[] itemIds; // Declare a dynamically sized array called itemIds
uint[3] prices = [1, 2, 3]; // initialize a fixed size array called prices, with prices 1, 2, and 3
uint[] prices = [1, 2, 3]; // same as above

(与c类似,如果声明时直接初始化相当于编译器可以直接指定内存,因此即使不给出长度也是定义一个静态数组)

数组成员

以下成员既可以操作数组,又可以获取有关数组的信息:

  • length:获取数组的长度。
  • push:在数组末尾追加一个元素。
  • pop:从数组末尾删除元素。
// Create a dynamic byte array
bytes32[] itemNames;
itemNames.push(bytes32("computer")); // adds "computer" to the array
itemNames.length; // 1

结构

结构是用户可以定义用来表示实际对象的自定义类型,通常用作架构或用于表示记录。
结构声明示例:

struct Items_Schema {
    uint256 _id;
    uint256 _price;
    string _name;
    string _description;
}

映射类型

映射是封装或打包在一起的键值对。映射最接近JavaScript中的字典或对象。
通常使用映射来建模实际对象,并执行快速数据查找。这些值可以包括结构等复杂类型,这使得映射类型灵活且可读性强。
下面的代码示例使用结构Items_Schema ,并将Items_Schema表示的项列表保存为字典。映射通过这种方式模拟数据库。

contract Items {
    uint256 item_id = 0;

    mapping(uint256 => Items_Schema) public items;

    struct Items_Schema {
      uint256 _id:
      uint256 _price:
      string _name;
    }

    function listItem(uint256 memory _price, string memory _name) public {
      item_id += 1;
      item[vehicle_id] = Items_Schema(item_id, _price, _name);
    }
}