Solidity基础语法
◦ solidity数据存储位置
引用类型在 Solidity 中数据有一个额外的属性:存储位置,可选项为 memory 和 storage。
- memory:存储在内存中,即分配、即使用,越过作用域则不可访问,等待被回收。
- storage:永久存储在以太坊区块链中,更具体地说存储在存储 Merkle Patricia 树中,形成帐户状态信息的一部分。一旦使用这个类型,数据将永远存在。
- calldata:存储函数参数,它是只读的,不会永久存储的一个数据位置。外部函数的参数被强制指定为 calldata,效果与 memory 类似。
◦ solidity中的基础数据类型及其⻓度
- 值类型 如果一个类型将数据(值)直接保存在内存中,则称该类型为值类型。
值类型是大小不超过32字节内存的类型。Solidity 提供以下值类型:
bool:可以保存 true 或 false 作为其值的布尔值 uint:这是无符号整数,只能保存0和正值 int:这是可以保存负值和正值的有符号整数 address:这表示以太坊环境中的账户地址 byte:这表示固定大小的字节数组(byte1 到 bytes32)
1.1 整形
整数有助于将数字存储在合约中。Solidity 提供以下两种类型的整数:
有符号的整数:带符号的整数可以同时具有负值和正值。
无符号整数:无符号整数只能保持正值和零。除正值和零值以外,它们也可以保持负值。
对于每种类型,Solidity 都有多种类型的整数。Solidity 提供了 uint8 类型来表示8位无符号整数,并且以8的倍数表示,直到达到256。总之,可以声明32个不同的具有8的倍数的无符号整数,例如 uint8、uint16、unit24、uint256 位。同样,有符号整数的数据类型也是相同的,如 int8、int16,直到 int256。
根据要求,应选择适当大小的整数。例如,当存储0〜255之间的值时,uint8是合适的,而存储介于-128〜127 之间则 int8 更合适。对于更高的值,可以使用更大的整数。
有符号和无符号整数的缺省值为零,在声明时它们会自动初始化。
可以对整数执行数学运算,例如加法、减法、乘法、除法、指数、否定、后增量和预增量
1.2 布尔类型
像任何编程语言一样,Solidity 提供了一种布尔数据类型。bool 数据类型可用于表示具有二进制结果的场景,例如 true 或 false、1或0等。此数据类型的有效值为 true 和 false。值得注意的是,Solidity 中的布尔不能转换为整数,就像它们在其他编程语言中一样。它是一个值类型,任何赋值给其他的布尔变量都会创建一个新副本。Solidity 中 bool的默认值为 false。
声明和赋值 bool 数据类型的代码如下:
bool isPaid = true;
1.3 字节数据类型
字节是指8位有符号整数。内存中的所有内容都存储在由二进制值0和1组成的位中。Solidity 还提供字节数据类型以存储二进制格式信息。通常,编程语言只有一种数据类型来表示字节。但是,Solidity 具有多种字节类型。它提供的数据类型范围为 bytes1〜bytes32(含),以根据需要表示不同的字节长度。这些被称为固定大小的字节数组,并被实现为值类型。bytes1 数据类型代表1个字节,bytes2 代表2个字节。字节的默认值是 0x00,并用此值初始化。Solidity 也有一个 byte 类型,它是 bytes1 的别名。
一个字节可以以十六进制格式赋值,如下所示:
bytes1 aa = 0x65;
一个字节可以被赋值为十进制格式的整数值,如下所示:
bytes1 bb = 10;
一个字节可以被赋值为十进制格式的负整数值,如下所示:
bytes1 ee = -100;
一个字节可以赋值为字符值,如下所示:
bytes1 dd = 'a';
在下面的代码片段中,256不适合放入单个字节,需要更大的字节数组:
bytes2 cc = 256;
1.4 枚举类型
枚举是包含一个预定义的常量值列表的值类型。它们通过值传递,每个副本都维护自己的值。不能在函数内声明枚举,并在合约的全局域命名空间内声明。预定义的常量是连续赋值的,从零开始增加整数值。如:
enum status {created, approved, provisioned, rejected, deleted}
1.5 地址类型
地址是20字节的数据类型。它是为了存储以太坊中的账户地址而特别设计的,其大小为160位或20字节。它可以保存合约账户地址以及外部拥有的账户地址。地址是一种值类型,它被赋值给另一个变量时会创建一个新副本。
地址具有 balance 属性,该属性返回账户可用的以太币数量,并具有一些用于账户间交易以太币和调用合约函数的功能。
它提供以下两个函数来交易以太币:
transfer
send
当向一个账户发送以太币时,更应该选择 transfer 函数而不是send函数。send 函数返回一个布尔值,具体取决于以太币发送是否成功执行,而 transfer 函数引发异常并将以太币返还给调用者。
它还提供了以下三个用于调用合约函数的函数:
Call
DelegateCall
Callcode
- 引用类型
引用类型不直接将其值存储在变量本身中。它们存储的不是值,而是值存储位置的地址。该变量保存了指向另一个实际存储数据的内存位置的指针。
Solidity 提供以下引用类型:
数组:这是固定大小或动态大小的数组。 结构:这是自定义的即用户定义的结构。 字符串:这是字符序列。在 Solidity 中,字符串最终被存储为字节。 映射:与存储键值对的其他语言中的散列表或字典相似。
2.1 数组 数组是数据类型,但更具体地说,它们是依赖于其他数据类型的数据结构。数组是指相同类型的数值组。数组有助于将这些值存储在一起,并简化迭代、排序和搜索该组中元素或子元素的过程。Solidity 提供了丰富的数组结构,可以满足不同的需求。
Solidity 中的数组示例如下:
uint[5] intArray
Solidity 中的数组可以是固定的或动态的。
固定数组
固定数组是指声明了预定大小的数组。固定数组的例子如下:
int[5] age;
byte[4] flags;
固定数组无法使用new关键字进行初始化。它们只能以内联方式初始化,如下面的代码所示:
int[5] age = [1,2,3,4,5];
它们也可以稍后在函数中内联初始化,如下所示:
int[5] age;
age = [1,2,3,4,5];
动态数组
动态数组是指在声明时没有预定大小的数组,但是,它们的大小是在运行时确定的。看看下面的代码:
int[] age;
byte[] flags;
动态数组可以内联初始化,可以在声明时初始化,如下所示:
int[] age = [1,2,3,4,5];
特殊数组
Solidity 提供了以下两个特殊数组:
字节数组 字符串数组
a.字节数组:
字节数组是一个动态数组,可以容纳任意数量的字节。它与byte[]不同。byte[] 数组每个元素占用32个字节,而字节数组紧紧地将所有字节保存在一起。
字节可以声明为具有初始长度大小的状态变量,如以下代码所示:
bytes localBytes = new bytes(0);
这也可以分成与以前讨论的数组类似的以下两条代码行:
bytes localBytes;
localBytes = new bytes(10);
字节数组可以直接赋值,如下所示:
localBytes = "solidity is good";
此外,如果数据位于存储位置,则可以将值压栈其中,如下面的代码所示:
localBytes.push(byte(10));
字节数组还提供读/写长度属性,如下所示:
return localBytes.length;
localBytes.length = 4;
b.字符串数组
字符串是基于上一节讨论的字节数组的动态数据类型。它们与附加约束的字节数组非常相似。字符串不能被索引或压栈,也不具有 length 属性。要对字符串变量执行任何这些操作,应首先将其转换为字节,然后在操作后将其转换回字符串。
字符串可以由单引号或双引号内的字符组成。字符串可以直接声明并赋值,如下所示:
String name = "mike";
它们也可以转换为字节,如下所示:
Bytes byteName = bytes(name);
数组属性
数组支持一些基本的属性。在 Solidity 中,由于有多种类型的数组,并非每种类型都支持所有这些属性。
这些属性如下所示:
index:除了字符串类型外,所有类型的数组都支持用于读取单个数组元素的 index 属性。仅动态数组,固定数组和字节类型支持用于写入单个数组元素的 index 属性。字符串和固定大小的字节数组不支持写入。 push:仅动态数组支持此属性。 length:除了字符串类型外,此属性由读取透视图中的所有数组支持。只有动态数组和字节支持修改长度属性。 2.2 结构 结构或结构体有助于实现自定义的用户数据类型。结构是一种复合数据类型,由多个不同数据类型的变量组成。它们与合约非常相似,但是,它们不包含任何代码。它们只包含变量。
Solidity 的结构中的 struct 关键字进行声明。结构中的变量在花括号{}内定义,如图所示:
struct user { string name; uint age; int id; address addr; }
使用下面的语法来创建一个结构的实例。不需要显式调用关键字 new。关键字new只用于创建合约或者数组的实例,如图所示:
student = user("LiMing", 23, 1, 0xc6a115f2abc09746963d6a6800bd82157d27d8e7);
2.3 映射 映射是 Solidity 中最常用的复杂数据类型之一。映射类似于其他语言中的散列表或字典。它们存储键值对,并允许根据提供的键来检索值。
使用 mapping 关键字声明映射,后跟由=>表示法分隔的键和值的数据类型。映射具有与任何其他数据类型一样的标识符,并且它们可用于访问映射。
一个声明映射的例子如下:
mapping(uint => address) Names;
在前面的代码中,uint 数据类型用于存储键而 address 数据类型用于存储值。Names 用作映射的标识符。
虽然它类似于散列表和字典,但 Solidity 不允许迭代映射。如果键已知,则可以检索映射中的值。下一个示例说明如何使用映射。合约中维护有一个 uint 类型的计数器作为映射的键,并且在函数的帮助下存储和检索地址详细信息。
要访问映射中的任何特定值,相关键应与映射名一起使用,如下所示:
Names[uint(3)]
要在映射中存储值,请使用以下语法:
Names[uint(4)] = 0xc6a115f2abc09746963d6a6800bd82157d27d8e7
**◦ solidity数据类型转换 **
**◦ solidity构造函数 **
**◦ solidity继承 **
◦ solidity函数修饰符
简单继承,多重继承,以及修饰器(modifier)和构造函数(constructor)的继承。
继承
继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,solidity也是面向对象的编程,也支持继承。
规则 virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
override:子合约重写了父合约中的函数,需要加上override关键字。
注意:用override修饰public变量,会重写与变量同名的getter函数,例如:
mapping(address => uint256) public override balanceOf; 简单继承 我们先写一个简单的爷爷合约Yeye,里面包含1个Log事件和3个function: hip(), pop(), yeye(),输出都是”Yeye”。
contract Yeye { event Log(string msg);
// 定义3个function: hip(), pop(), man(),Log值为Yeye。 function hip() public virtual{ emit Log("Yeye"); }
function pop() public virtual{ emit Log("Yeye"); }
function yeye() public virtual { emit Log("Yeye"); } }
我们再定义一个爸爸合约Baba,让他继承Yeye合约,语法就是contract Baba is Yeye,非常直观。在Baba合约里,我们重写一下hip()和pop()这两个函数,加上override关键字,并将他们的输出改为”Baba”;并且加一个新的函数baba,输出也是”Baba”。
contract Baba is Yeye{ // 继承两个function: hip()和pop(),输出改为Baba。 function hip() public virtual override{ emit Log("Baba"); }
function pop() public virtual override{ emit Log("Baba"); }
function baba() public virtual{ emit Log("Baba"); } } 我们部署合约,可以看到Baba合约里有4个函数,其中hip()和pop()的输出被成功改写成”Baba”,而继承来的yeye()的输出仍然是”Yeye”。
多重继承 solidity的合约可以继承多个合约。规则:
继承时要按辈分最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。
如果某一个函数在多个继承的合约里都存在,比如例子中的hip()和pop(),在子合约里必须重写,不然会报错。
重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)。
◦ solidity异常
Solidity是通过回退状态的方式来处理异常错误(有点像事务回滚)。发生异常时会撤消当前调用(及其所有子调用)所改变的状态,同时给调用者返回一个错误标识。注意捕捉异常是不可能的,因此没有try catch。
1.条件检查 Solidity提供了assert和require来进行
- require: require函数来检查输入变量或合约状态变量是否满足条件以及验证调用外部合约返回值。可以有返回值require(condition, 'Something bad happened');
- assert: assert函数通常用来检查(测试)内部错误
注:同样作为判断一个条件是否满足的函数,require会退回剩下的gas,而assert会消耗所有的gas。 2.触发异常 提供了revert,throw来触发异常:
- throw:关键字抛出异常(从0.4.13版本,throw关键字已被弃用,将来会被淘汰。)回滚所有状态改变,返回”无效操作代码错误”,而且消耗掉剩下的gas
- revert:函数可以用来标记错误并回退当前调用,允许返回一个数值,将剩余gas返还调用者
传统处理异常的方式if...throw模式 即 if(msg.sender != owner) { throw; } 等价于:
- if(msg.sender != owner) { revert(); }//如果不等则异常
- assert(msg.sender == owner);//校验是否等于
- require(msg.sender == owner);
3.如何选择 require()函数用于: (1).确认有效条件,例如输入, (2).确认合约声明变量是一致的 (3).从调用到外部合约返回有效值
revert()函数用于 : 处理与 require() 同样的类型,但是需要更复杂处理逻辑的场景 如果有复杂的 if/else 逻辑流,那么应该考虑使用 revert() 函数而不是require()。
assert()函数用于:预防本不该发生的事情,如果发生就意味着合约中存在需要修复的bug(比如assert(1 > 2))。一般地,尽量少使用 assert 调用,一般assert 应该在函数结尾处使用
◦ solidity事件
事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中.
来捋这个关系:区块链是打包一系列交易的区块组成的链条,每一个交易“收据”会包含0到多个日志记录,日志代表着智能合约所触发的事件。
在DAPP的应用中,如果监听了某事件,当事件发生时,会进行回调。
不过要注意:日志和事件在合约内是无法被访问的,即使是创建日志的合约。
在Solidity 代码中,使用event 关键字来定义一个事件,如:
event EventName(address bidder, uint amount);
这个用法和定义函数式一样的,并且事件在合约中同样可以被继承。触发一个事件使用emit(说明,之前的版本里并不需要使用emit),如:
emit EventName(msg.sender, msg.value);
触发事件可以在任何函数中调用,如:
function testEvent() public {
// 触发一个事件
emit EventName(msg.sender, msg.value);
}
监听事件
通过上面的介绍,可能大家还是不清楚事件有什么作用,如果你跟过Web3与智能合约交互实战这篇文章,你会发现点击"Updata Info"按钮之后,虽然调用智能合约成功,但是当前的界面并没有得到更新。
使用事件监听,就可以很好的解决这个问题,让看看如何实现。
修改合约,定义事件及触发事件
先回顾一下合约代码:
pragma solidity ^0.4.21;
contract InfoContract {
string fName;
uint age;
function setInfo(string _fName, uint _age) public {
fName = _fName;
age = _age;
}
function getInfo() public constant returns (string, uint) {
return (fName, age);
}
}
首先,需要定义一个事件:
event Instructor(
string name,
uint age
);
这个事件中,会接受两个参数:name 和 age , 也就是需要跟踪的两个信息。
然后,需要在setInfo函数中,触发Instructor事件,如:
function setInfo(string _fName, uint _age) public {
fName = _fName;
age = _age;
emit Instructor(_fName, _age);
}