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;
}
}