目录
1.first contract
·申明编译器版本
·定义合约
·合约构造函数
·定义变量
·定义函数
2.data type
·值类型(Value Types)
·引用类型(Reference Types)
·映射类型(Mapping Types)
Solidity是一种用于编写智能合约的编程语言。智能合约是在区块链平台上执行的自动化合约,它们定义了参与方之间的规则和条件,并确保这些规则在合约中被执行。
Solidity最初是为以太坊区块链平台设计的,但也被其他以太坊虚拟机(EVM)兼容的区块链平台广泛采用。它提供了一套丰富的库和功能,使开发者能够在智能合约中实现复杂的逻辑和业务流程。
使用Solidity编写的智能合约可以实现多种功能,包括创建和管理数字资产(如代币)、实施多方签名、执行投票和选举、创建去中心化应用程序(DApps)等。
一个合约通常由状态变量(合约数据)和合约函数组成。我们以最简单的Counter 计数器作为入门合约
编写合约首先要做的是声明编译器版本, 告诉编译器使用什么版本的编译器来编译
pragma solidity >=0.8.0; //使用大于等于0.8.0 版本的编译编译 Counter 合约
Solidity 使用 contract
定义合约,和其他语言的类(class
)很类似,合约本身也是一个数据类型, 称为合约类型,除此之外合约还可以定义事件、自定义类型等。
contract Counter { //定义了一个名为 Counter 的合约 }
构造函数是在创建合约时执行的一个特殊函数,用来初始化合约, constructor
关键字声明的一个构造函数。
如果没有初始化代码编译器会添加一个默认的构造函数constructor() public {}
。
状态变量的初始化,也可以在声明时进行指定,未指定时,默认为0。
contract Base { uint x; address owner; constructor(uint _x) public { x = _x; owner = msg.sender; } }
Solidity 是一个静态类型语言,在定义每个变量时需要声明该变量的类型,定义变量按格式: 变量类型
变量可见性
变量名
。变量可见性是可选的,没有显示申明可见性时,会使用缺省值 internal
。
uint public counter; //声明了一个变量名为 counter,类型为 uint(256位无符号整数)
合约中的变量会在区块链上分配一个存储单元。在以太坊中,所有的变量构成了整个区块链网络的状态,所以合约中变量通常称为状态变量。
但有两个特殊的“变量“, 他们不在链上分配存储单元:
constant
用来声明一个常量,不占用合约的存储空间,在编译时使用对应的表达式值替换常量名,即使用constant
修饰的状态变量,只能使用在编译时有确定值的表达式来给变量赋值。
contract C { uint constant x = 32**22 + 8; string constant text = "abc"; }
不可变量在构造函数中进行赋值,构造函数是在部署的时候执行,因此这是运行时赋值。
Solidity 中使用 immutable
来定义一个不可变量,它不会占用状态变量存储空间,在部署时,变量的值会被追加的运行时字节码中,因此它比使用状态变量便宜的多,同样带来了更多的安全性(确保了这个值无法再修改),因此不可变量在很多时候非常有用,比如保存创建者地址、关联合约地址等。
contract Example { uint immutable decimals; uint immutable maxBalance; constructor(uint _decimals, address _reference) public { decimals = _decimals; maxBalance = _reference.balance; } }
function关键字用来定于函数
function count() public { //名为 count() 函数,对counter状态变量加 1 counter = counter + 1; }
由于状态变量的改变,因此调用这个函数会修改区块链状态,这时我们就需要通过一个交易来调用该函数,调用者为交易提供 Gas,验证者(矿工)收取 Gas 打包交易,经过区块链共识后,counter
变量才真正算完成加1 ;
我们还可以根据需要定义函数的参数与返回值以及指定该函数是否要修改状态,一个函数定义形式可以这样表示:
function 函数名(<参数类型> <参数名>) <可见性> <状态可变性> [returns(<返回类型>)]{ }
在Solidity 中,返回值与参数的处理方式是一样的,代码中 返回值 result
也称为输出参数,我们可以在函数体里直接为它赋值,或直接在 return
语句中提供返回,返回值可以仅指定其类型,省略名称:
function addAB(uint a, uint b) public returns (uint) { .... return counter + a + b; }
同时,Solidity 支持函数有多个返回值:
function f() public pure returns (uint, bool, uint) { return (7, true, 2); } function g() public { *// 获取返回值* (uint x, bool b, uint y) = f(); } }
用 view 修饰的函数, 称为视图函数, 它只能读取状态,而不能修改状态,调用视图函数时,只需要当前链接的节点执行,就可返回结果
function cal(uint a, uint b) public view returns (uint) { return a * (b + 42) + now; }//cal() 函数不修改状态,它不需要提交交易,也不需要花费交易费
如果视图函数在一个会修改状态的函数中调用,那么视图函数会消耗 Gas,如:
function set(uint a, uint b) public returns (uint) { return cal(a, b); }
因为外部调用视图函数时 Gas 价格为0, 而在修改状态的函数中,Gas 价格随交易设定
用 pure 修饰的函数, 称为纯函数, 它既不能读取也不能修改状态,仅用作计算
function f(uint a, uint b) public pure returns (uint) { return a * (b + 42); }
值类型变量表示可以用32个字节表示的数据,包含以下类型,在赋值或传参时,总是进行拷贝。
当要表示一个数值时,通常用整型来表达
int/unit
int/uint 表示有符号和无符号不同位数整数。支持关键字uint8
到uint256
,uint
和int
默认对应的是uint256
和int256
。
关键字末尾的数字以8步进,表示变量所占空间大小,整数取值范围跟空间有关, 比如uint32
类型的取值范围是 0 到 2^32-1
(2的32次方减1),当没有为整型变量赋值时,会使用默认值 0。
整型支持的运算符包括以下几种:
比较运算符: <=
(小于等于)、<(小于) 、==
(等于)、!=(不等于)、>=
(大于等于)、>(大于)
位操作符: &
(和)、|(或)、^
(异或)、~
(位取反)
算术操作符:+
(加号)、-
(减)、-(负号),*
,/
, %(取余数), **
(幂)
移位: <<
(左移位)、 >>
(右移位)
Solidity 合约程序里,使用地址类型来表示我们的账号,另外合约和普通地址,都可以用address
类型表示,地址类型有两种:
address
:保存一个20字节的值(以太坊地址的大小)。
address payable
:表示可支付地址,在地址格式上,其实与address
完全相同,也是20字节,拥有的两个成员函数transfer
和send
,可以向该地址转账。当需要向地址转账时,可以使用以下代码把address
转换为address payable
:
address payable ap = payable(addr);
【注】:做此区分,显示的表达,一个地址可以接受ETH, 表示其有处理ETH的逻辑(EOA 账户本身可转账ETH);如果不做区分,当我们把 ETH 转到一个地址上时,恰巧如果后者是一个合约地址又没有处理ETH的逻辑,那么 ETH 将永远锁死在该合约地址上,任何人都无法提取和使用它。
地址类型的一些成员函数:
.balance
: 返回地址的余额, 余额以wei为单位 (uint256)。
.transfer(uint256 amount)
: 用来向地址发送数值为amount
的以太币(wei),transfer 函数只使用固定的 2300 gas , 发送失败时抛出异常。
.send(uint256 amount) returns (bool)
: send
和transfer
函数一样,同样使用固定的 2300 gas , 但在发送失败时返回false
,不抛出异常。
eg:
contract testAddr { // 如果合约的余额大于等于10,而x小于10,则给x转10 wei function testTrasfer(address payable x) public { address myAddress = address(this);//把合约转换为地址类型 if (x.balance < 10 && myAddress.balance >= 10) { //.balance获取余额 x.transfer(10); //转账 } } }
合约是一个类型,我们可以通过这个合约类型来创建合约(即部署合约),然后与合约里的函数交互,比如调用一个合约的函数可以创建一个另一个合约:
pragma solidity ^0.8.0; contract Hello { function sayHi() public view returns (uint) { return 10; } } contract HelloCreator { uint public x; Hello public h; • function createHello() public returns (address) { • h = new Hello(); • return address(h); } }
合约类型数据成员:
对于某个合约c有
(1)type(C).name
:获得合约的名字。
(2)type(C).creationCode
:获得创建合约的字节码。
(3)type(C).runtimeCode
:获得合约运行时的字节码。
【问】:如何区分合约和外部地址
答:区分一个地址是合约地址还是外部账号地址,关键是看这个地址有没有与之相关联的代码。EVM提供了操作码EXTCODESIZE
,用来获取地址相关联的代码大小(长度),如果是外部账号地址,则没有代码返回。因此我们可以使用以下方法判断合约地址及外部账号地址。
function isContract(address addr) internal view returns (bool) { uint256 size; assembly { size := extcodesize(addr) } return size > 0; }
如果是在合约外部判断,则可以使用web3.eth.getCode()
(一个Web3的API),或者是对应的JSON-RPC方法——eth_getcode。getCode()用来获取参数地址所对应合约的代码,如果参数是一个外部账号地址,则返回“0x”;如果参数是合约,则返回对应的字节码,下面两行代码分别对应无代码和有代码的输出。
>web3.eth.getCode(“0xa5Acc472597C1e1651270da9081Cc5a0b38258E3”) “0x” >web3.eth.getCode(“0xd5677cf67b5aa051bb40496e68ad359eb97cfbf8”) “0x600160008035811a818181146012578301005b601b6001356025565b8060005260206000f25b600060078202905091905056”
通过对比getCode()
的输出内容,就可以判断出是哪一种地址。
引用类型用来表示复杂类型,占用的空间超过32字节,在申明一个引用类型的变量,需要指定该变量的位置,拷贝时开销很大,因此可以使用引用的方式,通过多个不同名称的变量指向一个值,包括数组 和结构体。
在定义引用类型时,有一个额外属性来标识数据的存储位置:
memory(内存): 变量在运行时存在,其生命周期只存在于函数调用期间,gas开销较小。
storage(存储):保存状态变量,只要合约存在就一直保存在区块链中,gas开销最大。
calldata(调用数据):存储函数参数的特殊数据位置,用来接收外部数据,是一个不可修改的、非持久的函数参数存储区域,gas开销最小。
【注】:不同引用类型在进行赋值的时候,只有在不同的数据位置赋值时会进行一份拷贝,而在同一数据位置内通常是增加一个引用。
def:
数组和大多数语言一样, 在一个类型后面加上一个[]
,表示可以存储一组该类型的值。数组类型有两种:固定长度和动态长度
// 状态变量缺省位置为 storage uint [10] tens; // 固定长度的数组 uint [] numbers; // 动态长度的数组 address [10] admins; //admins最多有10个地址 // 作为参数,使用 calldata function copy(uint[] calldata arrs) public { numbers = arrs; // 赋值时,不同的数据位置的变量会进行拷贝。 } // 作为参数,使用 memory function handle(uint[] memory arrs) internal { } }
数组的初始化可以在声明时进行,还可以用new关键字进行声明,创建基于运行时长度的内存数组,使用 new 创建内存数组时,会根据长度在内存中分配相应的空间;如果变量是在存储中,则表示分配一个起始空间,在之后运行过程中可以扩展该空间
数组成员:
length
:表示当前数组的长度(只读)。
push()
:用来添加新的零初始化元素到数组末尾,并返回元素的引用,以便修改元素的内容,如:x.push().t = 2
或x.push() = b
,只对存储(storage)中的动态数组有效。
push(x):添加给定元素到数组末尾。 没有返回值,只对存储(storage)中的动态数组有效
pop()
:从数组末尾删除元素,数组的长度减1,会在移除的元素上隐含调用delete,及时释放不使用的空间,节约gas。pop()没有返回值,只对存储(storage)中的动态数组有效。
特殊的数组类型:
string:一个字符串也可以是一个字符数组,但不支持数组的push&pop方法
bytes:动态分配大小字节的数组,类似于byte[],但是bytes的gas费用更低。bytes 也可以用来表达字符串, 但通常用于原始字节数据;支持push&pop
//声明 bytes bs; bytes bs0 = "12abcd"; string str1 = "TinyXiong"; string name;
【注】:字符串s通过bytes(s)
转为一个bytes,通过下标访问bytes(s)[i]
获取到的不是对应字符,而是获取对应的UTF-8编码;Solidity 语言本身提供的string
功能比较弱,并没有提供一些实用函数
数组gas消耗:
function sum() public { uint len = numbers.length; for (uint i = 0; i < len; i++) { total += numbers[i]; }
分析上述sum()函数可以看出gas消耗是随着numbers
元素线性增长的,如果numbers
元素非常多,sum()
消耗 gas 会超过区块 gas 限制而无法执行,常见的解决方案:
将非必要的计算转移到链下进行。
想办法控制数组的长度。
想方法分段计算,让每段的计算工作量 Gas 可控。
Solidity 使用 struct
关键字来定义一个自定义组合类型
同时需要为每个成员定义其类型,除可以使用基本类型作为成员以外,还可以使用数组、结构体、映射作为成员:
struct Student { string name; mapping(string=>uint) score; int age; }
结构体的声明赋值
// 声明变量而不初始化 Person person; // 只能作为状态变量这样使用,按成员顺序(结构体声明时的顺序)赋值 Person person = Person(address(0x0), false, 18) ; // 在函数内声明 Person memory person = Person(address(0x0), false, 18) ; // 使用具名变量初始化,可不按成员定义的顺序赋值 Person person = Person({account: address(0x0), gender: false, age: 18}) ; //在函数内声明 Person memory person = Person({account: address(0x0), gender: false, age: 18}) ;
一种键值对的映射关系存储结构,定义方式为mapping,和Java的Map、Python的Dict在功能上类似。