目 录
pragma
pragma是指定当前Solidity文件编译器版本的指令。在pragma指令的帮助下,我们可以针对自己的代码选择相应的编译器版本。虽然在合约中写入pragma不是强制性的,但将pragma指令作为Solidity文件中的第一条语句是一种很好的做法。
pragma solidity >=0.7.0 <0.9.0;
上述语句的意思是:该Solidity文件可以被主版本大于7小于9的编译器编译。
pragma solidity ^0.4.19;
上述语句的意思是:该solidity只能被主版本为4的编译器编译。
^ 这个符号是指当前主版本的最新版本,也就是说 ^0.4.0 是指当前主版本号为4的最新版本。
mapping
映射是Solidity中最常用的复杂类型之一。映射类似于其他语言中的散列或者字典。它们存储键值对,并允许根据提供的键来检索值。
-
mapping类型只可以存在于storage中,不存在于memory中,因此它们是作为状态变量声明的。
-
mapping类型包含key/value对,不是实际存储key,而是存储key的keccak256哈希,用于查询value。
-
mapping不可以被分配给另一个mapping。
使用mapping关键字声明映射,后跟由=>表示法分割的键和值的数据类型。映射具有与任何其他数据类型一样的标识符,并且它们可以用于访问映射。
mapping(address => Voter) public voters;
上面这个语句的意思是:所有投票人,类型为address到voter映射。
注意:这个类型并不是本身自有的类型,而是合约中自行定义的结构体。有关信息请查阅的官方文档。
msg对象
msg对象代表调用合约时传递的消息内容。
-
msg.data(byte):完整的calldata
-
msg.gas(uint):剩余的gas量
-
msg.sender(address):消息的发送方(调用者)
-
msg.sig(bytes4):calldata的前四个字节(即函数标识符)
-
msg.value(uint):联盟链中无需使用此数据
msg.sender
msg.sender是非常重要的一个数据内容,它代表合约的调用者,会随着合约调用者身份的切换而改变。另外一个和调用者身份有关系的是tx对象下的origin,tx.origin始终代表最原始的调用者。当一个外部账户直接调用合约A时,合约A内看到的msg.sender和tx.origin没有区别,但若外部账户调用A合约,A合约又调用B合约时,此时B合约内看到的msg.sender是A合约的地址,tx.origin还是原来那个外部账户。如下图所示。
block对象
block对象就是区块对象。
-
block.coinbase (address): 当前块的矿工的地址
-
block.difficulty (uint):当前块的难度系数
-
block.gaslimit (uint):当前块gas的上限
-
block.number (uint):当前块编号
-
block.blockhash (function(uint) returns (bytes32)):函数,返回指定块的哈希值,已经被内建函数blockhash所代替
-
block.timestamp (uint):当前块的时间戳
contract
Solidity中的合约语法跟面向对象很相似,在一般的面向对象语言中,例如java:我们使用contract 来定义一个类;在这一个类之中,我们会有许多这个类的方法属性。在Solidity中,我们使用contract来定义一个合约,这个合约可以用对应的合约变量以及合约方法。
一个简单的合约定义:
pragma solidity >=0.7.0 <0.9.0;
contract Test{
//状态变量
uint256 data;
address owner;
//定义事件
event logData(uint256 dataToLog);
//函数修改器
modifier onlyOwner() {
if(msg.sender!=owner) throw;
}
//构造器,名字与合约名一致
function Sample(uint256 initData,address initOwner) {
data = initData;
owner = initOwner;
}
//函数
function getData() returns (uint256 returnedData) {
return data;
}
function setData() returns (uint256 newData) onlyOwner {
logData(newData);
data = newData;
}
}
constructor
合约函数构造器constructor,在部署合约时,构造函数用于初始化状态变量。
soliidity支持在合约中声明构造树1.在solidity中构造函数是可选的当合约中没有显示定义构造函数时,编译器会合成默认的构造函数。构造函数在部署合约时执行一次。这与其他编程语言不通,其他编程语言只有创建一个新的对象实例,就会执行一次相关构造函数。但是,在solidity中,执行的构造函数部署在EVM(以太坊虚拟机)上。
-
与其他编程语言不通,solidity智能合约中只能由一个构造函数。
-
构造函数与合约同名。
-
构造函数可以是public,也可以是internal。但不能是external或private。
-
构造函数不会显式的返回任何数据。
struct
struct是solidity中的自定义类型,相当于C语言中的结构体。使用solidity中的关键字struct进行自定义。结构体内还可以包含字符串、整型、映射、结构体灯复杂类型。
基本的结构体
struct Student{
uint stuId;
string stuName;
}
在People中定义Student结构体
结构体通常需要定义在一个合约中
contract People{
struct Student{
uint stuId;
string stuName;
}
}
构造结构体对象
contract People{
struct Student{
uint stuId;
string stuName;
}
function save() public{
Student memory student = Student(3200, "李华");
}
}
数据地址
-
memory :(值传递) 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。存储位置同我们普通程序的内存类似。即分配,即使用,越过作用域即不可被访问,等待被回收。
-
calldata :用来保存函数参数的特殊数据位置,是一个只读位置。是不可修改的、非持久的函数参数存储区域,效果大多类似memory。是外部函数的参数所必需指定的位置,但也可以用于其他变量。
-
storage:(指针传递) 状态变量保存的位置,只要合约存在就一直存储。
局部变量/状态变量 默认是storage
函数参数,包括返回的参数,默认是memory
函数默认为
public
类型,但是当我们的函数参数如果为storage
类型时,函数的类型必须为internal
或者private
转换问题:
-
storage—>storage
把一个storage类型赋值给另一个storage类型的值时,只是修改其指针(引用传递)。类似与C语言中的函数的参数通过数组传值。
pragma solidity ^0.4.0;
contract StorageToStorageTest{
struct S{string a;uint b;}
//默认是storage的
S s;
function storageTest(S storage s) internal{
S test = s;
test.a = "Test";
}
function call() returns (string){
storageTest(s);
return s.a;//Test
}
}
-
memory—>storage
分2种情况:
a.将memory–>状态变量;即将内存中的变量拷贝到存储中(值传递)
pragma solidity ^0.4.0; contract StorageToStorageTest{ struct S{string a;uint b;} //默认是storage的 S s; function storageTest(S s) internal{ s = s; s.a = "Test"; } function call() returns (string){ storageTest(s); return s.a;// } }
b.将memeory–>局部变量 报错
-
storage—>memory
将数据从storage拷贝到memory中
pragma solidity ^0.4.0; contract StorageToMemory{ struct S{string a;uint b;} S s = S("storage", 1); function storageToMemory(S storage x) internal{ S memory tmp = x;//由Storage拷贝到memory中 //memory的修改不影响storage tmp.a = "Test"; } function call() returns (string){ storageToMemory(s); return s.a;//storage } }
-
memory—>memory
storage转storage一样是引用传递
pragma solidity ^0.4.0; contract MemoryToMemory{ struct S{string a;uint b;} function smemoryTest(S s) internal{ S memory test = s; test.a = "Test"; } function call() returns (string){ //默认是storage的 S memory s = S("memory",10); smemoryTest(s); return s.a;//Test } }
地址类型
address
地址类型address是一个值类型。是20字节的数据类型。它是为了存储以太坊中的账户地址二特别设计的,其大小为160位或20字节。它可以保存合约账户地址以及外部拥有的账户地址。由于地址是一种值地址,它被赋值给另一个变量时会创建一个新副本。
address public chairperson;
地址类型成员
balance属性
用来快速查询余额
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
send函数
使用send方法将以太币发送到合约或个人拥有的账户。send函数返回一个布尔值true/false。在这种情况下,不返回异常。
msg.sender.send(1);
transfer函数
transfer方法类似于send方法,它负责将以太币或wei发送到一个地址。但是,区别在于,在执行失败的情况下,transfer会引发异常,而不是返回false,并且所有的更改都会被还原。
msg.sender.transfer(1);
transfer函数由于send函数,因为它在发生错误的情况时引发异常,这意味着在堆栈中产生异常并停止运行。
call函数
地址数据类型提供的call函数可以调用合约中的任何可用的函数。有时候合约接口(ABI)不可用,因此唯一调用函数的方法就是使用call函数。此方法不依附于ABI,可以根据需要调用任何函数。这些调用没有编译时检查,返回布尔值true或false。
最常用的调用方式,调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境(合约的 storage)。
delegatecall函数
调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境。
call函数
调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境。
payable
payable关键字是solidity语言中的一个修饰符,可以附加到任何函数上。附加后,该关键字允许该函数接收以太币。也就是说,当触发一个带有payable关键字的函数时,就可以将以太币(msg.value)与该交易一起发送出去。
revert
revert语句与require的作用是非常相似的。不同的是,它并不去检查条件也不依赖与任何状态或条件。碰到revert语句意味着异常会被抛出,未消耗的gas被退回,同时合约状态被恢复。
在下面的例子在,再用if进行条件检查时抛出异常。如果if条件的结果是false就会执行revert,这会导致异常的发生和合约执行的终止。
pragma solidity ^0.4.19
contract RevertContract{
function ValidInt8(int _data) public returns(uint8){
if(_data < 0 || _data > 255){
revert();
}
return uint8(_data);
}
}
以下场景使用
revert()
:处理与 require() 同样的类型,但是需要更复杂处理逻辑的场景 如果有复杂的 if/else 逻辑流,那么应该考虑使用 revert() 函数而不是require()。记住,复杂逻辑意味着更多的代码。
require
require函数用于确认条件有效性,如检验输入变量或合约状态变量是否满足条件以及验证外部合约条用返回的值。一般将require放在函数最开始的地方。
require由两个参数,第一个参数为条件判断的表达式(必须的参数),第二个参数为条件不满足式的提示信息(非必须的参数)。如下面的这个例子:
require(msg.value < 255, "too big");
当msg.value的值小于255时,则符合require中第一个表达式,所以程序进行向下进行。但如果msg.value大于255时,require函数就会抛出异常并给出提示“too big”。require出现抛出异常会退回未被消耗的gas。
以下场景使用
require()
:验证用户输入,即:
require(input<20);
验证外部合约响应,即:require(external.send(amount));
执行合约前,验证状态条件,即:require(block.number > SOME_BLOCK_NUMBER)
或者require(balance[msg.sender]>=amount)
一般地,尽量使用 require 函数 一般地,require 应该在函数最开始的地方使用 在我们的智能合约最佳实践中有很多使用 require() 的例子供参考。
assert
assert语句和require由类似的语法。它接受一个结果为true或false的语句。基于这个结果来确定是继续执行还是抛出异常。并且未消耗的gas不会杯退回给调用者而是被assert语句完全消耗掉,合约会被退回到初始状态。
assert一般是在函数的末尾使用,并尽量少使用assert的调用。
以下场景使用
assert()
:检查 overflow/underflow,即:
c = a+b; assert(c > b)
检查非变量(invariants),即:assert(this.balance >= totalSupply);
验证改变后的状态,不应该发生的条件 一般地,尽量少使用 assert 调用,如果要使用assert 应该在函数结尾处使用 基本上,require()
应该被用于函数中检查条件,assert()
用于预防不应该发生的情况,但不应该使条件错误。
访问权限关键字
智能合约访问权限共有4种类型:internal、public、private、external。
状态变量默认类型为:internal
合约函数默认类型未public
internal
internal声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是前面不能加前缀this,前缀this是表示通过外部方式访问。
-
internal类型的状态变量可供内部和子合约调用。
-
internal类型的函数和private类型的函数一样,智能合约自己内部调用,它和其他语言中的protected不完全一样。
-
子合约智能继承父合约中的所有public类型函数,可以对其进行重写,不能继承internal/private的函数。
-
内部(internal)函数只能在当前合约内被调用(在当前的代码块内,包括内部库函数,和继承的函数中),访问函数直接用函数名func。
-
外部(external)哈桑农户由地址和函数方法签名两部分组成,可作为外部函数调用的参数,或返回值,访问函数用this.func
public
public函数是合约接口的一部分,可以通过内部或者消息来进行调用。对于public类型的状态变量,会自动创建一个访问器。
-
当一个状态变量的权限为public类型时,它就会自动生成一个可供外部调用的get函数。
-
只有public类型的函数才可以被外部访问。
-
状态变量声明时,默认为internal类型,只有显式声明为public类型的状态变量才会自动生产一个和状态变量同名的get函数以供外部获取当前状态变量的值。
-
函数声明时默认为public类型,和显式声明为public类型的函数一样,都可供外部访问。
private
private函数和状态变量仅在当前合约中可以访问,在继承的合约内不可访问。
external
仅外部访问(在内部也能通过外部访问的方式访问)。
外部(external)函数是合约接口的一部分,这意味着它们可以从其他合约调用,也可以通过事务调用。
外部(external)函数func不能被内部调用)即func()不执行,但this.func()执行。
外部(external)函数,当它们接收大数组时,更有效。
function
function是solidity中的函数关键字。具体函数的定义形式如下:
function getName() public view returns(string){
return Myname;
}
returns
returns一般使用在函数的定义上,用来指定函数的返回类型。
利用returns函数接收多个值
函数返回多个值:
// 函数可以返回多个值
function multipleReturns() returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
同时接收所有值:
function processMultipleReturns() {
uint a;
uint b;
uint c;
// 这样来做批量赋值:
(a, b, c) = multipleReturns();
}
只接收部分返回值
// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() {
uint c;
// 可以对其他字段留空:
(,,c) = multipleReturns();
}
return
跟其他编程语言一样,表示用于返回值。
view
编写智能合约主要有以下三项活动:
-
更新状态变量
-
读取状态变量
-
逻辑执行
函数的执行和交易并不是免费的,需要花费gas。每笔交易都需要花费与其执行相对应的一定量的gas,调用者需要负责提供足量的gas以便成功执行。交易或修改以太坊全局状态的任何活动都如此。
有些函数仅负责读取和返回状态变量,这些函数就像其他编程语言中的getter属性一样。它们读取当前状态变量的值并将其返回给调用者。这些函数不会改变以太坊的涨停。
solidity开发人员可以使用view修改器标记其函数,以建议EVM此函数不会更改以太坊状态或之前提及的任何活动。
view
告诉我们运行这个函数不会更改和保存任何数据
pure
与view函数相比,pure函数在状态可变性方面由更多的限制。但是,它们的目的是相同的,即限制状态的可变性。
pure函数在view函数之上添加了进一步的限制,例如:甚至不允许pure函数读取以太坊的当前状态。简而言之,不允许pure函数读写以太坊的全局状态。
pure
告诉我们这个函数不但不会往区块链写数据,它甚至不从区块链读取数据。
constant
constat的作用跟view是一样的,是view的别名,不过constant在0.5.0版本中被去掉了。
想要更多的了解constant的相关信息以及它跟view的区别,可以看看下面这篇博客。
聊聊Solidity中的constant修饰符_程序新视界的博客-CSDN博客_constant solidity
event
事件是以太坊虚拟机(EVM)日志基础设施提供的一个接口,事件可以用来做操作记录,存储为日志。如果监听了某事件,当事件发生时,会进行回调。当定义的事件触发时,我们可以将事件存储到EVM的交易的日志中(日志是区块链上的一种特殊数据结构)。日志是与合约地址关联的,并存储在区块链中。
注意:
-
区块链上的交易往往会有日志记录,日志代表着智能合约所触发的事件。
-
不能直接访问日志和事件数据,即使是创建日志的合约。
在solidity中,使用event
关键字来定义一个事件。使用event
关键字声明一个事件,后跟一个标识符和参数列表并以分号结尾。参数中的值可用于记录信息或执行条件逻辑。事件信息及其值作为交易的一部分存储在区块内。
event EventName(address bidder, uint amount);
事件在合约中可以被继承。触发一个事件使用emit
。
emit
触发一个事件的代码
emit EventName(msg.sender, msg.value);
触发事件的调用
function testEvent() public {
// 触发一个事件
emit EventName(msg.sender, msg.value);
}
modifier
修改器
修改器是改变函数行为的特殊函数。比如用于在函数执行前检查某种前置条件。修改器是一种可被继承合约属性,同时还可被继承的合约重写(override)。下面我们来看一段示例代码:
pragma solidity >=0.6.0<0.8.0;
contract TestModifier{
uint public a = 0;
modifier myModifier() {
a = 5;
_; // 添加修改器的函数在执行时,函数代码被插入在这个位置执行。
a = 10;
}
// 添加修改器,callModifier函数调用后a=10
function callModifier() public myModifier {
a = 9;
}
}
带参数的修改器
pragma solidity >=0.6.0<0.8.0;
contract TestModifierArgument{
// 游戏玩家,等级>5,可以修改昵称,>10,可以修改皮肤颜色
uint public level = 0;
string public name;
string public skinColor;
// 不使用修改器
// function setName() public {
// if (level > 5) {
// name = "Noic";
// }
// }
// function setSkinColor() public {
// if (level > 10) {
// skinColor = "red";
// }
// }
// function setLevel(uint _level) public {
// level = _level;
// }
// 使用修改器
modifier levelRequire(uint _level) {
require(level > _level);
_;
}
function setName() public levelRequire(5) {
name = "Noic";
}
function setSkinColor() public levelRequire(10) {
skinColor = "red";
}
function setLevel(uint _level) public {
level = _level;
}
}
多重修改器
pragma solidity >=0.6.0<0.8.0;
contract TestModifierMany{
uint public a = 1;
modifier myModifier1{
a = 2;
_;
a = 3;
}
modifier myModifier2{
a = 4;
_;
a = 5;
}
// 先执行modifier2,执行完成后插入到modifier1中
// test1调用后a的值为3
function test1() public myModifier1 myModifier2 {
}
}