智能合约编程语言-solidity快速入门(上)
更新:HHH   时间:2023-1-7


solidity简介

本文默认读者已掌握至少一种面向对象编程语言,所以文中一些概念会借助其他语言进行类比。

solidity是用于实现智能合约的一种面向合约的高级编程语言,solidity受到C++、Python和JavaScript的影响,被设计为可运行在以太坊虚拟机(EVM)上,所以用户无需担心代码的可移植性和跨平台等问题。solidity是一种静态类型的语言,支持继承、库引用等特性,并且用户可自定义复杂的结构类型。

目前尝试 Solidity 编程的最好的方式是使用 Remix (由于是网页IDE可能加载起来需要一定的时间)。Remix 是一个基于 Web 的 IDE,它可以让你编写 Solidity 智能合约,然后部署并运行该智能合约,它看起来是这样子的:

也可以使用sublime或vs code等编辑器编写 Solidity 代码,然后复制粘贴到Remix上部署运行。

solidity官网地址如下:

https://solidity.readthedocs.io/en/latest/index.html


合约文件

本小节我们来说说合约文件,众所周知任何语言所编写的代码都需要存储在一个文件里,并且该文件都有一个特定的后缀名,我们一般将这种文件称之为代码文件。

solidity代码文件的后缀名为.sol,但我们通常会把使用solidity编写的文件称之为合约文件,一个合约文件通常会包含四个部分,其实与我们平时所编写其他语言的代码文件是类似的,如下图所示:

版本声明的代码需写在合约文件的开头,接着可以根据实际情况导入一些合约,所谓导入合约也就类似于其他面向对象的语言导入某个类的概念。然后就是声明一个合约,在合约里编写具体的代码,其实这里的合约与我们所熟悉的类的概念基本上是一样的,可以暂时将它们当做同一个东西。

我们先来对一个较为完整的合约代码进行一个预览,在之后会对代码中的每个部分进行逐一介绍:

// 版本声明
pragma solidity ^0.4.0;

// 导入一个合约
import "solidity_for_import.sol";

// 定义一个合约
contract ContractTest {
    // 定义一个无符号整型变量
    uint a;

    // 定义一个事件
    event Set_A(uint a);

    // 定义一个函数
    function setA(uint x) public {
        a = x;
        // 触发一个事件
        emit Set_A(x);
    }

    // 定义一个具有返回值的函数
    function getA() public returns (uint) {
        return a;
    }

    // 自定义一个结构类型
    struct Pos {
        // 定义一个有符号整型变量
        int lat;
        int lng;
    }

    // 定义一个地址类型,每个合约都运行在一个特定的地址上
    address public addr;

    // 定义一个函数修改器
    modifier owner () {
        require(msg.sender == addr);
        _;
    }

    // 让函数使用函数修改器
    function mine() public owner {
        a += 1;
    }
}

这里对函数修改器做一个简单的说明:

函数修改器的概念类似于python中的装饰器,其核心目的都是给函数增加函数内没有定义的功能,也就是对函数进行增强

从以上代码中,可以看到owner 函数修改器里定义了一句条件代码,其意义为:

msg.sender等于addr地址变量时,才继续往下执行,因为这个require函数是solidity校验条件用的,若不符合条件就会抛出异常

mine函数使用了owner函数修改器后,那么mine函数在执行之前,会先执行owner函数修改器里的条件代码,也就是说当msg.sender等于addr成立的话,才会执行mine函数里a += 1;的代码,否则就不会执行。从中也可以看出函数修改器里的_;语句,其实表示的就是mine函数里的代码,如此一来在不修改mine函数的前提下,给mine函数增加了额外的功能。


solidity 类型

Solidity是一种静态类型语言,意味着每个变量(本地或状态变量)需要在编译时指定变量的类型(或至少可以推导出类型),Solidity提供了一些基本类型可以用来组合成复杂类型。

Solidity和大多数语言一样,有两种类型:

  • 值类型(Value Type) - 变量在赋值或传参时,总是进行值拷贝。
  • 引用类型(Reference Types)

solidity所包含的值类型如下:

