Solidity:类型-值类型

类型

Solidity是一种静态类型语言,所以每个变量都需要在编译时指定变量的类型。

“undefined”或“null”值的概念在Solidity中是不存在的

值类型

变量始终按值来传递。当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。

布尔类型

bool:取值为字面常数值truefalse

运算符:

  • ! (逻辑非)
  • && (逻辑与)
  • || (逻辑或)
  • ==(等于)
  • !=(不等于)

运算符||&&都遵循短路规则

整型

int:表示有符号的不同位数的整型变量。支持int8int256,以8位为步长增长。int等价于int256

uint:表示无符号的不同位数的整型变量。支持uint8uint256,以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标准如下图,几乎所有空间都用来表示数字,但只有少数的位来表示小数点的位置

img

地址类型Address

地址类型有两种形式

  • address:保存一个20字节(160位)的值(以太坊地址的大小)
  • address payable:可支付地址,与 address相同,不过有成员函数transfersend

区别在于address payable可以接收以太币的地址,而一个普通的address则不能

类型转换:

允许从address payableaddress的隐式转换,而从addressaddress 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

addressaddress payable的区别是在0.5.0版本引入的,同样从这个版本开始,合约类型不再继承自地址类型

不过如果合约有可支付的回退(payable fallback)函数或receive函数,合约类型仍然可以显示转换为addressaddress payable

地址类型成员变量
  • balancetransfer

可以使用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

sendtransfer的低级版本。如果执行失败。当前的合约不会因为异常而终止,但send会返回false

在使用send的时候会有些风险:如果调用栈的深度是1024,会导致发送失败,如果接收者用光了gas也会导致发送失败。所以为了保证以太币发送的安全,一定要检查send的返回值,使用transfer或者更好的办法:使用接收者自己取回资金的模式

  • calldelegatecallstaticcall

为了与不符合应用二进制接口的合约交互,或者要更直接地控制编码,提供了函数calldelegatecallstaticcall。它们都带有一个bytes memory参数和返回执行成功状态(bool)和数据(bytes memory

函数abi.encodeabi.encodePacked,abi.encodeWithSelectorabi.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.sendermsg.value值。此函数已在0.5.0版本中删除。

从以太坊拜占庭(byzantium)版本开始提供了staticcall,它与call基本相同,但如果被调用的函数以任何方式修改状态变量,都将回退

所有三个函数 calldelegatecallstaticcall 都是非常低级的函数,应该只把它们当作最后一招来使用,因为它们破坏了 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之前,合约直接从地址类型派生,并且addressaddress payable 之间没有区别。

如果声明一个合约类型的局部变量(MyContract c),则可以调用该合约的函数。注意需要赋相同合约类型的值给它,还可以实例化合约(即新创建一个合约对象)

合约和address的数据表示是相同的

合约不支持任何运算符

合约类型的成员是合约的外部函数及其public的状态变量

对于合约C可以使用type(C)获取合约的类型信息

定长字节数组

关键字有:bytes1bytes2bytes3,…, bytes32

运算符:

  • 比较运算符:<=<==!=>=> (返回布尔型)
  • 位运算符: &|^ (按位异或), ~ (按位取反)
  • 移位运算符: << (左移位), >> (右移位)
  • 索引访问:如果xbytesI类型,那么x[k](其中0<=k<I)返回第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

科学符号是支持的,尽管指数必须是整数,但底数可也是小数。比如:2e102.5e1

为了提高可读性可以在数字之间加上下划线。 例如,十进制123_000,十六进制 0x2eff_abde,科学十进制表示1_2e345_678都是有效的。 下划线仅允许在两位数之间,并且不允许下划线连续出现。添加到数字文字中下划线没有额外的语义,下划线会被编译器忽略。

数值字面常量表达式本身支持任意精度,除非它们被转换成了非字面常量类型。这意味着在数值常量表达式中,计算不会溢出,而除法也不会截断

例如, (2**800 + 1) - 2**800 的结果是字面常量 1(属于uint8类型),尽管计算的中间结果已经超过了以太坊虚拟机的机器字长度。此外,.5 * 8 的结果是整型 4 (尽管有非整型参与了计算)。

只要操作数是整型,任意整型支持的运算符都可以被运用在数值字面常量表达式。如果两个中的任一个数是小数,则不允许进行位运算。如果指数是小数的话,也不支持幂运算(可能会得到一个无理数)

常量作为左(或基)操作数和整数类型的移位和幂运算时总是执行正确的(指数)操作,不管右(指数)操作数的类型如何

Solidity对每个有理数都有对应的数值字面常量类型。整数字面常量和有理数字面常量都属于数值字面常量类型。除此之外,所有的数值字面常量表达式(即只包含数值字面常量和运算符的表达式)都属于数值字面常量类型。因此数值字面常量表达式1 + 22 + 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)之间的字符

此外,字符串字面常量支持下面的转义字符:

  • \<newline> (转义实际换行)
  • \\ (反斜杠)
  • \' (单引号)
  • \" (双引号)
  • \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)都不会被当成字符串字面常量的终止符。如果前面没有前置\,则换行符仅终止字符串字面常量。

Unicode 字面常量

常规字符串文字只能包含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)”

枚举可以在合约或库定义之外的文件级别上的声明。

函数类型

函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。函数类型有类型:

  • 内部(internal)函数类型
  • 外部(external)函数类型

内部函数只能在当前合约内被调用(在当前代码块内,包括内部库函数和继承的函数中),因此它们不能在当前合约上下文的外部被执行。调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。

外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。

函数类型表示成如下形式

function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

与参数类型相反,返回类型不能为空–如果函数类型不需要返回,则需要删除整个returns(<return types>)部分

函数类型默认是内部函数,因此不需要声明internal关键字,这仅适用于函数类型,合约中定义的函数明确指定可见性,它们没有默认值

类型转换:

函数类型A可以隐式转换为函数类型B当且仅当:它们的参数类型相同,返回类型相同,它们的内部/外部属性是相同的,并且A的状态可变性不比B的状态可变性更具限制性,例如:

  • pure函数可以转换为viewnon-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: ...} 代替。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XdpCs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值