Solidity是一种静态类型语言,所以每个变量都需要在编译时指定变量的类型。
“undefined”或“null”值的概念在Solidity中是不存在的
变量始终按值来传递。当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。
bool
:取值为字面常数值true
和false
运算符:
!
(逻辑非)&&
(逻辑与)||
(逻辑或)==
(等于)!=
(不等于)运算符||
和&&
都遵循短路规则
int
:表示有符号的不同位数的整型变量。支持int8
到int256
,以8位为步长增长。int
等价于int256
uint
:表示无符号的不同位数的整型变量。支持uint8
到uint256
,以8位为步长增长。uint
等价于uint256
运算符:
<=
, <
, ==
, !=
, >=
, >
(返回布尔值)&
, |
, ^
(异或), ~
(位取反)<<
(左移位) , >>
(右移位)+
, -
, 一元运算负 -
(仅针对有符号整型), *
, /
, %
(取余或叫模运算) , **
(幂)对于整形x
,可以使用type(x).min和type(x).max去获取这个类型的最小值和最大值。
Solidity的整数是有取值范围。在0.8.0开始,算术运算有两个计算模式:一个是截断模式或不检查模式,一个是检查模式。默认情况下,都会进行溢出检查,如果出现结果落在取值范围之外,就会调用失败异常进行回退。可通过unchecked{...}
切换到不检查模式
比较整型的值
位运算在数字的二进制补码表示上执行。
移位操作的结果具有左操作数的类型,同时会截断结果以匹配类型。右操作数必须是无符号类型。
x
正还是负,x << y
相当于 x * 2 ** y
。x
为正数,x >> y
相当于 x / 2 ** y
x
为负数,x >> y
相当于 (x + 1) / 2**y - 1
在版本0.5,0
之前,对于负 x
的右移 x >> y
相当于 x / 2 ** y
,即右移使用向上舍入而不是向下舍入
加法、减法和乘法与我们现实中使用相同。
在0.8.0
版本中加入溢出检查,默认情况下,算术运算都会进行溢出检查,但也可以通过unchecked{...}
来禁用检查,此时会返回截断后的结果;
在0.8.0
之前,是使用OpenZepplin SafeMath 库进行溢出的检查功能
表达式-x
相当于(x的类型(0)-x)。-x
只能应用在有符号型的整数上。
如果int x = type(int).min
,那么-x
将不在正数取值的范围内。这意味着这个检测unchecked{assert(-x==x)}
是可以通过的。如果是checked
模式,则会触发异常。具体详细理解,可以看一下补码的概念。
除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。在Solidity中,小数会取零。这意味着int256(-7)/int256(2) == int256(-2)
在智能合约中,字面常量上进行除法会保留精度(保留小数位)
除以0会发生Panic错误,而且不可以通过unchecked{...}
禁用掉
表达式type(int)min / -1
是仅有的整除会发生向上溢出的情况。在算术检查模式下,这会触发一个失败异常 ,在截断模式下,表达式的值将是type(int).min
模运算a%n
是在操作数a
的除以n
之后产生余数r
,其中q = int(a/n)
和r = a - (n*q)
。这意味着模运算结果与左操作数的符号相同。对于负数的a:a % n == -(a % n)
对0取模会发生错误Panic错误,该检查不能通过unchecked {...}
。
幂运算仅适用于无符号类型。结果的类型总是基数的类型。请注意类型足够大以能够容纳幂运算的结果,要么发生潜在的assert异常或者使用截断模式
在checked
模式下,幂运算仅会为小基数使用相对便宜的exp
操作码。例如x**3
的表达式,可能x*x*x
也许会更便宜。在任何情况下,都建议进行Gas消耗测试和使用优化器
注意 0**0
在EVM中定义为 1
Solidity还没有完全支持定长浮点型。可以定义声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。
fixed
:表示各种大小的有符号的定长浮点型。在关键字fixedMxn
中,M
表示该类型占用的位数,N
表示可用的小数位数。M
必须能整除8,即8到256位。N
则可以是从0到80之间的任意数。fixed
的别名是fixed28x19
ufixed
:表示各种大小的无符号的定长浮点型。在关键字fixedMxn
中,M
表示该类型占用的位数,N
表示可用的小数位数。M
必须能整除8,即8到256位。N
则可以是从0到80之间的任意数。ufixed
的别名是ufixed28x19
运算符:
<=
, <
, ==
, !=
, >=
, >
(返回值是布尔型)+
, -
, 一元运算 -
, 一元运算 +
, *
, /
, %
(取余数)浮点型(IEEE754标准)和定长浮点型之间最大的不同点是,在前者中整数部分和小数部分需要的位数是灵活可变的,而后者中这两部分的长度是受到严格的规定。IEEE754标准如下图,几乎所有空间都用来表示数字,但只有少数的位来表示小数点的位置
地址类型有两种形式
address
:保存一个20字节(160位)的值(以太坊地址的大小)address payable
:可支付地址,与 address
相同,不过有成员函数transfer
和send
区别在于address payable
可以接收以太币的地址,而一个普通的address
则不能
类型转换:
允许从address payable
到address
的隐式转换,而从address
到address payable
必须显示的转换,通过payable(address)进行转换
在 0.5版本,执行这种转换的唯一方法是使用中间类型,先转换为uint160 如 address payable ap = address(uint160(addr))
address
允许和uint160
、 整型字面常量、bytes20
及合约类型相互转换
只有通过payable(...)
表达式把address
类型和合约类型转换为address payable
。只有能接收以太币的合约类型,才能进行此转换。例如合约要么有receive或可支付的回退函数。注意payable(0)
是有效的,这是此规则的例外。
如果你需要address
类型的变量,并计划发送以太币给这个地址,那么声明类型为address payable
可以明确表达出你的需求。同样,尽早对它们进行区分或转换
运算符:
<=
, <
, ==
, !=
, >=
和 >
如果将使用较大字节数组类型转换为address
,例如bytes32
,那么address
将被截断。为了减少歧义,0.4.24及更高编译器版本要求我们在转换中显式截断处理。 以32bytes值 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC
为例, 如果使用 address(uint160(bytes20(b)))
结果是 0x111122223333444455556666777788889999AAAA
, 而使用 address(uint160(uint256(b)))
结果是 0x777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC
。
address
和 address payable
的区别是在0.5.0版本引入的,同样从这个版本开始,合约类型不再继承自地址类型
不过如果合约有可支付的回退(payable fallback)函数或receive函数,合约类型仍然可以显示转换为address
或address payable
balance
和transfer
可以使用balance
属性来查询一个地址的余额,也可以使用transfer
函数向一个可支付地址(payable address)发送以太币(以wei为单位):
address x = 0x234;
address myAddress = this;
if(x.balance < 10 && myAddress.balance >= 10)
x.transfer(10);
如果当前合约的余额不够多,则transfer
函数会执行失败,或如果以太转移被接收账户拒绝,transfer
函数同样会失败而进行回退
如果x
是一个合约地址,它的代码(如果有receive函数,执行receive接收以太函数,或存在fallback函数,执行Fallback回退函数)会跟transfer
函数调用一起执行(这是EVM的一个特性,无法阻止)。如果在执行过程中用光了gas或因为任何原因执行失败,以太币交易会被打回,当前的合约也会在终止的同时抛出异常
send
send
是transfer
的低级版本。如果执行失败。当前的合约不会因为异常而终止,但send
会返回false
在使用send
的时候会有些风险:如果调用栈的深度是1024,会导致发送失败,如果接收者用光了gas也会导致发送失败。所以为了保证以太币发送的安全,一定要检查send
的返回值,使用transfer
或者更好的办法:使用接收者自己取回资金的模式
call
,delegatecall
和staticcall
为了与不符合应用二进制接口的合约交互,或者要更直接地控制编码,提供了函数call
,delegatecall
和 staticcall
。它们都带有一个bytes memory
参数和返回执行成功状态(bool
)和数据(bytes memory
)
函数abi.encode
,abi.encodePacked
,abi.encodeWithSelector
和abi.encodeWithSignature
可用于编码结构化数据
例如:
bytes memory payload = abi.encodeWithSignature("register(string)","Myname");// 通过函数签名并传入参数
(bool success,bytes memory returnData) = address(nameReg).call(payload);通过call进行合约交互
require(success);
此外,为了与不符合应用二进制接口的合约交互,于是就有了可以接收任意类型任意数量参数的call
函数。这些参数会被打包到以32字节为单位的连续区域中存放。其中一个例外是当第一个参数被编码成正好4个字节的情况。在这种情况下,这个参数后边不会填充后续参数编码,以允许使用函数签名
address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;//地址
nameReg.call(bytes4(keccak256("fun(uint256)")),a);//当第一个参数被编码成正好4个字节的情况。在这种情况下,这个参数后边不会填充后续参数编码,以允许使用函数签名
所有这些函数都是低级函数,应谨慎使用。具体来说,任何未知的合约都可能是恶意的,我们在调用一个合约的同时就将控制权交给它了,而合约又可以回调合约,所以要准备好在调用返回时改变相应的状态变量,与其他合约交互的常规方法是在合约对象上调用函数(x.f())
0.5以前版本的Solidity允许这些函数接收任意参数,并且还会以不同方式处理bytes4类型的第一个参数。在版本0.5.0删除了这些边缘情况
可以使用gas
修改器调整提供的gas数量
address(nameReg).call{gas: 100000}(abi.encodeWithSignature("register(string)","Myname"));
类似地,也能控制提供以太币的值
address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)","Myname"));
最后,这些修改器可以联合使用。每个修改器出现的顺序不重要
address(nameReg).call{gas: 1000000, value: 1 ether}encodeWithSignature("register(string)","Myname"));
以类似的方式,可以使用函数delegatecall
:区别在于只调用给定地址的代码(函数),其他状态属性如(存储,余额…)都来自当前合约。delegatecall
的目的是使用另一个合约中的库代码。用户必须确保两个合约中的存储结构都适合委托调用(delegatecall)。
在以太坊家园(homestead)之前,只有callcode
函数,它无法访问原始的msg.sender
和msg.value
值。此函数已在0.5.0版本中删除。
从以太坊拜占庭(byzantium)版本开始提供了staticcall
,它与call
基本相同,但如果被调用的函数以任何方式修改状态变量,都将回退
所有三个函数 call
,delegatecall
和 staticcall
都是非常低级的函数,应该只把它们当作最后一招来使用,因为它们破坏了 Solidity 的类型安全性。
所有三种方法都提供gas
选项,而delegatecall
不支持value
选项。
不管是读取状态还是写入状态,最好避免在合约代码中硬编码使用的gas值。这可能会引入“陷阱”,而且gas的消耗也是可能会改变的。
所有合约都可以转换为address
类型,因此可以使用address(this).balance
查询当前合约的余额。
每一个contract定义都有他自己的类型
你可以隐式地将合约转换为从他们继承的合约。合约可以显式转换为address
类型
只有当合约具有receive函数或payable回退函数时,才能显式和address payable
类型相互转换,转换仍然使用address(x)
执行,如果合约类型没有接收或payable回退功能,则可以使用payable(address(x))
转换为address payable
在版本0.5.0之前,合约直接从地址类型派生,并且address
和 address payable
之间没有区别。
如果声明一个合约类型的局部变量(MyContract c
),则可以调用该合约的函数。注意需要赋相同合约类型的值给它,还可以实例化合约(即新创建一个合约对象)
合约和address
的数据表示是相同的
合约不支持任何运算符
合约类型的成员是合约的外部函数及其public的状态变量
对于合约C
可以使用type(C)
获取合约的类型信息
关键字有:bytes1
, bytes2
, bytes3
,…, bytes32
。
运算符:
<=
, <
, ==
, !=
, >=
, >
(返回布尔型)&
, |
, ^
(按位异或), ~
(按位取反)<<
(左移位), >>
(右移位)x
是bytesI
类型,那么x[k]
(其中0<=k)返回第k
个字节(只读)
该类型可以和作为右操作数的无符号整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。进行有符号整数位移运算会引发运行时异常。
成员变量:
.length
表示这个字节数组的长度(只读)可以将byte[]当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费31个字节。更好地做法是使用bytes
。否则可能会增加gas的成本
在 0.8.0 之前, byte
用作为 bytes1
的别名。
bytes
:变长字节数组,默认元素是32个字节。它并不是值类型
string
:变长UTF-8编码字符串类型。它并不是值类型
比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
这样的通过了地址校验和测试的十六进制字面常量会作为 address
类型。 而没有通过校验测试, 长度在 39 到 41 个数字之间的十六进制字面常量,会产生一个错误,您可以在零前面添加(对于整数类型)或在零后面添加(对于bytesNN类型)以消除错误。
混合大小写的地址校验和格式定义在 EIP-55中。
整数字面常量由范围在0-9的一串数字组成,表现成十进制。Solidity中没有八进制的,因此前置0是无效的
十进制小数字面常量带有一个.
,至少在其一边会有一个数字。比如:1.
,.1
,和 1.3
科学符号是支持的,尽管指数必须是整数,但底数可也是小数。比如:2e10
, 2.5e1
。
为了提高可读性可以在数字之间加上下划线。 例如,十进制123_000
,十六进制 0x2eff_abde
,科学十进制表示1_2e345_678
都是有效的。 下划线仅允许在两位数之间,并且不允许下划线连续出现。添加到数字文字中下划线没有额外的语义,下划线会被编译器忽略。
数值字面常量表达式本身支持任意精度,除非它们被转换成了非字面常量类型。这意味着在数值常量表达式中,计算不会溢出,而除法也不会截断
例如, (2**800 + 1) - 2**800
的结果是字面常量 1
(属于uint8
类型),尽管计算的中间结果已经超过了以太坊虚拟机的机器字长度。此外,.5 * 8
的结果是整型 4
(尽管有非整型参与了计算)。
只要操作数是整型,任意整型支持的运算符都可以被运用在数值字面常量表达式。如果两个中的任一个数是小数,则不允许进行位运算。如果指数是小数的话,也不支持幂运算(可能会得到一个无理数)
常量作为左(或基)操作数和整数类型的移位和幂运算时总是执行正确的(指数)操作,不管右(指数)操作数的类型如何
Solidity对每个有理数都有对应的数值字面常量类型。整数字面常量和有理数字面常量都属于数值字面常量类型。除此之外,所有的数值字面常量表达式(即只包含数值字面常量和运算符的表达式)都属于数值字面常量类型。因此数值字面常量表达式1 + 2
和 2 + 1
的结果跟有理数3
的数值字面常量类型相同。
在早期版本中(0.4.0之前),整数字面常量的除法也会截断,但在现在的版本中,会将结果转换成一个有理数。即 5 / 2
并不等于 2
,而是等于 2.5
。
数值字面常量表达式只要在非字面表达式中使用就会转换成非字面常量类型。在下面的例子中,尽管我们知道 b
的值是一个整数,但 a + 2.5
这部分表达式并不进行类型检查,因此编译不能通过.
uint a = 12;
uint b = a + 2.5 + 3;
字符串字面常量是指由双引号或单引号引起来的字符串("foo"
或者 'bar'
)。 它们也可以分为多个连续的部分("foo" "bar"
等效于”foobar”
),这在处理长字符串时很有用。 不像在 C 语言中那样带有结束符;”foo”
相当于 3 个字节而不是 4 个字节。 和整数字面常量一样,字符串字面常量的类型也可以发生改变,但它们可以隐式地转换成bytes1
,……,bytes32
,如果合适的话,还可以转换成bytes
以及string
。
例如:bytes32 samevar = "stringlit";
字符串字面常量在赋值给bytes32
时被解释为原始的字节形式。
字符串字面常量只能包含可打印的ASCII字符,这意味着他是介于0x1F(32)和0x7E(126)之间的字符
此外,字符串字面常量支持下面的转义字符:
\
(转义实际换行)\\
(反斜杠)\'
(单引号)\"
(双引号)\b
(退格)\f
(换页)\n
(换行符)\r
(回车)\t
(标签 tab)\v
(垂直标签)\xNN
(十六进制转义)\uNNNN
(unicode 转义)\xNN
表示一个16进制值,最终转换成合适的字节,而\uNNNN
表示Unicode编码值,最终会转换为UTF-8的序列。
以下示例中的字符串长度为十个字节,它以换行符开头,后跟,后跟双引号,单引号,反斜杠字符,以及字符序列 abcdef
。
任何Unicode行终结符(即LF,VF,FF,CR,NEL,LS,PS)都不会被当成字符串字面常量的终止符。如果前面没有前置\
,则换行符仅终止字符串字面常量。
常规字符串文字只能包含ASCII,而Unicode文字(以关键字unicode为前缀)可以包含任何有效的UTF-8序列。它们还支持与转义序列完全相同的字符作为常规字符串文字。
string memory a = unicode"helllo";
十六进制字面常量以关键字hex
打头,后面紧跟着用单引号或双引号引起来的字符串(例如:hex"002233F"
)。字符串的内容必须是一个十六进制的字符串,它们的值将使用二进制表示。
它们的内容必须是十六进制数字,可以选择单个下划线作为字节边界分隔符。字面常量的值将是十六进制序列的二进制表示形式。
用空格分隔的多个十六进制字面常量被合并为一个字面常量:hex"010A3123" hex"010A3123"
等同于hex"010A3123010A3123"
十六进制字面常量跟字符串字面常量很类似,具有相同的转换规则
string d = hex"010A3123";
string c = hex"01_0A_31_23";
string b = hex"01_0A_31_23" hex"01_0A_31_23";
string a = hex"010A3123" hex"010A3123";
枚举是在Solidity中创建用户定义类型的一种方法。它们是显示所有整型相互转换,但不允许隐式转换。从整型显式转换枚举,会在运行时检查整数是否在枚举范围内,否则会导致异常(Panic异常(检查异常))。枚举需要至少一个成员,默认值是第一个成员,枚举不能多于256个成员
数据表示与C中的枚举相同:选项从“0”开始的无符号整数值表示。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Demo {
enum letters{A, B, C, D}
letters letter;
letters constant defaultLetter = letters.A;
function setLetter(letters _letter) public {
letter = _letter;
}
function getLetter() public view returns (letters) {
return letter;
}
function getDefault() public pure returns (uint) {
return uint(defaultLetter);
}
}
由于枚举类型不属于ABI的一部分,因此对于所有来自Solidity外部的调用“getLetter”
的签名会自动被改成“getLetter()returns (uint8)”
枚举可以在合约或库定义之外的文件级别上的声明。
函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。函数类型有类型:
内部函数只能在当前合约内被调用(在当前代码块内,包括内部库函数和继承的函数中),因此它们不能在当前合约上下文的外部被执行。调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
函数类型表示成如下形式
function () {internal|external} [pure|constant|view|payable] [returns ()]
与参数类型相反,返回类型不能为空–如果函数类型不需要返回,则需要删除整个returns(
部分
函数类型默认是内部函数,因此不需要声明internal
关键字,这仅适用于函数类型,合约中定义的函数明确指定可见性,它们没有默认值
类型转换:
函数类型A
可以隐式转换为函数类型B
当且仅当:它们的参数类型相同,返回类型相同,它们的内部/外部属性是相同的,并且A
的状态可变性不比B
的状态可变性更具限制性,例如:
pure
函数可以转换为view
和non-payable
函数view
函数可以转换为non-payable
函数payable
函数可以转换为non-payable
函数其他的转换则不可以
如果一个函数payable
,这意味着它也接受0以太的支付,因此它也是non-payable
。另一方面,non-payable
函数将拒绝发送给它的以太币,所以non-payable
函数不能转换为payable
函数
如果当函数类型的变量还没有初始化时就调用它的话会引发一个Panic异常。如果在一个函数被delete
之后调用它也会发生相同的情况
如果外部函数类型在Solidity的上下文环境以外的地方使用,它们会被视为function
类型。该类型将函数地址紧跟其函数标识一起编码为一个bytes24
类型
当前合约的public函数既可以被当作内部函数也可以被当作外部函数使用。如果想将一个函数当作内部函数使用,就用function
调用,如果想将其当作外部函数,使用this.function
成员方法:
public(或external)函数都有下面的成员:
.address
:返回函数的合约地址.selector
:返回ABI函数选择器public(或external)函数过去有额外两个成员:.gas(uint)
和.value(uint)
在0.6.2中弃用了,在 0.8.0 中移除了。 用 {gas: ...}
和 {value: ...}
代替。