Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指定变量的类型。
“undefined”或“null”值的概念在Solidity中不存在,但是新声明的变量总是有一个 默认值 ,具体的默认值跟类型相关。 要处理任何意外的值,应该使用 错误处理 来恢复整个交易,或者返回一个带有第二个 bool 值的元组表示成功。
以下类型也称为值类型,因为这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。
bool :可能的取值为字面常量值 true 和 false
运算符:
! (逻辑非)
&& (逻辑与, “and” )
|| (逻辑或, “or” )
== (等于)
!= (不等于)
int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8 到 uint256 (无符号,从 8 位到 256 位)以及 int8 到 int256,以 8 位为步长递增。 uint 和 int 分别是 uint256 和 int256 的别名。
运算符:
比较运算符: <= , < , == , != , >= , > (返回布尔值)
位运算符: & , | , ^ (异或), ~ (位取反)
移位运算符: << (左移位) , >> (右移位)
算数运算符: + , - , 一元运算负 - (仅针对有符号整型), * , / , % (取余或叫模运算) , ** (幂)
对于整形 X,可以使用 type(X).min 和 type(X).max 去获取这个类型的最小值与最大值。
0.8.0 开始,算术运算有两个计算模式:一个是 “wrapping”(截断)模式或称 “unchecked”(不检查)模式,一个是”checked” (检查)模式。 默认情况下,算术运算在 “checked” 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。 你也可以通过 unchecked { … } 切换到 “unchecked”模式
位运算在数字的二进制补码表示上执行。 这意味着: ~int256(0)== int256(-1)
移位操作的结果具有左操作数的类型,同时会截断结果以匹配类型。 右操作数必须是无符号类型。 尝试按带符号的类型移动将产生编译错误。
x << y 等于数学表达式 x * 2 ** y。
x >> y 等于数学表达式 x / 2 ** y , 四舍五入到负无穷。
对于移位操作不会像算术运算那样执行溢出检查,其结果总是被截断。
表达式 -x 相当于 (T(0) - x) 这里 T 是指 x 的类型。 -x 只能应用在有符号型的整数上。 如果 x 为负数, -x 为正数。 由于使用两进制补码表示数据,你还需要小心:
如果有 int x = type(int).min;, 那 -x 将不在正数取值的范围内。 这意味着这个检测 unchecked { assert(-x == x); } 是可以通过的(即这种情况下,不能假设它的负数会是正数),如果是 checked 模式,则会触发异常。
除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。 在Solidity中,分数会取零。 这意味着 int256(-5) / int256(2) == int256(-2) 。
除以0 会发生 Panic 错误 , 而且这个检查,不可以通过 unchecked { … } 禁用掉。
模运算 a%n 是在操作数 a 的除以 n 之后产生余数 r ,其中 q = int(a / n) 和 r = a - (n * q) 。 这意味着模运算结果与左操作数相同的符号相同(或零)。 对于 负数的a : a % n == -(-a % n), 几个例子:
int256(5) % int256(2) == int256(1)
int256(5) % int256(-2) == int256(1)
int256(-5) % int256(2) == int256(-1)
int256(-5) % int256(-2) == int256(-1)
幂运算仅适用于无符号类型。 结果的类型总是等于基数的类型. 请注意类型足够大以能够容纳幂运算的结果,要么发生潜在的assert异常或者使用截断模式。
x**3 的例子,表达式 xxx 也许更便宜。 在任何情况下,都建议进行 gas 消耗测试和使用优化器。
Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。。
地址类型有两种形式,他们大致相同:
address:保存一个20字节的值(以太坊地址的大小)。
address payable :可支付地址,与 address 相同,不过有成员函数 transfer 和 send 。
这种区别背后的思想是 address payable 可以向其发送以太币,而不能先一个普通的 address 发送以太币,例如,它可能是一个智能合约地址,并且不支持接收以太币。
类型转换:
允许从 address payable 到 address 的隐式转换,而从 address 到 address payable 必须显示的转换, 通过 payable(
) 进行转换。可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个可支付地址(payable address)发送 以太币 (以 wei 为单位):
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
如果当前合约的余额不够多,则 transfer 函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer 函数同样会失败而进行回退。
如果 x 是一个合约地址,它的代码(更具体来说是, 如果有receive函数, 执行 receive 接收以太函数, 或者存在fallback函数,执行 Fallback 回退函数 函数)会跟 transfer 函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币 交易会被打回,当前的合约也会在终止的同时抛出异常。
send 是 transfer 的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 send 会返回 false。
在使用 send 的时候会有些风险:如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证 以太币 发送的安全,一定要检查 send 的返回值,使用 transfer 或者更好的办法: 使用接收者自己取回资金的模式
为了与不符合 应用二进制接口 的合约交互,或者要更直接地控制编码,提供了函数 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);
require(success);
可以使用 gas 修改器 调整提供的 gas 数量:
address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));
delegatecall 的目的是使用另一个合约中的库代码。 用户必须确保两个合约中的存储结构都适合委托调用 (delegatecall)。
从以太坊拜占庭(byzantium)版本开始 提供了 staticcall ,它与 call 基本相同,但如果被调用的函数以任何方式修改状态变量,都将回退。
所有三个函数 call , delegatecall 和 staticcall 都是非常低级的函数,应该只把它们当作 最后一招 来使用,因为它们破坏了 Solidity 的类型安全性。
所有三种方法都提供 gas 选项,而 value 选项仅 call 支持 。
你可以查询任何智能合约的部署代码。使用 .code 来获取EVM的字节码,其返回 bytes memory ,值可能是空。 使用 .codehash 获得该代码的 Keccak-256哈希值 (为 bytes32 )。注意, addr.codehash 比使用 keccak256(addr.code) 更便宜。
所有合约都可以转换为 address 类型,因此可以使用 address(this).balance 查询当前合约的余额。
每一个 contract 定义都有他自己的类型。
您可以隐式地将合约转换为从他们继承的合约。 合约可以显式转换为 address 类型。
只有当合约具有 接收receive函数 或 payable 回退函数时,才能显式和 address payable 类型相互转换 转换仍然使用 address(x) 执行, 如果合约类型没有接收或payable 回退功能,则可以使用 payable(address(x)) 转换为 address payable 。
合约不支持任何运算符。
合约类型的成员是合约的外部函数及 public 的 状态变量。
对于合约 C 可以使用 type© 获取合约的类型信息
关键字有:bytes1, bytes2, bytes3, …, bytes32。
运算符:
比较运算符: <=, <, ==, !=, >=, > (返回布尔型)
位运算符: &, |, ^ (按位异或), ~ (按位取反)
移位运算符: << (左移位), >> (右移位)
索引访问:如果 x 是 bytesI 类型,那么 x[k] (其中 0 <= k < I)返回第 k 个字节(只读)。
该类型可以和作为右操作数的无符号整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行有符号整数位移运算会引发运行时异常
bytes: 变长字节数组,参见 数组。它并不是值类型。
string: 变长 UTF-8 编码字符串类型,参见 数组。并不是值类型。
比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 这样的通过了地址校验和测试的十六进制字面常量会作为 address 类型。 而没有通过校验测试, 长度在 39 到 41 个数字之间的十六进制字面常量,会产生一个错误,您可以在零前面添加(对于整数类型)或在零后面添加(对于bytesNN类型)以消除错误。
字符串字面常量是指由双引号或单引号引起来的字符串( “foo” 或者 ‘bar’)。 它们也可以分为多个连续的部分( “foo” “bar” 等效于 “foobar”),这在处理长字符串时很有用。 不像在 C 语言中那样带有结束符; “foo” 相当于 3 个字节而不是 4 个。 和整数字面常量一样,字符串字面常量的类型也可以发生改变,
但它们可以隐式地转换成 bytes1,……, bytes32,如果合适的话,还可以转换成 bytes 以及 string。
例如: bytes32 samevar = “stringliteral” 字符串字面常量在赋值给 bytes32 时被解释为原始的字节形式。
字符串字面常量支持下面的转义字符:
\ (转义实际换行)
\\ (反斜杠)
\' (单引号)
\" (双引号)
\b (退格)
\f (换页)
\n (换行符)
\r (回车)
\t (标签 tab)
\v (垂直标签)
\xNN (十六进制转义,见下文)
\uNNNN (unicode 转义,见下文)
常规字符串文字只能包含ASCII,而Unicode文字(以关键字unicode为前缀)可以包含任何有效的UTF-8序列。 它们还支持与转义序列完全相同的字符作为常规字符串文字。
string memory a = unicode"Hello ";
十六进制字面常量以关键字 hex 打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex"001122FF" )。 字符串的内容必须是一个十六进制的字符串,它们的值将使用二进制表示。
枚举是在Solidity中创建用户定义类型的一种方法。 它们是显示所有整型相互转换,但不允许隐式转换。 从整型显式转换枚举,会在运行时检查整数时候在枚举范围内,否则会导致异常( Panic异常 )。 枚举需要至少一个成员,默认值是第一个成员,枚举不能多于 256 个成员。
数据表示与C中的枚举相同:选项从“0”开始的无符号整数值表示。
使用 type(NameOfEnum).min 和 type(NameOfEnum).max 你可以得到给定枚举的最小值和最大值。
一个用户定义的值类型允许在一个基本的值类型上创建一个零成本的抽象。 这类似于一个别名,但有更严格的类型要求。
用户定义值类型使用 type C is V 来定义,其中 C 是新引入的类型的名称, V 必须是内置的值类型(”底层类型”)。 函数 C.wrap 被用来从底层类型转换到自定义类型。同样地,函数函数 C.unwrap 用于从自定义类型转换到底层类型。
type UFixed256x18 is uint256;
函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。 函数类型有两类: - 内部(internal) 函数类型 - 外部(external) 函数类型。
内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。 调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
function () {internal|external} [pure|constant|view|payable] [returns ()]
与参数类型相反,返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个 returns () 部分。
函数类型默认是内部函数,因此不需要声明 internal 关键字。
类型转换:
函数类型 A 可以隐式转换为函数类型 B 当且仅当: 它们的参数类型相同,返回类型相同,它们的内部/外部属性是相同的,并且 A 的状态可变性比 B 的状态可变性更具限制性,比如:
pure 函数可以转换为 view 和 non-payable 函数
view 函数可以转换为 non-payable 函数
payable 函数可以转换为 non-payable 函数
public(或 external)函数都有下面的成员:
.address 返回函数的合约地址。
.selector 返回 ABI 函数选择器
引用类型可以通过多个不同的名称修改它的值
引用类型包括结构,数组和映射,如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:
内存 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
存储 状态变量保存的位置,只要合约存在就一直存储.
调用数据 用来保存函数参数的特殊数据位置,是一个只读位置。
所有的引用类型,如 数组 和 结构体 类型,都有一个额外注解 数据位置 ,来说明数据存储位置。
有三种位置: 内存memory 、 存储storage 以及 调用数据 calldata。
调用数据 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存 。 主要用于外部函数的参数,但也可用于其他变量。
在 存储 和 内存 之间两两赋值(或者从 调用数据 赋值 ),都会创建一份独立的拷贝。
从 内存 到 内存 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
从 存储 到本地存储变量的赋值也只分配一个引用。
其他的向 存储 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面 ArrayContract 合约 更容易理解)。
数组可以在声明时指定长度,也可以动态调整大小(长度)。
一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。
可以使用 .push() 方法在末尾追加一个新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。
bytes 和 string 也是数组
bytes 和 string 类型的变量是特殊的数组。
bytes 类似于 bytes1[],但它在 调用数据 和 内存 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。
string 与 bytes 相同,但不允许用长度或索引来访问。
可以使用 string.concat 连接任意数量的 string 字符串。 该函数返回一个 string memory ,包含所有参数的内容,无填充方式拼接在一起。 如果你想使用不能隐式转换为 string 的其他类型作为参数,你需要先把它们转换为 string。
同样, bytes.concat 函数可以连接任意数量的 bytes 或 bytes1 … bytes32 值。 该函数返回一个 bytes memory ,包含所有参数的内容,无填充方式拼接在一起。
可使用 new 关键字在 内存 中基于运行时创建动态长度数组。 与 存储 数组相反的是,你 不能 通过修改成员变量 .push 改变 内存 数组的大小。
必须提前计算所需的大小或者创建一个新的内存数组并复制每个元素。
在Solidity中的所有变量,新分配的数组元素总是以 默认值 初始化。
pragma solidity >=0.4.16 <0.9.0;
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, 2, 3] 的类型是 uint8[3] memory。 因为每个常量的类型都是 uint8 ,如果你希望结果是 uint[3] memory 类型,你需要将第一个元素转换为 uint 。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract LBC {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory) public pure {
// ...
}
}
数组常量 [1, -1] 是无效的,因为第一个表达式类型是 uint8 而第二个类似是 int8 他们不可以隐式的相互转换。 为了确保可以运行,你是可以使用例如: [int8(1), -1] 。
如果要初始化动态长度的数组,则必须显示给各个元素赋值:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
function f() public pure {
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
}
}
当使用存储数组时,你需要注意避免悬空的引用。 悬空引用是指一个指向不再存在的东西的引用,或者是对象被移除而没有更新引用。 例如,如果你将一个数组元素的引用存储在一个局部的引用中,然后从包含数组中 .pop() 出来,就会发生悬空引用。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract C {
uint[][] s;
function f() public {
// 保存s最后一个元素的指向。
uint[] storage ptr = s[s.length - 1];
// 移除 s 最后一个元素
s.pop();
// 向不再属于数组的元素写入数据
ptr.push(0x42);
// 现在添加元素到 ``s`` 不会添加一个空元素, 而是数组长度为 1, ``0x42`` 作为其元素。
s.push();
assert(s[s.length - 1][0] == 0x42);
}
}
ptr.push(0x42) 写入 不 会回退, 尽管 ptr 不再指向有效的 s 元素, 由于编译器假定未使用的存储空间总是被清零的, 所以随后的 s.push() 不会明确地将零写入存储空间。 所以在 push() 之后, s 的最后一个元素的长度是 1 ,并且包含 0x42 作为第一个元素。 0x42
作为其第一个元素。
数组切片是数组连续部分的视图,用法如:x[start:end] , start 和 end 是 uint256 类型(或结果为 uint256 的表达式)。 x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end - 1] 。
如果 start 比 end 大或者 end 比数组长度还大,将会抛出异常。
start 和 end 都可以是可选的: start 默认是 0, 而 end 默认是数组长度。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// 定义的新类型包含两个属性。
// 在合约外部声明结构体可以使其被多个合约共享。 在这里,这并不是真正需要的。
struct Funder {
address addr;
uint amount;
}
contract CrowdFunding {
// 也可以在合约内部定义结构体,这使得它们仅在此合约和衍生合约中可见。
struct Campaign {
address beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}
}
映射类型在声明时的形式为 mapping(KeyType => ValueType)。 其中 KeyType 可以是任何基本类型,即可以是任何的内建类型, bytes 和 string 或合约类型、枚举类型。 而其他用户定义的类型或复杂的类型如:映射、结构体、即除 bytes 和 string 之外的数组类型是不可以作为 KeyType 的类型的。
映射本身是无法遍历的,即无法枚举所有的键。不过,可以在它们之上实现一个数据结构来进行迭代。 例如,以下代码实现了 IterableMapping 库,然后 User 合约可以添加数据, sum 函数迭代求和所有值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;
struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }
struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}
type Iterator is uint;
library IterableMapping {
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0)
return true;
else {
keyIndex = self.keys.length;
self.keys.push();
self.data[key].keyIndex = keyIndex + 1;
self.keys[keyIndex].key = key;
self.size++;
return false;
}
}
function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0)
return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size --;
}
function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}
function iterateStart(itmap storage self) internal view returns (Iterator) {
return iteratorSkipDeleted(self, 0);
}
function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
return Iterator.unwrap(iterator) < self.keys.length;
}
function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1);
}
function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
uint keyIndex = Iterator.unwrap(iterator);
key = self.keys[keyIndex].key;
value = self.data[key].value;
}
function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return Iterator.wrap(keyIndex);
}
}
// 如何使用
contract User {
// Just a struct holding our data.
itmap data;
// Apply library functions to the data type.
using IterableMapping for itmap;
// Insert something
function insert(uint k, uint v) public returns (uint size) {
// This calls IterableMapping.insert(data, k, v)
data.insert(k, v);
// We can still access members of the struct,
// but we should take care not to mess with them.
return data.size;
}
// Computes the sum of all stored data.
function sum() public view returns (uint s) {
for (
Iterator i = data.iterateStart();
data.iterateValid(i);
i = data.iterateNext(i)
) {
(, uint value) = data.iterateGet(i);
s += value;
}
}
}
三元运算符是一个表达是形式:
如果 a 是一个 LValue(即一个变量或者其它可以被赋值的东西),以下运算符都可以使用简写:
a += e 等同于 a = a + e。其它运算符如 -=, *=, /=, %=, |=, &= , ^= , <<= 和 >>= 都是如此定义的。 a++ 和 a-- 分别等同于 a += 1 和 a -= 1,但表达式本身的值等于 a 在计算之前的值。 与之相反, --a 和 ++a 虽然最终 a 的结果与之前的表达式相同,但表达式的返回值是计算之后的值。
delete a 的结果是将 a 类型初始值赋值给 a。即对于整型变量来说,相当于 a = 0,delete 也适用于数组,对于动态数组来说,是将重置为数组长度为0的数组,而对于静态数组来说,是将数组中的所有元素重置为初始值。对数组而言, delete a[x] 仅删除数组索引 x 处的元素,其他的元素和长度不变,这以为着数组中留出了一个空位。如果打算删除项,映射可能是更好的选择。
如果对象 a 是结构体,则将结构体中的所有属性(成员)重置。
// SPDX-License-Identifier: GPL-3.0
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);
}
}
如果某些情况下编译器不支持隐式转换,但是你很清楚你要做的结果,这种情况可以考虑显式转换。 注意这可能会发生一些无法预料的后果,因此一定要进行测试,确保结果是你想要的! 下面的示例是将一个 int8 类型的负数转换成 uint:
int8 y = -3;
uint x = uint(y);
这段代码的最后, x 的值将是 0xfffff…fd (64 个 16 进制字符),因为这是 -3 的 256 位补码形式。