注:其中标红的是最常用的类型

官网关于solidity类型的文档地址如下:

https://solidity.readthedocs.io/en/latest/types.html

1.布尔类型取值范围是true和false,使用bool关键字进行声明,声明方式如下:

// 版本声明
pragma solidity ^0.4.0;

// 定义一个合约
contract ContractTest {
    bool b1 = true;
    bool b2 = false;
}   

2.solidity中有两种整型的定义方式,一种是无符号整型,另一种则是有符号整型。并且支持关键字uint8 到 uint256 (以8步进),uint 和 int 默认对应的是 uint256 和 int256。如下示例:

// 版本声明
pragma solidity ^0.4.0;

// 定义一个合约
contract ContractTest {
    // 定义一个无符号的整型变量
    uint a;
    // 定义一个有符号的整型变量
    int i;
}

solidity常量

在solidity里使用constant关键字来声明常量,但并非所有的类型都支持常量,当前支持的仅有值类型和字符串:

pragma solidity ^0.4.0;

contract C {
    uint constant x = 32**22 + 8;
    string constant text = "abc";
    bytes32 constant myHash = keccak256("abc");
}

在solidity中还可以将函数声明为常量,该函数的返回值就是常量值,这类函数将承诺自己不修改区块链上任何状态:

// 定义有理数常量
function testLiterals() public constant returns (int) {
    return 1;
}

// 定义字符串常量
function testStringLiterals() public constant returns (string) {
    return "string";
}

// 定义16进制常量,以关键字hex打头,后面紧跟用单或双引号包裹的字符串,内容是十六进制字符串
function testHexLiterals() public constant returns (bytes2) {
    return hex"abcd";
}

有理数常量函数里的运算可以是任意精度的,不会有溢出的问题:

// 定义有理数常量
function testLiterals() public constant returns (int) {
    return 1859874861811128585416.0 + 123.0;
}

科学符号也支持,基数可以是小数,但指数必须是整数,如下:

// 定义有理数常量
function testLiterals() public constant returns (int) {
    return 2e10;
}

solidity地址类型

solidity中使用address关键字声明地址类型变量,该类型属于值类型,地址类型主要用于表示一个账户地址,一个以太坊地址的长度为20字节的16进制数,地址类型也有成员,地址是所有合约的基础。

地址类型的主要成员:

  • 属性:balance,用来查询账户余额
  • 函数:transfer(),用来转移以太币(默认以wei为单位)

代码示例如下:

pragma solidity ^0.4.7;

contract AddrTest {

    // payable关键字定义一个可接受以太币的函数
    function deposit() public payable {

    }

    // 查询账户余额
    function getBalance() public constant returns (uint) {
        return this.balance;
    }

    // 转移以太币
    function transferEther(address towho) public {
        towho.transfer(10);
    }
}

然后我们将这段代码复制粘贴到remix中编译运行看看,首先需要在Compile选项卡中将代码进行编译:

编译成功后,到Run选项卡中,部署该合约:

部署成功后,可以查看到合约中的各个函数,并且只需要点击就可以运行指定的函数:

此时我们来点击执行一下getBalance函数:

可以看到,此时该合约的账户余额为0,现在我们来存储10个wei的以太币到合约中:

此时再执行getBalance函数,合约余额为10个wei:

然后我们再来看看转移/发送以太币的transferEther函数,此时我们这个合约地址的余额为10个wei,当我将这10个wei的以太转移到另一个地址后,当前合约的余额为0:

在solidity中一个能通过地址合法性检查(address checksum test)的十六进制常量就会被认为是地址,如:

0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF

而不能通过地址合法性检查的39到41位长的十六进制常量,会提示一个警告,被视为普通的有理数常量。

关于账户地址的合法性检查定义参考如下提案:

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md


solidity数组

在上文中我们提到Solidity 类型分为值类型和引用类型,以上小节介绍了常见的值类型,接下来会介绍一下引用类型。

引用类型是一个复杂类型,占用的空间通常超过256位, 拷贝时开销很大,因此我们需要考虑将它们存储在什么位置,是存储在memory(内存,数据不是永久存在)中还是存储在storage(永久存储在区块链)中。

