Vs Code 中使用 solidity
安装插件
安装 solidity 插件
书写合约
在编写 solidity 的合约时要在首行加上 MIT 许可证,并选择适当的 SPDX 许可证标识符。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
// Contract code goes here...
contract Hello{
function hello() public pure returns(string memory){
return "hello world";
}
}
编译代码
F5 编译当前合约,Cmd+F5 编译所有合约
solidity 学习
入门篇
pragma
指定 solidity 的版本,只对自己的源文件生效
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Hello {
// 在solidity 中中文字符无法被识别,要使用Unicode编码进行转译
string public message = unicode"你好";
function helloWorld() view public returns (string memory) {
return message; // 返回 message 变量的值
}
}
引入 SPDX 许可证
Solidity ^0.6.8 以上版本要求引入 SPDX 许可证,否则会出现警告
有关于 SPDX 的详细解析可以跳转查看:Solidity SPDX是什么 - 编程宝库 (codebaoku.com)
一般是在 .sol 文件第一句加上:
// SPDX-License-Identifier: MIT
或者
// SPDX-License-Identifier: SimPL-3.0
contract/智能合约
智能合约是位于以太坊区块链上特定地址的代码和数据的集合。
contract 关键字表示一个智能合约。
导入文件
//要从当前目录导入文件 x,请使用 import "./x"。如果不指定当前路径,可能会在全局 “include” 目录中引用另一个文件。
// 从 data01.sol 中导入所有全局符号
import "./data01.sol";
// 创建一个新的全局符号 symbolName,它的成员都是来自 data01.sol 的全局符号。
import * as storedData from "./data01.sol";
保留关键字
Solidity 中的保留关键字在以下地址中有所展示:
Solidity 基础语法 - 编程宝库 (codebaoku.com)
代码注释
function getResult() public view returns(uint){
// 这是一行注释,类似于c++中的注释
/*
* 这是多行注释
* 类似于c语言中的注释
*/
uint a = 1;
uint b = 2;
uint result = a + b;
return result;
}
数据类型
- 值类型
- 布尔型 bool
- 整型 int/uint int8 to int256 uint8 to unit256
- 定长浮点型 fixed/unfixed fixedMxN ufixedMxN
uint x = 100;
int x = -200;
byte32 b = 0x01020304
// 测量类型的最大值和最小值:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
uint public a = type(uint).min;
uint public b = type(uint).max;
}
- 地址类型
- 地址类型表示以太坊地址,长度为20字节。
- 地址可以使用 .balance 属性获得余额
- 也可以使用 .transfer() 方法将余额转到另一个地址。
address x = 0x212;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10)
x.transfer(10);
- 引用类型
- 数组 (字符串与bytes是特殊的数组,所以也是引用类型)
- struct (结构体)
- map (映射)
变量
变量基础内容
- 状态变量
- 变量值永久保存在智能合约存储空间中的变量。
- 定义形式类似于类中的成员变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest {
uint storedData; // 状态变量
constructor() {
storedData = 10; // 使用状态变量
}
}
- 局部变量
- 变量值仅在函数执行过程中有效的变量,函数退出后,变量无效。
- 函数参数也是局部变量。
- 局部变量不会上链,只存在于所处函数的生命周期。
pragma solidity ^0.8.0;
contract SolidityTest {
function sum() public pure returns(uint){
uint a = 1; // 局部变量
uint b = 2;
uint result = a + b;
return result; // 访问局部变量
}
}
// 运行上述程序,输出:
0: uint256: 3
- 全局变量
- 保存在全局命名空间,用于获取整个区块链相关信息的特殊变量,与当前合约无关
- 比如:时间戳、块高等,更多可查看:Solidity 变量 - 编程宝库 (codebaoku.com)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest {
// 获取当前区块号、时间戳、调用者地址
function getGlobalVars() public view returns(uint,uint,address){
return (block.number,block.timestamp,msg.sender);
}
}
- 变量命名规则
- 不应使用 Solidity 保留关键字作为变量名。例如:break 或 boolean 变量名无效。
- 不应以数字(0-9)开头,必须以字母或下划线开头。
- 变量名区分大小写。例如:Name和name是两个不同的变量。
特殊变量/全局变量
名称 | 返回 |
---|---|
blockhash(uint blockNumber) returns (bytes32) | 给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。 |
block.coinbase (address payable) | 当前区块矿工的地址 |
block.difficulty (uint) | 当前区块的难度 |
block.gaslimit (uint) | 当前区块的gaslimit |
block.number (uint) | 当前区块的number |
block.timestamp (uint) | 当前区块的时间戳,为unix纪元以来的秒 |
gasleft() returns (uint256) | 剩余 gas |
msg.data (bytes calldata) | 完成 calldata |
msg.sender (address payable) | 消息发送者 (当前 caller) |
msg.sig (bytes4) | calldata的前四个字节 (function identifier) |
msg.value (uint) | 当前消息的wei值 |
now (uint) | 当前块的时间戳 |
tx.gasprice (uint) | 交易的gas价格 |
tx.origin (address payable) | 交易的发送方 |
// 示例展示如何使用特殊变量msg,该变量在Solidity中用于获取发送者地址。
pragma solidity ^0.5.0;
contract LedgerBalance {
mapping(address => uint) public balances;
function updateBalance(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}
contract Updater {
function updateBalance() public returns (uint) {
LedgerBalance ledgerBalance = new LedgerBalance();
ledgerBalance.updateBalance(10);
return ledgerBalance.balances(address(this));
}
}
这段代码定义了两个 Solidity 合约:LedgerBalance 和 Updater。
LedgerBalance 合约定义了一个名为 balances 的公共状态变量,它是一个映射,将每个地址映射到一个无符号整数。该合约还定义了一个名为 updateBalance 的公共函数,它接受一个参数 newBalance,将调用者的地址映射到 newBalance。
Updater 合约定义了一个名为 updateBalance 的公共函数,该函数创建新的 LedgerBalance 实例,并调用 updateBalance 函数,将调用者地址映射到值 10。最后,该函数返回 address(this) 在 balances 映射中对应的值,因此相当于查询当前合约在 LedgerBalance 合约中的余额。
需要注意的是,由于 Solidity 适用于以太坊虚拟机(EVM),因此这些合约的执行和状态更改都发生在以太坊区块链上。例如,在 updateBalance 函数中, balances[msg.sender] = newBalance; 将会修改以太坊区块链上的状态,而不仅仅是本地内存变量。
变量默认值
-
bool 类型变量默认值为 false
-
int 类型变量默认值为 0
-
uint 类型变量默认值为 0
-
address 类型变量默认值为:0x000....,共 40个 0
-
bytes32 类型变量默认值为:0x00000000000000000000.....,共 64个 0
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest {
bool public a; // false
int public b; // 0
uint public c; // 0
address public d; // 0x0000000000000000000000000000000000000000
bytes32 public e; // 0x0000000000000000000000000000000000000000000000000000000000000000
}
变量作用域
- public
- 公共状态变量可以在内部访问,也可以从外部访问
- 对于公共状态变量,将自动生成一个 getter 函数。
- private
- 私有状态变量只能从当前合约内部访问,派生合约内不能访问。
- internal
- 内部状态变量只能从当前合约或其派生合约内访问。
- external
- 外部状态变量只能在合约之外调用 ,不能被合约内的其他函数调用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract C{
uint public data = 30;
uint internal iData = 20;
uint private pData = 10;
function x() public returns (uint){
data = 3; // 内部访问
iData = 2; // 内部访问
pData = 1; // 内部访问
return data;
}
}
// 调用外部合约
contract Caller{
C c = new C();
function f() public view returns(uint){
// return c.data(); // 外部访问
// return c.iData(); // error 不允许外部访问
// return c.pData(); // error 不允许外部访问
}
}
// 派生合约
contract D is C{
uint storedData; // 状态变量
function y() public returns (uint){
data = 3; // 派生合约内部访问
iData = 2; // 派生合约内部访问
// pData = 1; // error 不允许派生合约内部访问
return iData;
}
function getResult() public view returns(uint){
return storedData; // 访问状态变量
}
}
这段 Solidity 代码定义了三个合约 C、Caller 和 D。
C 合约定义了三个状态变量:data(公共)、iData(内部)和 pData(私有)。它还定义了名为 x() 的公共函数,该函数可以从内部访问所有三个状态变量并将 data 的值返回。
Caller 合约是一个外部合约,它创建了一个新的 C 实例,并定义了名为 f() 的公共函数。然而,在该函数中,尝试调用 c.data()、c.iData() 和 c.pData() 会导致编译错误,因为 iData 和 pData 是不能被外部访问的。
D 合约是一个派生合约,它继承了 C 合约所有的状态变量和函数。它定义了两个新的函数:y() 和 getResult()。函数 y() 可以从派生合约内部访问 data 和 iData 状态变量,但不能访问 pData,因为它是私有的。函数 getResult() 是一个查看函数,它可以返回 storedData 状态变量的值,该状态变量在函数中被声明,但没有被使用。
总体来说,此代码演示了 Solidity 中的状态变量的不同可见性级别。同时也展示了如何在内部、外部和派生合约中访问这些状态变量。
-
public 与 private
-
public修饰的变量和函数,任何用户或者合约都能调用和访问。
-
private修饰的变量和函数,只能在其所在的合约中调用和访问,即使是其子合约也没有权限访问。
-
-
external 与 internal
-
除 public 和 private 属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部) 和 external(外部)。
-
internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约可以访问父合约中定义的“内部”函数。
-
external 与public 类似,不过这些函数只能在合约之外调用,不能被合约内的其他函数调用。
-
常量
-
状态变量的值如果恒定不变,就可以通过
constant
进行修饰,定义为常量。-
常量的命名常常使用大写字母表示,单词之间用下划线“_”连接。
-
不是所有的类型都支持常量,当前仅支持
值类型
和字符串
。 -
constant常量
必须在编译期间通过一个表达式赋值 -
编译器并不会为
constant常量
在storage
上预留空间
-
-
常量 constant 特点
-
常量与变量相对,需要硬编码在合约中,合约部署之后,无法改变。
- 常量更加节约gas,一般用大写来代表常量。
-
运算符
算术 | 比较 | 逻辑 | 位 | 赋值 | 条件 |
+(加) 求和 例: A + B = 30 | == (等于) | && (逻辑与) 如果两个操作数都非零,则条件为真。 例: (A && B) 为真 | & (位与) 对其整数参数的每个位执行位与操作。 例: (A & B) 为 2. | =(简单赋值) 将右侧操作数的值赋给左侧操作数 例: C = A + B 表示 A + B 赋给 C | ? : (条件运算符 ) 如果条件为真 ? 则取值X : 否则值Y |
- (减) 相减 例: A - B = -10 | != (不等于) | || (逻辑或) 如果这两个操作数中有一个非零,则条件为真。 例: (A || B) 为真 | | (位或) 对其整数参数的每个位执行位或操作。 例: (A | B) 为 3. | += (相加赋值) 将右操作数添加到左操作数并将结果赋给左操作数。 例: C += A 等价于 C = C + A | |
* (乘) 相乘 例: A * B = 200 | > (大于) | ! (逻辑非) 反转操作数的逻辑状态。如果条件为真,则逻辑非操作将使其为假。 例: ! (A && B) 为假 | ^ (位异或) 对其整数参数的每个位执行位异或操作。 例: (A ^ B) 为 1. | −= (相减赋值) 从左操作数减去右操作数并将结果赋给左操作数。 例: C -= A 等价于 C = C – A | |
/ (除) 相除 例: B / A = 2 | < (小于) | ~ (位非) 一元操作符,反转操作数中的所有位。 例: (~B) 为 -4. | *= (相乘赋值) 将右操作数与左操作数相乘,并将结果赋给左操作数。 例: C *= A 等价于 C = C * A | ||
% (取模) 取模运算 例: B % A = 0 | >= (大于等于) | << (左移位)) 将第一个操作数中的所有位向左移动,移动的位置数由第二个操作数指定,新的位由0填充。将一个值向左移动一个位置相当于乘以2,移动两个位置相当于乘以4,以此类推。 例: (A << 1) 为 4. | /= (相除赋值) 将左操作数与右操作数分开,并将结果分配给左操作数。 例: C /= A 等价于 C = C / A | ||
++ (递增) 递增 例: A++ = 11 | <= (小于等于) | >> (右移位) 左操作数的值向右移动,移动位置数量由右操作数指定 例: (A >> 1) 为 1. | %= (取模赋值) 使用两个操作数取模,并将结果赋给左边的操作数。 例: C %= A 等价于 C = C % A | ||
-- (递减) 递减 例: A--= 9' |
算术运算符
+ - * / % ++ --
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
uint storedData;
constructor(){
storedData = 10;
}
function getResult() public pure returns(string memory){
uint a = 1; // 局部变量
uint b = 2;
uint result = a + b;
return integerToString(result);
}
function integerToString(uint _i) internal pure returns(string memory _uintAsString){
if(_i == 0){
return "0";
}
uint j = _i;
uint len;
while(j!=0){
len++;
j/=10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while(_i != 0){
bstr[k--] = bytes1(uint8(48 + _i % 10 ));
_i /= 10;
}
return string(bstr); // 访问局部变量
}
}
这段 Solidity 代码定义了一个名为 SolidityTest 的合约。它有一个状态变量 storedData 和一个构造函数,在构造函数中将 storedData 的初始值设置为 10。
合约还定义了两个函数:getResult() 和 integerToString()。getResult() 函数是一个查看函数,它执行两个无符号整数相加并返回结果的字符串表示形式。为了实现这一点,它声明了三个局部变量 a、b 和 result,并使用 integerToString() 函数将结果从无符号整数转换为字符串。需要注意的是,getResult() 函数被声明为 pure,因为它不会修改任何状态变量。
integerToString() 函数接受一个无符号整数 _i,并返回其字符串表示形式。在该函数中声明了两个局部变量 j 和 len,分别用于存储 _i 的副本和 _i 的位数。然后,在 while 循环中,使用整数除法操作和递减运算符来计算 _i 的长度。接下来,该函数创建了一个长度为 len 的 bytes 数组,并从右往左填充该数组的每个位置,以便生成 _i 的字符串表示形式。最后,integerToString() 函数将 bytes 数组转换为字符串并将其返回。
值得注意的是,该函数中的所有变量都是局部变量,因此它不会修改任何状态变量。
比较运算符
== != > < >= <=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
uint storedData;
constructor(){
storedData = 10;
}
function getResult() public pure returns(string memory){
uint a = 1; // 局部变量
uint b = 2;
uint result = a + b;
return integerToString(result);
}
function integerToString(uint _i) internal pure returns(string memory _uintAsString){
if(_i == 0){
// 比较运算符
return "0";
}
uint j = _i;
uint len;
while (j!=0){
// 比较运算符
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while(_i != 0){
bstr[k--] = bytes1(uint8(48 + _i % 10));
_i /= 10;
}
return string(bstr); // 访问局部变量
}
}
逻辑运算符
&& || !
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
uint storedData;
constructor(){
storedData = 10;
}
function getResult() public pure returns(string memory){
uint a = 2; // 局部变量
uint b = 2;
uint result = a & b;
return integerToString(result);
}
function integerToString(uint _i)internal pure returns(string memory){
if(_i == 0){
return "0";
}
uint j = _i;
uint len = 0;
while(j!=0){
len++;
j/=10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while(_i != 0){
bstr[k--] = bytes1(uint8(48 + _i % 10));
_i/=10;
}
return string(bstr); // 访问局部变量
}
}
位运算符
& | ^ ~ << >>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
uint storedData;
constructor(){
storedData = 10;
}
function getResult() public pure returns(string memory){
uint a = 2; // 局部变量
uint b = 2;
uint result = a & b; // 位与
return integerToString(result);
}
function integerToString(uint _i)internal pure returns (string memory){
if(_i == 0){
return "0";
}
uint j = _i;
uint len;
while ( j!=0){
len++;
j/=10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while(_i != 0){
bstr[k--] = bytes1(uint8(48+_i%10));
_i /= 10;
}
return string(bstr); // 访问局部变量
}
}
storedData 是一个无符号整数类型的状态变量,用于存储数据。默认初始值为 10。
constructor 是合约的构造函数,用于在创建合约时初始化 storedData 值为 10。
getResult() 函数是一个公共函数,其返回值类型为 string memory。它定义了两个无符号整数类型的局部变量 a 和 b,并使用位运算符 & 来计算它们的与结果。然后,它将此结果转换为字符串并返回。
integerToString() 函数是一个内部函数,其输入参数 _i 是一个无符号整数类型。它将整数 _i 转换为字符串类型并返回。此函数首先检查传入的整数是否为 0。如果是,则直接返回字符串“0”。否则,它使用循环来计算此整数的位数,并创建一个长度为 len 的字节数组来存储相应的字符。最后它通过将字节数组转换为字符串来返回结果。
赋值运算符
= += -= *= /= %=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest {
uint storedData;
constructor() {
storedData = 10;
}
function getResult() public pure returns(string memory){
uint a = 1;
uint b = 2;
uint result = a + b;
return integerToString(result);
}
function integerToString(uint _i) internal pure
returns (string memory) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10; // 赋值运算
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while (_i != 0) {
bstr[k--] = bytes1(uint8(48 + _i % 10));
_i /= 10;// 赋值运算
}
return string(bstr); // 访问局部变量
}
}
storedData 是一个无符号整数类型的状态变量,用于存储数据。默认初始值为 10。
constructor 是合约的构造函数,用于在创建合约时初始化 storedData 值为 10。
getResult() 函数是一个公共函数,其返回值类型为 string memory。它定义了两个无符号整数类型的局部变量 a 和 b,并使用加法运算符 + 来计算它们的和。然后,它将此结果转换为字符串并返回。
integerToString() 函数是一个内部函数,其输入参数 _i 是一个无符号整数类型。它将整数 _i 转换为字符串类型并返回。此函数首先检查传入的整数是否为 0。如果是,则直接返回字符串“0”。否则,它使用循环来计算此整数的位数,并创建一个长度为 len 的字节数组来存储相应的字符。最后它通过将字节数组转换为字符串来返回结果。
条件运算符
?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
uint storedData;
constructor(){
storedData = 10;
}
function getResult() public pure returns(string memory){
uint a = 1;
uint b = 2;
uint result = (a>b?a:b);
return integerToString(result);
}
function integerToString(uint _i)internal pure returns(string memory){
if(_i == 0){
return "0";
}
uint j = _i;
uint len;
while(j!=0){
len++;
j/=10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while(_i != 0){
bstr[k--] = byte(uint8(48 + _i % 10));
_i /= 10;
}
return string(bstr);
}
}
storedData 是一个无符号整数类型的状态变量,用于存储数据。默认初始值为 10。
constructor 是合约的构造函数,用于在创建合约时初始化 storedData 值为 10。
getResult() 函数是一个公共函数,其返回值类型为 string memory。它定义了两个无符号整数类型的局部变量 a 和 b,并使用三元运算符 ? : 来计算它们的最大值。然后,它将此结果转换为字符串并返回。
integerToString() 函数是一个内部函数,其输入参数 _i 是一个无符号整数类型。它将整数 _i 转换为字符串类型并返回。此函数首先检查传入的整数是否为 0。如果是,则直接返回字符串“0”。否则,它使用循环来计算此整数的位数,并创建一个长度为 len 的字节数组来存储相应的字符。最后它通过将字节数组转换为字符串来返回结果。
条件语句
- if
if (条件表达式) {
被执行语句(如果条件为真)
}
- if...else
if (条件表达式) {
被执行语句(如果条件为真)
} else {
被执行语句(如果条件为假)
}
- if...else if
if (条件表达式 1) {
被执行语句(如果条件 1 为真)
} else if (条件表达式 2) {
被执行语句(如果条件 2 为真)
} else if (条件表达式 3) {
被执行语句(如果条件 3 为真)
} else {
被执行语句(如果所有条件为假)
}
循环
- while
while (表达式) {
// 如果表达式的结果为真,就循环执行以下语句
......
}
- do ... while
do {
// 如果表达式的结果为真,就循环执行以下语句
......
} while (表达式);
- for
for (初始化; 测试条件; 迭代语句) {
// 如果表达式的结果为真,就循环执行以下语句
......
}
- 循环控制语句:break、continue
- continue – 跳出本次循环,继续执行下一次循环
- break – 跳出循环,不再执行当前循环
字符串
- 字符串值使用双引号(")和单引号(')包括,字符串类型用 string 表示。
- 字符串是特殊的数组,是引用类型。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
string public data1 = "test1";
bytes public data2 = "test2";
}
/*在上面的例子中,"test" 是一个字符串,data 是一个字符串变量。
Solidity 提供字节与字符串之间的内置转换,可以将字符串赋给 bytes 类型变量。*/
转义字符
序号 | 转义字符 | 序号 | 转义字符 |
1 | \n 开始新的一行 | 7 | \r 回车 |
2 | \\ 反斜杠 | 8 | \t 制表符 |
3 | \' 单引号 | 9 | \v 垂直制表符 |
4 | \" 双引号 | 10 | \xNN 表示十六进制值并插入适当的字节 |
5 | \b 退格 | 11 | \uNNNN 表示Unicode值并插入UTF-8序列 |
6 | \f 换页 |
bytes 到字符串的转换
- 可以使用 string() 构造函数将 bytes 转换为字符串
bytes memory bstr = new bytes(10);
string message = string(bstr);
- bytes 到字符串的转换
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest {
string public data;
bytes bstr = new bytes(2);
function trans() external{
bstr[0] = 'a';
bstr[1] = 'b';
data = string(bstr);
}
}
字符串到 bytes 的转换
- 可以使用bytes()构造函数将字符串转换为bytes
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest {
constructor(){
}
function getResult(string memory s) public pure returns(bytes memory){
return bytes(s); // 字符串转换到bytes
}
}
数组
声明数组
- 固定长度数组
- 声明一个固定长度的数组,需要指定元素类型和数量
- arraySize 必须是一个大于零的整数数字,type 可以是任何数据类型
type arrayName [arraySize];
// 声明一个 uint 类型,长度为10的数组:balance,如下所示:
uint balance[10];
- 动态数组
- 声明一个动态数组,只需要指定元素类型,无需指定数量
- arraySize 必须是一个大于零的整数数字,type 可以是任何数据类型
type arrayName [];
// 声明一个 uint 类型的动态数组:balance,如下所示:
uint balance[];
初始化数组
uint balance[3] = [1,2,3]; // 初始化固定长度数组
uint balance[] = [1,2,3]; // 初始化动态数组
balance[2] = 5; // 设置第3个元素的值为5
访问数组元素
// 可以通过索引访问数组元素
uint salary = balance[2];
动态数组和固定长度数据的区别
- 固定长度的数组在运行期间,是无法改变长度的。而动态数组可以改变
- 动态数组使用 push 方法,在末尾追加一个元素
- 使用 pop 方法,截掉末尾元素
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
// 动态数组
uint[] public arr = [1,2,3,4];
// 固定长度数组
uint[4] public arrFixed=[1,2,3,4];
// 获得数组全部元素
function getArr() external view returns(uint[] memory){
return arr;
}
// 固定长度数组操作
function operatFixedArr() external{
// 获得数组长度
uint len1 = arrFixed.length; // len=4
// 获得第二个元素
uint x = arrFixed[1]; // x=2
// 删除第二个元素,delete只将元素设置为初值,并不改变数组长度
delete arrFixed[1]; // [1,0,3,4]
}
// 动态数组操作
function operateDynamicArr() external{
// 获得数组长度
uint len2 = arr.length; // len=4
// 追加一个元素
arr.push(5); // [1,2,3,4,5]
// 弹出一个元素
arr.pop(); // [1,2,3,4]
// 获得第二个元素
uint x = arr[1]; // x=2
// 删除第二个元素,delete 只将元素设置为初值,并不改变数组长度
delete arr[1]; // [1,0,3,4]
// 删除第二个元素,并且长度减1
removeAt(1); // [1,3,4]
}
// 删除数组元素,并且长度减1
function removeAt(uint i) public {
require(i>=0&&i<arr.length);
for(uint k=i;k<arr.length-1;k++){
arr[k] = arr[k+1];
}
arr.pop();
}
}
/*getArr:返回动态数组 arr 中的所有元素。
operatFixedArr:演示如何使用固定长度数组 arrFixed,获取数组长度、访问特定元素以及删除特定元素。
operateDynamicArr:演示如何使用动态数组 arr,获取数组长度、追加/弹出元素、访问特定元素以及删除特定元素。
removeAt:删除指定索引位置的元素,并将数组长度减1,其中使用了 for 循环和 pop() 函数。*/
创建内存数组
使用 new 关键字在内存中创建动态数组。内存动态数组的长度一旦确定,不能改变。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
function getMemoryArr() external pure returns(uint[] memory){
uint[] memory arr = new uint[](3);
arr[0] = 1;
return arr;
}
}
结构体
定义结构体
struct struct_name{
type1 type_name_1;
type2 type_name_2;
type3 type_name_3;
}
// 示例
struct Book{
string title;
string author;
uint book_id;
}
访问结构体成员
// 要访问结构体的任何成员,要使用成员访问操作符(.)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
struct Book{
string title;
string author;
uint id;
}
Book book;
function setBook() public {
book = Book('learn','codebaoku.com',1);
}
function getBookAuthor() public view returns(string memory){
return book.author;
}
}
结构体操作方法
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
struct Book{
string title;
string author;
uint id;
address owner;
}
Book public book;
Book[] public books;
mapping(address=>Book[])public booksByOwner;
function operations() external{
// 结构体直接按照字段顺序,进行初始化
Book memory book1 = Book('learn Java','codebaoku.com',1,msg.sender);
// 结构体按照字段名,进行初始化
Book memory book2 = Book({title:'learn',author:'codebaoku.com',id:2,owner:msg.sender});
// 结构体按照默认值,进行初始化
Book memory book3;
book3.id = 3;
book3.title = 'learn';
book3.author = '11';
book3.owner = msg.sender;
// 结构体数组操作
books.push(book1);
books.push(book2);
books.push(book3);
// 结构体状态变量操作
Book storage _book = books[0];
delete _book.id;
delete books[0];
_book.id = 100;
}
}
映射
- mapping 用于以键值对的形式存储数据,等同于其它编程语言的哈希表或字典
- _KeyType:可以是任何内置类型,或者types和字符串,不允许使用引用类型或复杂对象
- _ValueType:可以是任何类型
mapping(_KeyType => _ValueType)
- 注意
- 映射的数据位置(data location)只能是 storage,通常用于状态变量
- 映射可以标记为 public,Solidity 自动为它创建 getter
- 映射可以视为哈希表,在实际的初始化过程中创建每个可能的 key,并将其映射到字节形式全是零的值:一个类型的 默认值
- 映射和哈希表不同:在映射中,实际并不存储 key,而是存储它的 keccak256哈希值,从而便于查询实际的值
- 正因为如此,映射是没有长度的,也没有 Key 的集合或 value 的集合的概念。映射只能是存储的数据位置,因此只存在作为状态变量或作为函数内的存储引用 或 作为库函数的参数。它们不能用于合约共有函数的参数或返回值。
- 可以将映射声明为 public,然后来让 Solidity 创建一个 getter 函数。_KeyType 将成为 getter 的必须参数,并且 getter 会返回 _ValueType
- 状态变量示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LederBalance{
mapping(address => uint)public balances;
function updateBalance(uint newBalance) public {
// 设置 mapping 的 key 和 value
balances[msg.sender] = newBalance;
}
function get() public view returns(uint){
// 通过 key 获取 mapping 的 value
return balances[msg.sender];
}
}
contract Updater{
function updateBalance() public returns(uint){
LederBalance ledgerBalance = new LederBalance();
ledgerBalance.updateBalance(10);
return ledgerBalance.get();
}
}
- 局部变量示例
- mapping 类型可以用做局部变量,但只能引用状态变量,而且存储位置为 storeage
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
struct Book{
string title;
string author;
uint id;
address owner;
}
Book public book;
Book[] public books;
mapping(address=>Book[])public booksByOwner;
function operations() external{
// 结构体直接按照字段顺序,进行初始化
Book memory book1 = Book('learn Java','codebaoku.com',1,msg.sender);
// 结构体按照字段名,进行初始化
Book memory book2 = Book({title:'learn',author:'codebaoku.com',id:2,owner:msg.sender});
// 结构体按照默认值,进行初始化
Book memory book3;
book3.id = 3;
book3.title = 'learn';
book3.author = '11';
book3.owner = msg.sender;
// 结构体数组操作
books.push(book1);
books.push(book2);
books.push(book3);
// 结构体状态变量操作
Book storage _book = books[0];
delete _book.id;
delete books[0];
_book.id = 100;
}
}
枚举
- enum 是一种用户自定义类型,用于表示多种状态
- enum 内部是一个自定义的整型,默认的类型为 uint8,当枚举数足够多时,自动变成 uint16
- enum至少应该有一名成员
- enum 可以与整数进行显式转换,但不能进行隐式转换。
- 显示转换会在运行时检查数值范围,如果不匹配,将会引起异常
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
enum Status{
None,
Pending,
Completed,
Canceled
}
Status public status;
// 获取状态
function getStatus() external view returns(Status){
return status;
}
// 设置状态
function setStatus(Status _status) external{
status = _status;
}
//设置完成状态
function setCompleted() external{
status = Status.Completed;
}
}
类型转换
隐式转换
- 隐式转换时必须符合一定条件,不能导致信息丢失。
- uint8 可以转换为 uint16
- 但是 int8 不可以转换为 uint256,因为 int8 可以包含 uint256中不允许的负值
显式转换
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SolidityTest{
function test1() public pure returns(int){
int8 y = -3;
// uint x = uint(y); // int8不可以转换为uint256
// 转换成更小的类型,会丢失高位
uint32 a = 0x12345678;
uint16 b = uint16(a); // b = 0x5678
// 转换为更大的类型,将会向左侧添加填充位
uint16 a1 = 0x1234;
uint32 b1 = uint32(a1); // b1 = 0x00001234
// 转换到更小的字节类型,会丢失后面数据
bytes2 a2 = 0x1234;
bytes1 b2 = bytes1(a2); // b2 = 0x12
// 转换为更大的字节类型时,向右添加填充位
bytes2 a3 = 0x1234;
bytes4 b3 = bytes4(a3); // b3 = 0x12340000
// 只有当字节类型和 int 类型大小相同时,才可与进行类型转换
bytes2 a4 = 0x1234;
uint32 b4 = uint16(a4); // b4 = 0x00001234
uint32 c = uint32(bytes4(a4)); // c = 0x12340000
uint8 d = uint8(uint16(a4)); // d = 0x34
uint8 e = uint8(bytes1(a4)); // e = 0x12
// 把整数赋值给整型时,不能超出范围而发生截断,否则会报错
uint8 a5 = 12; // no error
uint32 b5 = 1234; // no error
// uint16 c5 = 0x123456; // error,有截断,变成0x3456
return 0;
}
}
数据位置
引用类型 / 复合数据类型
- 有一些数据类型由简单数据类型组合而成,通过名称引用,这些类型通常被称为引用类型
- 数组(字符串与 bytes 是特殊的数组,也是引用类型)
- struct (结构体)
- map (映射)
- 这些类型涉及到的数据量较大,复制它们可能要消耗大量Gas,非常昂贵,所以使用它们时,必须考虑存储位置。例如,是保存在内存中,还是在 EVM 存储区中。
数据位置(data location)
- 在合约中声明和使用的变量都有一个数据位置,指明变量值应该存储在哪里
- 合约变量的数据位置将会影响 Gas 消耗量
- Solidity 提供 3 种类型的数据位置
- storage
- memory
- calldata
- storage
- 存储位置 storage 用来存储永久数据,可以被合约中的所有函数访问
- storage 变量,通常用于存储智能合约的状态变量,它在函数调用之间保持持久性
- storage 与其他数据位置相比,成本较高
- 可以理解为计算机的硬盘数据,所有数据都永久存储
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SolidityTest{ struct MyData{ uint id; string value; } MyData[] public myData; constructor(){ myData.push(MyData(1,"value1")); myData.push(MyData(2,"value2")); } function operations() external{ // storage 存储位置 MyData storage d = myData[0]; // 修改状态变量 myData 的内容 d.value = "new value"; } } /*合约 SoldityTest 部署后,调用 operations 方法,状态变量 myData 的数据被修改,其中 myData[0].value 变为 "new value"。*/
- memory
- 存储位置 memory 用来存储临时数据,比 storage 便宜
- memory 变量,通常用于保存临时变量,以便在函数执行期间进行计算
- 一旦函数执行完毕,它的内容就会被丢弃,它只能在所在的函数中访问
- 可以理解为每个单独函数的内存 RAM
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SolidityTest{ struct MyData{ uint id; string value; } MyData[] public myData; constructor(){ myData.push(MyData(1,"value1")); myData.push(MyData(2,"value2")); } function operations() external{ // memory 存储位置 MyData memory d = myData[0]; // 修改状态变量 myData 的内容 d.value = "new value"; } } /*合约 SoldityTest 部署后,调用 operations 方法,状态变量 myData 的数据未被修改,其中 myData[0].value 仍然为 "value1"*/
- calldata
- 是不可修改的非持久性数据位置,类似memory,但只能出现在函数的输入参数位置
- 外部函数 external function 的传入参数强制为 calldata 类型
- 使用 calldata 的好处是在内部函数中作为输入参数传递时,省掉了数据拷贝的成本
- 节省了 gas 费用,memory 作为参数时,需要进行数据拷贝
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SolidityTest{ function operations(string calldata val)pure external{ // 使用 calldata 传递参数,省去复制成本 internalFunc1(val); // 使用 memory 传递参数,需要进行复制 internalFunc2(val); } function internalFunc1(string calldata val) internal pure{ //... } function internalFunc2(string memory val) internal pure{ //... } }
变量数据位置
- 规则1 - 状态变量
- 状态变量总是存储在存储区中
- 此外,不能显式地标记状态变量的位置
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract DataLocation{ // 状态变量总是存储在存储区中。 // storage uint stateVariable; uint[] stateArray; // 此外,不能显式地标记状态变量的位置 uint storage stateVariable; // 错误 uint[] memory stateArray; // 错误 }
- 规则2 - 函数参数与返回值
- 函数参数包括返回参数都存储在内存中
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract DataLocation{ // storage uint stateVariable; uint[] stateArray; // 函数参数 uint num1 与 uint num2,返回值 uint result 都存储在内存中。 function calculate(uint num1,uint num2) public pure returns(uint result){ return num1 + num2; } }
- 函数参数包括返回参数都存储在内存中
- 规则3 - 局部变量
- 值类型的局部变量存储在内存中。
- 对于引用类型的局部变量,需要显式地指定数据位置
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Locations{ /* 此处都是状态变量 */ // 存储在storage(存储区)中 bool flag; uint number; address account; function doSomething() public pure{ /* 此处都是局部变量 */ // 值类型 // 所以它们被存储在memory(内存)中 bool flag2; uint number2; address account2; // 引用类型,需要显示指定数据位置,此处指定为内存 uint[] memory localArray; } }
-
不能显式覆盖具有值类型的局部变量
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; function doSomething() public { /* 此处都是局部变量 */ // 值类型 bool memory flag2; // 错误 uint storage number2; // 错误 address account2; }
-
规则4 - 外部函数的参数
-
外部函数的参数(不包括返回参数)存储在 calldata 中
-
赋值数据位置
-
solidity 数据可以通过两种方式从一个变量复制到另一个变量。
-
一种方法是复制整个数据(按值复制)
-
另一种方法是引用复制。
-
-
规则1 - 状态变量赋值给状态变量
- 将一个状态变量赋值给另一个状态变量,将创建一个新的副本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Locations{ uint public stateVar1 = 10; uint public stateVar2 = 20; function doo() public returns(uint){ stateVar1 = stateVar2; stateVar2 = 30; return stateVar1; // returns 20 /* 本例中,stateVar1 和 stateVar2 是状态变量。 在 do 函数中,我们将 stateVar2 的值赋值给 stateVar1。 stateVar1 的值是 20,但是为了确定它创建了一个新的副本,我们改变了stateVar2 的值。 因此,如果它不创建副本,那么 stateVar1 的值应该是30,创建副本则是20。 这同样适用于引用类型的状态变量。 */ } }
- 将一个状态变量赋值给另一个状态变量,将创建一个新的副本
-
规则2 - 内存局部变量复制到状态变量
-
从内存局部变量复制到状态变量,总是会创建一个新的副本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Locations{ uint stateVar = 10; // storage function doone() public returns(uint){ uint localVar = 20; // memory stateVar = localVar; localVar = 40; return stateVar; // returns 20 /* 在上面的例子中,我们有一个状态变量和一个局部内存变量。 在函数中,我们把局部变量的值赋给状态变量。 为了检查赋值后的行为,我们改变了局部变量的值,并返回状态变量的值。 这里可以看到,它会返回20,说明从内存局部变量复制到状态变量,会创建一个新的副本。*/ } }
-
-
规则3 - 状态变量复制到内存局部变量
-
从状态变量复制到内存局部变量,将创建一个副本。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Locations{ uint stateVar = 10; // storage function dothree() public returns(uint){ uint localVar = 20; // memory localVar = stateVar; stateVar = 40; return localVar; // returns 10 /* 在上面的例子中,我们有一个状态变量和一个局部内存变量。在函数中,我们把局部变量的值赋给状态变量。 为了检查赋值后的行为,我们改变了局部变量的值,并返回状态变量的值。 这里可以看到,它会返回20,说明从内存局部变量复制到状态变量,会创建一个新的副本。 */ } }
-
-
规则4 - 内存变量复制到内存变量
-
对于引用类型的局部变量,从一个内存变量复制到另一个内存变量,不会创建副本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Locations{ function doSomething() public view returns(uint[] memory,uint[] memory){ uint[] memory localMemoryArray1 = new uint[](3); localMemoryArray1[0] = 4; localMemoryArray1[1] = 5; localMemoryArray1[2] = 6; uint[] memory localMemoryArray2 = localMemoryArray1; localMemoryArray1[0] = 10; return(localMemoryArray1,localMemoryArray2); // returns 10,4,6 | 10,4,6 /* 在上面的示例中,我们在内存中初始化了一个名为localMemoryArray1的数组变量,并赋值为4、5和6。 然后,我们将该变量复制到名为localMemoryArray2的新内存变量中。 然后,我们修改了localMemoryArray1中第一个索引的值,并返回了两个数组。 这将得到相同的结果,因为它们都指向相同的位置。 */ } }
-
对于值类型的局部变量,从一个内存变量复制到另一个内存变量,仍然创建一个新副本
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Locations{ function dofour() public pure returns(uint){ uint localVar1 = 10; // memory uint localVar2 = 20; // memory localVar1 = localVar2; localVar2 = 40; return localVar1; // returns 20 } }
-