Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指定变量的类型。
Solidity 提供了几种基本类型,并且基本类型可以用来组合出复杂类型。
需要注意以下几点:
1.“undefined”或“null”值的概念在Solidity中不存在。
2.变量声明后将有默认初始值,其初始值字节表示全部为零。
3.bool 类型的默认值是 false。
4.uint 或 int 类型的默认值是 0 。
5.静态大小的数组和 bytes1 到 bytes32 ,每个单独的元素将被初始化为与其类型相对应的默认值。
6.对于动态大小的数组 bytes 和 string 类型,其默认缺省值是一个空数组或空字符串。
7.对于 enum 类型, 默认值是第一个成员。
值类型
布尔类型
bool:可能的取值为字面常量值 true 和 false 。
整型
int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8 到 uint256 (无符号,从 8 位到 256 位)以及 int8 到 int256,以 8 位为步长递增。 uint 和 int 分别是 uint256 和 int256 的别名。
警告
Solidity中的整数是有取值范围的。 例如 uint32 类型的取值范围是 0 到 2 ** 32-1 。 0.8.0 开始,算术运算有两个计算模式:一个是 “wrapping”(截断)模式或称 “unchecked”(不检查)模式,一个是”checked” (检查)模式。 默认情况下,算术运算在 “checked” 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。 你也可以通过 unchecked { ... }
切换到 “unchecked”模式,更多可参考 unchecked .
定长浮点型
警告
Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。
fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxN 和 fixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixed 和 fixed 分别是 ufixed128x19 和 fixed128x19 的别名。
地址类型 Address
地址类型有两种形式,他们大致相同:
1.address:保存一个20字节的值(以太坊地址的大小)。
2.address payable :可支付地址,与 address 相同,不过有成员函数 transfer 和 send 。
这种区别背后的思想是 address payable 可以接受以太币的地址,而一个普通的 address 则不能。
类型转换:
允许从 address payable 到 address 的隐式转换,而从 address 到 address payable 必须显示的转换, 通过payable(<address>)
进行转换。
address 允许和 uint160、 整型字面常量、bytes20 及合约类型相互转换。
只能通过 payable(...) 表达式把 address 类型和合约类型转换为 address payable。 只有能接收以太币的合约类型,才能够进行此转换。例如合约要么有 receive 或可支付的fallback函数。 注意 payable(0) 是有效的,这是此规则的例外。
地址类型成员变量
balance 和 transfer
可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位):
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
如果当前合约的余额不够多,则 transfer 函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer 函数同样会失败而进行回退。
注意
如果 x 是一个合约地址,它的代码(更具体来说是, 如果有receive函数, 执行 receive 接收以太函数, 或者存在fallback函数,执行 Fallback 回退函数 函数)会跟 transfer 函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether 交易会被打回,当前的合约也会在终止的同时抛出异常。
当然因为这个特性的存在,我们在设计合约的时候需要考虑到可重入攻击(利用transfer执行之后回调用fallback进入递归反复转账)
send
send 是 transfer 的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 send 会返回 false。
注意
send 是 transfer 的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 send 会返回 false。在使用 send 的时候会有些风险:如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证 以太币Ether 发送的安全,一定要检查 send 的返回值,使用 transfer 或者更好的办法: 使用接收者自己取回资金的模式。
call, delegatecall 和 staticcall
为了与不符合 应用二进制接口Application Binary Interface(ABI) 的合约交互,或者要更直接地控制编码,提供了函数 call,delegatecall 和 staticcall 。 它们都带有一个 bytes memory 参数和返回执行成功状态(bool)和数据(bytes memory)。
函数 abi.encode,abi.encodePacked,abi.encodeWithSelector 和 abi.encodeWithSignature 可用于编码结构化数据。
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);
此外,为了与不符合 应用二进制接口Application Binary Interface(ABI) 的合约交互,于是就有了可以接受任意类型任意数量参数的 call 函数。 这些参数会被打包到以 32 字节为单位的连续区域中存放。 其中一个例外是当第一个参数被编码成正好 4 个字节的情况。 在这种情况下,这个参数后边不会填充后续参数编码,以允许使用函数签名。
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);
可以使用gas修改器,调整提供的 gas 数量,也能控制提供的 以太币Ether 的值。 修改器modifier 可以联合使用。每个修改器出现的顺序不重要。
以类似的方式,可以使用函数 delegatecall :区别在于只调用给定地址的代码(函数),其他状态属性如(存储,余额 …)都来自当前合约。 delegatecall 的目的是使用另一个合约中的库代码。 用户必须确保两个合约中的存储结构都适合委托调用 (delegatecall)。
合约类型
1.只有当合约具有 接收receive函数 或 payable 回退函数时,才能显式和 address payable 类型相互转换 转换仍然使用 address(x) 执行, 如果合约类型没有接收或payable 回退功能,则可以使用 payable(address(x)) 转换为 address payable 。
2,.您还可以实例化合约(即新创建一个合约对象),使用new创建合约。
3.合约类型的成员是合约的外部函数及 public 的 状态变量。
4.对于合约 C 可以使用 type(C) 获取合约的类型信息,
定长字节数组
关键字有:bytes1, bytes2, bytes3, …, bytes32。
成员变量:.length 表示这个字节数组的长度(只读).
可以将 byte[] 当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费 31 字节。 更好地做法是使用 bytes。
边长字节数组
地址字面常量
有理数和整数字面常量
字符串字面常量及类型
Unicode 字面常量
常规字符串文字只能包含ASCII,而Unicode文字(以关键字unicode为前缀)可以包含任何有效的UTF-8序列。 它们还支持与转义序列完全相同的字符作为常规字符串文字。
十六进制字面常量
枚举类型
枚举是在Solidity中创建用户定义类型的一种方法。 它们是显示所有整型相互转换,但不允许隐式转换。 从整型显式转换枚举,会在运行时检查整数时候在枚举范围内,否则会导致异常( Panic异常 )。 枚举需要至少一个成员,默认值是第一个成员,枚举不能多于 256 个成员。
数据表示与C中的枚举相同:选项从“0”开始的无符号整数值表示。
pragma solidity >=0.4.16 <0.9.0;
contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() public {
choice = ActionChoices.GoStraight;
}
// 由于枚举类型不属于 |ABI| 的一部分,因此对于所有来自 Solidity 外部的调用,
// "getChoice" 的签名会自动被改成 "getChoice() returns (uint8)"。
function getChoice() public view returns (ActionChoices) {
return choice;
}
function getDefaultChoice() public pure returns (uint) {
return uint(defaultChoice);
}
}
函数类型
1.内部(internal) 函数类型,外部(external) 函数类型,公共函数(public),私有函数(private),函数类型默认是内部函数,因此不需要声明 internal 关键字
2.类型转换:
pure 函数可以转换为 view 和 non-payable 函数
view 函数可以转换为 non-payable 函数
payable 函数可以转换为 non-payable 函数,其他的转换则不可以。
3.public(或 external)函数都有下面的成员:
.address 返回函数的合约地址。
.selector 返回 ABI 函数选择器
引用类型
1.memory 内存即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
2.存储 storage 状态变量保存的位置,只要合约存在就一直存储.
3.调用数据 calldata 用来保存函数参数的特殊数据位置,是一个只读位置。
数据位置
数据位置与赋值行为
1.在存储storage和内存memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
2.从内存memory到内存memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
3.从 存储storage 到本地存储变量的赋值也只分配一个引用。
4.其他的向 存储storage 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝。
数组
1.一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。
2.一个长度为 5,元素类型为 uint 的动态数组的数组(二维数组),应声明为 uint[][5]
创建内存数组
1.不能 通过修改成员变量 .push 改变 内存 memory 数组的大小
2.定长的 内存memory 数组并不能赋值给变长的 内存memory 数组
3.
contract TX {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}
数组成员
1.length
2.push() x.push().t = 2 或 x.push() = b
3.push(x) 固定 gas 费用
4.pop 根据现有的数组长度和移除的个数收取 gas 费用
数组切片
1.数组切片是数组连续部分的视图,用法如:x[start:end] , start 和 end 是 uint256 类型(或结果为 uint256 的表达式)。 x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end - 1] 。
2.目前数组切片,仅可使用于 calldata 数组.
结构体
1.和大多数编程语言一样,结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。
2.在合约外部声明结构体可以使其被多个合约共享。
映射
映射类型在声明时的形式为 mapping(_KeyType => _ValueType)。
使用:
pragma solidity >=0.4.0 <0.9.0;
contract MappingExample {
mapping(address => uint) public balances;
function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}
contract MappingLBC {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(this);
}
}
可迭代映射
映射本身是无法遍历的,即无法枚举所有的键。不过,可以在它们之上实现一个数据结构来进行迭代。
struct IndexValue { uint keyIndex; uint value; } //keyindex 表示key的索引
struct KeyFlag { uint key; bool deleted; } //key的实际值和删除状态
struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys; //创建一个key的数组来储存KEY的状态
uint size;
}
涉及 LValues 的运算符
delete
pragma solidity >=0.4.0 <0.9.0;
contract DeleteLBC {
uint data;
uint[] dataArray;
function f() public {
uint x = data;
delete x; // 将 x 设为 0,并不影响数据
delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本
uint[] storage y = dataArray;
delete dataArray;
// 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响,
// 因为它是一个存储位置是 storage 的对象的别名。
// 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
assert(y.length == 0);
}
}
基本类型之间的转换
隐式转换
uint8 可以转换成 uint16,int128 转换成 int256,但 int8 不能转换成 uint256
显式转换
如果某些情况下编译器不支持隐式转换,但是你很清楚你要做的结果,这种情况可以考虑显式转换。
uint16 b = uint16(a); // 此时 b 的值是 0x5678
字面常量与基本类型的转换
整型与字面常量转换
十进制和十六进制字面常量可以隐式转换为任何足以表示它而不会截断的整数类型
uint32 b = 1234; // 可行
uint16 c = 0x123456; // 失败, 会截断为 0x3456
定长字节数组与字面常量转换
十进制字面常量不能隐式转换为定长字节数组。十六进制字面常量可以是,但仅当十六进制数字大小完全符合定长字节数组长度。 不过零值例外,零的十进制和十六进制字面常量都可以转换为任何定长字节数组类型:
bytes2 b = 0x12; // 不可行
bytes2 c = 0x123; // 不可行
bytes2 d = 0x1234; // 可行
bytes2 e = 0x0012; // 可行
bytes4 f = 0; // 可行
bytes4 g = 0x0; // 可行
字符串字面常量和十六进制字符串字面常量可以隐式转换为定长字节数组,如果它们的字符数与字节类型的大小相匹配:
bytes2 b = "xy"; // 可行
bytes2 c = hex"12"; // 不可行
bytes2 d = hex"123"; // n不可行
bytes2 e = "x"; // 不可行
bytes2 f = "xyz"; // 不可行
地址类型
从 bytes20 或其他整型显示转换为 address 类型时,都会作为 address payable 类型。
一个地址 address a 可以通过payable(a) 转换为 address payable 类型.