所有的复杂类型如数组和结构体都有一个额外的属性:数据的存储位置(data location),可为memory和storage。根据上下文的不同,大多数时候数据存储的位置有默认值,也可以通过指定关键字storage和memory修改它。

函数参数(包含返回的参数)默认是memory。而局部复杂类型变量(local variables)和状态变量(state variables) 默认是storage。局部变量即部作用域(越过作用域即不可被访问,等待被回收)的变量,如函数内的变量,状态变量则是合约内声明的公有变量。

除此之外,还有一个存储位置是:calldata,用来存储函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多。还有一个存储位置是:calldata,用来存储函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多。

数组是一种典型的引用类型,在solidity中数组的定义方式如下:

  • T[k]:元素类型为T,固定长度为k的数组
  • T[]:元素类型为T,长度可动态调整的数组
  • bytes和string 是一种特殊的数组,string 可转为 bytes,而bytes则类似于byte[]

数组类型有两个主要成员:

  • 属性:length
  • 函数:push()

具体的示例代码如下:

pragma solidity ^0.4.7;

contract ArrayTest {
    // 定义一个无符号整型的变长数组
    uint[] public numbers = [1, 2, 3];

    // 定义一个字符串
    string str = "abcdefg";

    function getNumbersLength() public returns (uint) {
        // 往数组中添加一个元素
        numbers.push(4);

        // 返回数组的长度
        return numbers.length;
    }

    function getStrLength() public constant returns (uint) {
        // 将字符串转换为bytes并返回长度
        return bytes(str).length;
    }

    function getFirst() public constant returns (byte) {
        // 将字符串转换为bytes后,通过下标访问元素
        return bytes(str)[0];
    }

    function newMemory(uint len) public constant returns (uint) {
        // 定义一个定长数组并通过memory指定数组的存储位置
        uint[] memory memoryArr = new uint[] (len);
        return memoryArr.length;
    }

    function changeFirst(uint[3] _data) public constant returns (uint[3]) {
        // 通过索引操作元素
        _data[0] = 0;
        return _data;
    }
}

solidity结构体和映射

Solidity提供struct关键字来定义自定义类型也就是结构体,自定义的类型属于引用类型,如果学习过go语言的话应该对其不会陌生。如下示例:

// 版本声明
pragma solidity ^0.4.7;

// 定义一个合约
contract ContractTest {
    // 声明一个结构体
    struct Funder {
        address addr;
        uint amount;
    }

    // 将自定义的结构体声明为状态变量
    Funder funder;

    // 使用结构体
    function newFunder() public {
        funder = Funder({addr: msg.sender, amount: 10});
    }
}

solidity拥有映射类型,映射类型是一种键值对的映射关系存储结构,有点类似于python语言中的字典。定义方式为mapping(_KeyType => _KeyValue)。键类型允许除映射、变长数组、合约、枚举、结构体外的几乎所有类型值类型没有任何限制,可以为任何类型包括映射类型。

映射可以被视作为一个哈希表,所有可能的键会被虚拟化的创建,映射到一个类型的默认值(二进制的全零表示)。在映射表中,并不存储键的数据,仅仅存储它的keccak256哈希值,这个哈希值在查找值时需要用到。正因为此,映射是没有长度的,也没有键集合或值集合的概念。

映射类型有一点比较特殊,它仅能用来作为状态变量,或在内部函数中作为storage类型的引用。

可以通过将映射标记为public,来让Solidity创建一个访问器。通过提供一个键值做为参数来访问它,将返回对应的值。映射的值类型也可以是映射,使用访问器访问时,要提供这个映射值所对应的键,不断重复这个过程。

示例代码如下:

// 版本声明
pragma solidity ^0.4.7;

// 定义一个合约
contract ContractTest {
    // 定义一个映射类型,key类型为address,value类型为uint
    mapping(address => uint) public balances;

    function updateBalance(uint newBalance) public {
        // msg.sender作为键,newBalance作为值,将这对键值添加到该映射中
        balances[msg.sender] = newBalance;
    }
}
返回开发技术教程...