1. 开发环境(IDE)
练习Solidity最简单的IDE是一款在线的IDE-Remix。
直接访问如下地址即可:
https://remix.ethereum.org/
2. Solidity源文件结构
使用Solidity编写的合约源文件,后缀为.sol(Solidity的简称,就和.java类似)。
一个源文件中可以包含任意数量的如下部分:
- contract定义:使用contract关键字定义一个合约,类似java中的class关键字定义一个类一样。
- import指令:用于导入外部的Solidity文件。
- pragma指令:用于指定当前源文件使用的EVM版本范围。
- using for指令:
- struct定义:用于定义一个结构体类型,类似于java中的class定义一个新的类型一样。
- enum定义:用于定义一个枚举结构类型,类似于java中的enum定义一个枚举类型一样。
- function定义:用于定义一个函数,类似于java类中定义的方法一样。
- error定义:用于定义一个错误,类似于java中的异常一样。
- event定义:用于定义一个事件,类似于java中的log对象。
- modifier定义:用于定义一个函数修饰器,类似于java中的拦截器或者AOP(动态代理织入的切面逻辑)一样。
- 变量定义:类似于java中的成员变量或者局部变量。
- constant变量:
3. 一个固定的注释:SPDX许可标识符
// SPDX-License-Identifier: MIT
请注意:
- 可以在源代码任何地方添加上述注释,但建议放在首行
- 该注释只是额外说明,当前编写的合约是开源的,并且遵循SPDX的许可
4. 指定源文件的EVM版本
由于对Solidity源文件的编译需要以太坊虚拟机EVM的参与,而Solidity合约语言属于一门正在起步的语言,因此它对应的语法、以及编译器、运行环境等都在持续不断的迭代优化。
基于此,当你编写好一个源文件后,必须通过 pragma 指令指定当前源文件要求的EVM版本范围。
pragma solidity >=0.7.0 <0.9.0;
- pragma指令是一个本地源文件指令,它的作用访问永远是当前Solitidy源文件,如果通过import导入一个外部的Solidity文件,外部文件的pragma指令并不会导入进来。换言之,每个Solidity源文件都需要通过pragma指定EVM的版本范围。
- pragma指令指定EVM版本范围后,并不会自动选择执行当前文件的EVM版本,它只是用来告诉EVM编译器,明确的对源码中指定的EVM版本范围和真正使用的EVM版本做匹配,一旦不匹配,则直接提示编译错误。
5. 导入其他源文件
6. 注释
单行注释:// 多行注释:/**/
7. 合约结构-如何定义一个合约?
在Solidity中,合于类似于面向编程语言中的类。每个合约中可以包含如下部分:
- 状态变量(成员变量)
- 函数
- 函数修饰器
- 事件
- 错误
- 结构类型的声明
- 枚举类型的声明 并且,一个合约可以继承其他合约。
7.1 状态变量
状态变量就是合约成员,同样用来表示合约的状态,因为它表示的值将被永远的存储在合约实例对象中。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract SimpleStorage {
uint storedData; // 状态变量
// ...
}
定义状态变量需要指定:
- 类型
- 变量名称
- 可见性修饰符
7.2 函数
函数表示代码的基本执行单位,Solidity中的函数通常定义在一个contract声明的合约体内,当然也可以定义在合约体外部(同一个源文件中的合约体外部也可以声明函数,这个java中的使用不同)。
使用function声明一个函数,其目的是为了外部或者其他程序调度执行它,一个函数的核心功能是:接收一堆参数,返回对应的执行结果。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract SimpleAuction {
function bid() public payable { // 函数
// ...
}
}
// 定义在合约之外的辅助函数
function helper(uint x) pure returns (uint) {
return x * 2;
}
定义函数需要指定:
- function关键字
- 函数名称
- ()中指定函数接收的形式参数,包括参数类型和参数名称,可以指定多个
- 函数可见性修饰符
- 函数交易状态修饰符(用于表示一个函数和区块链进行交易的程度)
- returns关键字指定返回值(()中指定返回值类型,或者同时指定返回值类型和对应的返回值变量名称,在方法体中只需要给引用的返回值变量赋值即可自动返回),支持同时指定多个返回值类型或者返回值类型和对应声明的返回变量名
7.3 函数修饰器
使用modifier关键字声明一个函数修饰器。
// SPDX-License-Identifier: GPL-3.0
// 指定编译器版本
pragma solidity >=0.7.0 <0.9.0;
// 定义一个合约,用于实现函数修饰器
contract Modifier{
// 定义一个状态成员:
// private:表示该成员的可见性,只能在当前合约中可见,其他合约(子合约和外部)都不可以访问该成员
// internal:表示该成员可见性为内部可见,这意味着该成员可以被当前合约直接访问,或者在当前合约的派生合约中访问,类似于java中的protected或者default
// public:该合约可以被外部和其他合约访问,请注意,public修饰的成员,编译器会自动生成对应的public的get函数,供外部直接调用;当内部调用该成员时,直接通过this访问即可
bool private myStatus ;
modifier CheckAmount(uint _amount){
// 修饰器中,直接打印一个日志
emit PrintRequestAmount(_amount, "Begin to run modifier of CheckAmount");
require(_amount>10,"_amount must more than 10");
// _;这个语句,专门用于函数修饰器中,用来表示继续执行原始函数,类似于aop中的放行动作
_;
}
// 定义一个同名的函数修饰器,入参不同(同名异参即重载),Solidity将直接编译报错,即它不支持函数修饰器的重写操作
modifier CheckAmount(string _desc){
_;
}
// 定义一个事件,当函数成功执行后,打印日志
event PrintRequestAmount(uint requestAmount,string desc);
// 定义一个函数:入参是一个数字,使用自定义的函数修饰器修饰,将入参传递给函数修饰器
function transAmount(uint amount) public CheckAmount(amount){
// 进来之后,直接通过事件发布一个日志记录操作
emit PrintRequestAmount(amount, "print request amount");
}
// 尽管成员被定义为private,这只是表示该成员本身不能直接在外部或者其他合约中访问,但可以通过提供一个public函数,来间接的向外部暴露该成员的值
// view:表示该函数是一个只读函数,它意味着该函数只可以读取合约状态值,而不能修改合约状态值。很重要的一点是:view表示该函数并不会和区块链交互,只是在EVM内存中(区块链交易池中存储)读取
// 并且,很重要的一点,它修改的函数被执行时,
function retriveStatus() public view returns(bool){
// 在一个函数的交易属性修饰符为view的函数中,尝试修改合约状态成员,则编译错误
// myStatus = false;
return myStatus;
}
}
定义函数修饰器:
- 使用modifier关键字去声明
- 指定函数修饰器名称(一般首字母大写,和合约名称类似)
- ()指定修饰器需要接收的形式参数类型和参数名称,可以指定多个或者不指定
- 函数修饰器使用{}定义修饰器的拦截逻辑,和普通方法体类似,但一般用于在其中指定一些require的校验动作
- 函数修饰器始终在被修饰的函数真正执行之前调度,它类似一个前置拦截
- 函数修饰器方法体最后一行必须使用_;,该语句用于放行,即将调用线程放行到真正被修饰的目标函数中去,如果不指定,则编译报错
- 函数修饰器可以随着contract合约的继承关系一起被继承,也可以在子合约中进行覆盖(覆写)
- 在同一个contract合约中,同一个函数修饰器不允许被重载(重写),即尽管参数不同,但同一个合约中,不允许存在同名的函数修饰器声明
7.4 事件
Solidity中的事件使用关键字event来声明。它的事件和其他语言中的事件有些许区别,它在程序执行过程中发布的事件,实际上是会始终被EVM的log系统进行监听。
简单点描述就是:EVM在执行Solidity字节码时,其内部的日志服务会始终监听源码中通过emit关键字发布的事件函数,即发布一个事件函数的本质就是在调度EVM的日志接口去输出对应的事件日志。
事件是能方便地调用以太坊虚拟机日志功能的接口
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.22;
event HighestBidIncreased(address bidder, uint amount); // 事件
contract SimpleAuction {
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // 发布事件
}
}
定义一个事件:
- 使用event去声明事件
- 指定事件名称,最好首字母大写,和合约名称类似
- ()中指定事件函数需要接收的形式参数类型和对应的参数名称,可以指定多个或者不指定
- 事件定义中没有函数体,它不需要,它类似于一个函数式接口定义
- 在代码执行过程中,通过emit去触发(发布)指定的事件,并传入对应类型的事件参数
- 事件最终的执行结果,会通过EVM输出结果中的logs字段进行描述
7.5 错误
Solidity中使用error关键字来声明一个错误,错误本身和event声明的事件类似,是一个函数式接口,它没有函数体,看起来更像一个描述指定错误信息的实体定义。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// 没有足够的资金用于转账。要求 `requested`。
/// 但只有 `available` 可用。
error NotEnoughFunds(uint requested, uint available);
contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
uint balance = balances[msg.sender];
if (balance < amount)
revert NotEnoughFunds(amount, balance);
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
}
定义错误:
- 使用error关键字用于声明一个错误
- 指定错误名称,首字母大写,类似contract定义的合约名称一样
- ()中指定错误函数需要接受的形式参数类型和对应的参数名称,可以不指定或者指定多个
- 通过revert关键字抛出错误,并传入指定的实际参数值
- revert关键字用于回滚整个交易在区块链上产生的事务,即一旦合约程序在某处通过revert关键字抛出了错误,该合约对区块链产生的所有影响将回滚到操作区块链之前的状态
7.6 结构类型
Solidity中通过关键字struct来声明一个结构体,一个结构体本身是一个引用类型,类似于java中是实体类,用于描述一组有效的结构信息。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Ballot {
struct Voter { // 结构
uint weight;
bool voted;
address delegate;
uint vote;
}
}
声明结构体:
- 通过关键字struct声明
- 指定结构体的名称,建议首字母大写,驼峰命名
- {}中定义结构体所需要的字段,指定字段的类型,和字段的名称。多个字段之间通过;并换行进行定义
- 结构类型的声明必须在contract定义中
- struct类似于java中的class,它是一个关键字,用于自定义一个结构类型,即用它来定义一个自定义的引用类型
- 定义好类型后,就可以使用该自定义结构类型去修饰对应的变量
7.7 枚举类型
Solidity中使用关键字enum来定义一个枚举类型。枚举类型是一堆常量值组成的类型,其中所描述的常量值称为枚举常量,枚举常量的特征是有限的、可枚举的。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Purchase {
enum State { Created, Locked, Inactive } // 枚举
}
声明枚举类型:
- 使用enum关键字声明枚举类型
- 指定枚举类型的名称,建议首字母大写,驼峰命名
- {}中指定枚举常量,多个枚举常量使用,分隔
- 枚举类型的声明必须在contract定义中
- enum和java中的enum一样,它是一个关键字,用于自定义一个枚举类型,枚举类型也是一个引用类型
- 声明好枚举类型后,即可以通过它去定义其他变量或者在外部直接通过它引用对应的枚举常量
8. Solidity中的类型
Solidity是一种静态类型语言,这意味着每个变量(状态变量/成员变量、局部变量)都需要明确的指定其类型。
Solidity提供了几种基本类型,可以通过组合基本类型来构建出复杂的类型。
8.1 类型的默认值
请注意,在Solidity中不存在undefinied或者null值的概念,这一点和其他的高级语言比如java、javaScript有很大的不同。
一个新声明的状态变量,只要声明了该状态变量,编译器便会自动初始化该变量,并赋予它声明类型的默认值。
但是,和java中一样,对于function中定义的局部变量而言,必须显式的手动初始化(赋值),编译器并不会为局部变量自动初始化默认值。
8.2 类型的划分
8.2.1 值类型(基本类型)
值类型是指:在进行函数调用或者赋值等操作时,传递的始终是值,即始终进行值本身的拷贝动作。
和java中的基本类型类似,值类型的变量本身存储的就是值。
8.2.1.1 布尔类型
bool:可能的取值只能是常量 true 或者 false。
bool类型声明的变量可以使用如下运算符:
- !(逻辑非)
- &&(逻辑与,and)
(逻辑或,or) - ==(等于)
- !=(不等于) 运算符 && 和 || 遵循同样的短路原则,即f(x)&&f(y),如果f(x)是false的话,则不会执行f(y)。
8.2.1.2 整型
8.2.2 引用类型
Solidity中的引用类型包括如下:
- 使用struct自定义的结构体
- 数组
- 使用mapping关键字定义的映射 如果定义一个引用类型的变量,则必须明确地、显式地指定引用类型变量的对象存储位置。 请注意,EVM编译器要求任何应用类型变量(不论是引用类型的状态变量、还是局部变量、或者是方法形式参数变量),都需要显式地指定对应引用对象的数据存储位置。 而对于所有的值类型定义的变量,不能显式指定它的存储位置,因为值类型变量的存储位置由EVM根据上下文自动设置,一旦手动指定,编译器将报错。
定义引用类型变量时需要手动指定的对象存储位置
- memory :内存,表示该变量存储在局部内存中,作用范围只是定义在function函数中的局部变量,函数执行完毕,则内存释放。
- calldata :内存,和memory存储位置类似,也是在function局部变量中定义,它往往用于定义function的形式参数,表示该引用参数的存储位置在当前function的内存中,并且该存储位置的数据不能够被修改,和memory相比,它修饰的引用类型不能被重置。它主要针对提供给外部调用的external函数。
- storage :存储状态变量(成员变量)的数据位置,它的生命周期和合约部署后的实例周期完全一致,只要合约实例存在,它就存在;只有合约实例销毁,它才会释放。并且,storage修饰的状态变量一旦随着合约部署交易被打包执行,将持久化到区块链上永远存储。
关于storage的解释
storage 只能修饰合约成员变量(且默认修饰不能手动指定),即状态变量。或者可以修饰一个局部变量,但该局部变量必须直接指向一个状态变量。
关于它的存储位置,实际上直接指向的合约实例被打包部署到区块链上的存储位置,且会被永远存储。
EVM对合约字节码的部署过程,本身就是一个部署交易,它会要求区块链网络节点中成功挖到一个新的区块后,才会被部署(实例化字节码,初始化合约状态变量)并将其打包到新区块中。否则,合约部署交易会一直在区块链网络的交易池内存中存储等待被打包部署。
因此,当一个合约成功被部署意味着合约实例已经被成功创建并发布到区块链上了,这时合约中storage修饰的合约状态变量就随着合约的部署永远持久化到区块链上,并且可以供其他区块链节点进行访问。
关于存储位置的使用规则
- 状态变量:不论是值类型还是引用类型,默认存储在storage中,生命周期和合约实例完全一致,一旦部署成功将永存区块链上。并且不能手动显式指定,否则编译报错。
- 局部变量:
- 值类型的局部变量,默认存储位置是栈,不能手动指定存储位置否则报错。
- 引用类型的局部变量,存储位置必须显式指定为memory或者storage(只有当局部引用变量直接引用一个状态变量时才能显式指定为storage)。如果是函数的形式参数,则可以指定为calldata。
关于局部引用变量显式指定storage的规则限制
- 只有当局部引用变量直接引用一个合约状态变量时,才可以将其显式指定为storage,否则只能将其指定为memory。
- 局部引用变量使用storage只能引用一次外部的合约状态变量,相当于是对外部合约变量的一次性别名,引用后不能再次重置该局部引用变量的地址,否则编译报错。关于这一点,在delete运算符中有明显的体现。
- 局部引用变量使用storage引用外部合约状态变量,实际上是对外部合约变量的storage存储位置地址做了拷贝,而不是值拷贝,因此,通过操作局部引用变量指针,可以同时修改外部合约状态内容。
文档信息
- 本文作者:Marshall