Solidity基础学习

Solidity基础

Solidity基础数据类型

Solidity中,变量类型有以下几大类:

  • 值类型
  • 地址类型
  • 引用类型

1. 值类型

字节型bytes8 to bytes32 8位到32位的字节型数据。

类型 保留字 取值
布尔型 bool true/false
整型 int/uint 有符号整数/无符号整数。
整型 int8 to int256 8位到256位的带符号整型数。int256与int相同。
整型 uint8 to uint256 8位到256位的无符号整型。uint256和uint是一样的。
定长浮点型 fixed/unfixed 有符号和无符号的定长浮点型
定长浮点型 fixedMxN 带符号的定长浮点型,其中M表示按类型取的位数,N表示小数点。M应该能被8整除,从8到256。N可以是0到80。fixed与fixed128x18相同。
定长浮点型 ufixedMxN 无符号的定长浮点型,其中M表示按类型取的位数,N表示小数点。M应该能被8整除,从8到256。N可以是0到80。fixed与fixed128x18相同。

值类型数据:

1
2
3
uint x = 100;
int x = -200;
byte32 b = 0x01020304

我们可以测量类型的最大值和最小值。

1
2
3
4
5
6
7
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

contract Test {
uint public a = type(uint).min;
uint public b = type(uint).max;
}

2. 地址类型

地址类型表示以太坊地址,长度为20字节。地址可以使用 .balance 属性获得余额,也可以使用 .transfer() 方法将余额转到另一个地址。

1
2
3
4
5
address x = 0x212;
address myAddress = this;

if (x.balance < 10 && myAddress.balance >= 10)
x.transfer(10);

3. 引用类型/复合数据类型

Solidity中,有一些数据类型由值类型组合而成,相比于简单的值类型,这些类型通常通过名称引用,被称为引用类型。

引用类型包括:

  • 数组 (字符串与bytes是特殊的数组,所以也是引用类型)
  • struct (结构体)
  • map (映射)

Solidity复杂数据类型

字符串string

Solidity 中,字符串值使用双引号(“)和单引号(‘)包括,字符串类型用 string 表示。

字符串是特殊的数组,是引用类型。

1
2
3
4
5
6
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SolidityTest {
string public data = "test";
}

Solidity 提供字节与字符串之间的内置转换,可以将字符串赋给 bytes 类型变量。

bytes到字符串的转换

可以使用 string() 构造函数将 bytes 转换为字符串。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
// 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);
}
}

运行上述程序,调用trans后,查看data:

1
0: string: ab

字符串到bytes的转换

可以使用bytes()构造函数将字符串转换为bytes。

1
2
3
4
5
6
7
8
9
10
11
// 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
}
}

数组

重点:

  • 在solidity中,数组大小可以是固定大小的,也可以是动态长度的。
  • 对于 storage 数组,元素可以是任意类型(其他数组、映射或结构)。
  • 对于 memory 数组,元素类型不能是映射类型,如果它是一个 public 函数的参数,那么元素类型必须是 ABI 类型。
  • 类型为 bytes 和 string 的变量是特殊数组。bytes 类似于 byte[],但它在 calldata 中被紧密地打包。因此,相比于 byte[],bytes 应该优先使用,因为更便宜
  • 字符串 string 等价于 bytes,但不允许长度或索引访问。

1.声明数组

固定长度组

声明一个固定长度的数组,需要指定元素类型和数量

uint balance[10];

动态数组

声明一个动态数组,只需要指定元素类型,无需指定数量

uint balance[];

2. 初始化数组

初始化数组,可以使用下面的语句:

1
2
3
uint balance[3] = [1, 2, 3]; // 初始化固定长度数组
uint balance[] = [1, 2, 3]; // 初始化动态数组
balance[2] = 5; // 设置第 3 个元素的值为 5

3.动态数组和固定长度数据的区别

  • 固定长度的数组在运行期间,是无法改变长度的。而动态数组可以改变。

  • 动态数组可以使用 push 方法,在末尾追加一个元素;使用 pop 方法,截掉末尾元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 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 len = arrFixed.length; // len = 4
// 获得第二个元素
//uint x = arrFixed[1]; // x = 2
// 删除第二个元素,delete只将元素设置为初值,并不改变数组长度
delete arrFixed[1]; // [1,0,3,4]
}

// 动态数组操作
function operateDynamicArr() external {
// 获得数组长度
//uint len = 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();
}
}

4.创建内存数组

我们可以使用 new 关键字在内存中创建动态数组。内存动态数组的长度一旦确定,不能改变。

1
2
3
4
5
6
7
8
9
10
// 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;
}
}

结构体

类似于C语言,solidity 也有结构体 struc 类型,用于表示复合型数据。

1.定义结构体

要定义结构,使用struct关键字。struct关键字定义了一个新的数据类型,包含多个成员。struct语句的格式如下

1
2
3
4
5
struct Book { 
string title;
string author;
uint book_id;
}

2. 访问结构体成员

要访问结构的任何成员,使用成员访问操作符(.)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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 Java', 'codebaoku.com', 1);
}
function getBookAuthor() public view returns (string memory) {
return book.author;
}
}

3.结构体操作方法

结构体变量共有三种初始化方式。我们还可以定义结构体数组、结构体状态变量,并对其进行操作。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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 JS', author:'codebaoku.com', id:2, owner:msg.sender});

// 结构体按照默认值,进行初始化
Book memory book3;
book3.id = 3;
book3.title = 'Learn C';
book3.author = 'codebaoku.com';
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

Solidity 映射 mapping 用于以键值对的形式存储数据,等同于其它编程语言的哈希表或字典。

映射 mapping 是智能合约中很常用的一种数据类型,它是引用类型

下面是声明映射类型的语法。

1
mapping(_KeyType => _ValueType)
  • _KeyType:可以是任何内置类型,或者 bytes 和 字符串。不允许使用引用类型或复杂对象。
  • _ValueType: 可以是任何类型。

Solidity变量

Solidity 支持三种类型的变量:

  • 状态变量 – 变量值永久保存在智能合约存储空间中的变量。
  • 局部变量 – 变量值仅在函数执行过程中有效的变量,函数退出后,变量无效。
  • 全局变量 – 保存在全局命名空间,用于获取整个区块链相关信息的特殊变量,与当前合约无关,比如:时间戳、块高等。

Solidity 是一种静态类型语言,这意味着需要在声明期间指定变量类型。

每个变量声明时,都有一个基于其类型的默认值。没有 undefined 或 null 的概念。比如 int 类型的默认值为 0。

1. 状态变量

状态变量是变量值永久保存在合约存储空间中的变量。状态变量的定义形式类似于类中的成员变量。

1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SolidityTest {
uint storedData; // 状态变量
constructor() {
storedData = 10; // 使用状态变量
}
}

2. 局部变量

局部变量是变量值仅在函数执行过程中有效的变量,函数退出后,变量无效。函数参数也是局部变量。

局部变量不会上链,只存在于所处函数的生命周期。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SolidityTest {
function sum() public view returns(uint){
uint a = 1; // 局部变量
uint b = 2;
uint result = a + b;
return result; // 访问局部变量
}
}

运行上述程序,输出:

1
0: uint256: 3

3. 全局变量

全局变量是指全局工作区中存在的特殊变量,提供有关区块链和交易属性的信息。

1
2
3
4
5
6
7
8
9
// 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);
}
}
名称 返回
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) 交易的发送方

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.5.0

contract LedgerBanlence {
mapping(address => uint256) public banlances;

function updateBanlance(uint newBanlance) public {
balances[msg.sender] = newBanlance;
}
}

contract Updater {
function updateBanlance() public returns(uint){
ledgerBalance ladgerBanlance = new legerBanlance();
ledgerBanlance.updateBalance(10);
return ledgerBanlance.banlances(address(this));
}
}

4. Solidity 变量命名规则

在为变量命名时,请记住以下规则:

  • 不应使用 Solidity 保留关键字作为变量名。例如:break 或 boolean 变量名无效。
  • 不应以数字(0-9)开头,必须以字母或下划线开头。例如:123test 是一个无效的变量名,但是 _123test是一个有效的变量名。
  • 变量名区分大小写。例如:Name和name是两个不同的变量。

5.变量默认值

Solidity 智能合约中所有的状态变量和局部变量,都有默认值。

这些变量在没有被赋值之前,它的值已默认值的形式存在。

其中:

bool 类型变量默认值为 false;

int 类型变量默认值为 0;

uint 类型变量默认值为 0;

address 类型变量默认值为:0x0000000000000000000000000000000000000000,共 40个 0;

bytes32 类型变量默认值为:0x0000000000000000000000000000000000000000000000000000000000000000,共 64个 0。

6.变量的4个作用域

局部变量的作用域仅限于定义它们的函数,但是状态变量可以有四种作用域类型:

  • public – 公共状态变量可以在内部访问,也可以从外部访问。对于公共状态变量,将自动生成一个 getter 函数。
  • private – 私有状态变量只能从当前合约内部访问,派生合约内不能访问。
  • internal – 内部状态变量只能从当前合约或其派生合约内访问。
  • external - 外部状态变量只能在合约之外调用 ,不能被合约内的其他函数调用

image-20230113100019755

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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; // 访问状态变量
}
}

7.常量(constant)

智能合约中,状态变量的值如果恒定不变,就可以通过 constant 进行修饰,定义为常量。

常量的命名常常使用大写字母表示,单词之间用下划线“_”连接。

常量有如下规定:

  • 不是所有的类型都支持常量,当前仅支持值类型字符串
  • constant常量必须在编译期间通过一个表达式赋值
  • 编译器并不会为constant常量storage上预留空间
1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SolidityTest {
uint public constant X = 32**22 + 8;
string public constant TEXT = "abc";
bytes32 public constant MY_HASH = keccak256("abc");
}

常量constant的特点

  • 常量与变量相对,需要硬编码在合约中,合约部署之后,无法改变。
  • 常量更加节约gas,一般用大写来代表常量。

Solidity函数

函数基础构成

  1. 函数的定义

    Solidity中, 定义函数的语法如下:

    1
    2
    3
    4
    5
    function function_name(<parameter list>) <visibility> <state mutability> [returns(<return type>)] {
    //语句
    }

    //函数由关键字function声明,后面跟函数名、参数、状态可变性、可见性、返回值的定义。

    可见性(visibility)

    • Private(私有):函数只能在所定义的智能合约内部调用。
    • Internal(内部):可以在所定义智能合约内部调用该函数,也可以从继承合约中调用该函数。
    • External(外部):只能从智能合约外部调用。 如果要从智能合约中调用它,则必须使用 this。
    • Public(公开):可以从任何地方调用。

    状态可变性 mutability

    • view:用view声明的函数只能读取状态,而不能修改状态。
    • pure:用pure声明的函数既不能读取也不能修改状态。
    • payable:用payable声明的函数可以接受发送给合约的以太币,如果未指定,该函数将自动拒绝所有发送给它的以太币

return 语句

Solidity中, 函数可以返回多个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Test {
function getResult() public view returns(uint product, uint sum){
uint a = 1; // 局部变量
uint b = 2;
product = a * b; // 使用返回参数返回值
sum = a + b; // 使用返回参数返回值

// 也可以使用return返回多个值
// return(a*b, a+b);
}
}

函数返回值

Solidity 函数的返回值可以使用名字,也可以采用匿名方式

Solidity 函数的返回值可以通过名字赋值,也可以使用 return 返回。

Solidity 函数支持多个返回值。

特殊函数

Solidity pure 函数

solidity pure函数,也就是纯函数,是指函数不会读取或修改状态。

换言之,solidity pure函数不会操作链上数据。

如果函数中存在以下语句,则被视为读取状态,编译器将抛出警告。

  • 读取状态变量。
  • 访问 address(this).balance<address>.balance
  • 访问任何区块、交易、msg等特殊变量(msg.sig 与 msg.data 允许读取)。
  • 调用任何不是纯函数的函数。
  • 使用包含特定操作码的内联程序集。

如果发生错误,pure函数可以使用revert()require()函数来还原潜在的状态更改。

声明为pure函数,可以在函数声明里,添加pure关键字。

示例1

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Test {
function add(uint a, uint b) public pure returns(uint){
return a + b;
}
}

运行上述程序,输入参数 2 和 1,输出:

1
0: uint256: product 3

Solidity view 函数

solidity view函数,也就是视图函数,是指函数只会读取状态,不会修改状态。

换言之,solidity view函数只会读取链上数据,不会修改链上数据。

如果函数中存在以下语句,则被视为修改状态,编译器将抛出警告。

  • 修改状态变量。
  • 触发事件。
  • 创建合约。
  • 使用selfdestruct
  • 发送以太。
  • 调用任何不是视图函数或纯函数的函数
  • 使用底层调用
  • 使用包含某些操作码的内联程序集。

状态变量的Getter方法默认是view函数。

声明为view图函数,可以在函数声明里,添加view关键字。

示例

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

contract ViewDemo {
uint factor = 2; // 状态变量

function getResult(uint num) public view returns(uint){
//只对链上数据factor进行了读取,并没有修改
uint result = num * factor;
return result;
}
}

运行上述程序,输入100,输出:

1
0: uint256: 200

在这个例子中,函数getResult中,只是读取了状态变量factor的值,而没有修改它的值,所以这个函数是一个view函数。

Solidity构造函数

Solidity构造函数是一个特殊函数,它仅能在智能合约部署的时候调用一次,之后就不能再次被调用。

Solidity构造函数常用来进行状态变量的初始化工作。

Solidity编译器中,使用关键词 constructor 作为构造函数。

示例1

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Test {
uint a;

// 不带参数的构造函数
constructor() {
a = 0;
}
}

示例2

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Test {
uint a;

// 带参数的构造函数
constructor(uint _a) {
a = _a;
}
}

Solidity modifier函数修改器

Solidity 中关键字 modifier 用于声明一个函数修改器。

我们可以将一些通用的操作提取出来,包装为函数修改器,来提高代码的复用性,改善编码效率

函数修改器 modifier 的作用与 Java Spring 中的切面功能很相似,当它作用于一个函数上,可以在函数执行前或后预先执行 modifier 中的逻辑,以增强其功能。

函数修改器 modifier 常用于在函数执行前检查某种前置条件。

函数修改器 modifier 是一种合约属性,可被继承,同时还可被派生的合约重写(override)。

  1. 基本函数修改器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ModifierTest {
bool public paused;
uint public counter;

function setPaused(bool _paused) external {
paused = _paused;
}

modifier isNotPaused() {
require(!paused); // 检查前置条件:判断paused是否被设置,如果paused为true,那么终止执行。
_; // 执行被isNotPaused修饰的函数
}

function add() external isNotPaused{
counter++;
}
}

add函数被修改器isNotPaused修饰,所以先执行 require(!paused),检查前置条件,然后再执行add函数的代码。

  1. _的作用

函数修改器中有一行代码只有下划线 _ ,我们认为下划线 _ 代表了被修饰函数的代码。

也就是说,下划线实际上帮我们标记了被 modifier 修饰函数的执行位置

  1. 带参数的函数modifier
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ModifierTest {
bool public paused;
uint public counter;

function setPaused(bool _paused) external {
paused = _paused;
}

modifier isNotPaused(uint x) {
require(x > 10 ); // 检查前置条件:判断x是否大于10,如果x大于或者等于10,那么继续执行。
_; // 执行被isNotPaused修饰的函数
}

function add(uint x) external isNotPaused(x){
counter++;
}

solidity函数重载

Solidity的函数重载,是指同一个作用域内,相同函数名可以定义多个函数。

这些函数的参数(参数类型或参数数量)必须不一样,返回值可以一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

contract Test {
function getSum(uint a, uint b) public pure returns(uint){
return a + b;
}
function getSum(uint a, uint b, uint c) public pure returns(uint){
return a + b + c;
}
function callSumWithTwoArguments() public pure returns(uint){
return getSum(1,2);
}
function callSumWithThreeArguments() public pure returns(uint){
return getSum(1,2,3);
}
}

Solidity数学函数

Solidity 也提供了内置的数学函数。下面是常用的数学函数:

  • addmod(uint x, uint y, uint k) returns (uint) 计算(x + y) % k,计算中,以任意精度执行加法,且不限于2^256大小。
  • mulmod(uint x, uint y, uint k) returns (uint) 计算(x * y) % k,计算中,以任意精度执行乘法,且不限于2^256大小。

Solidity加密函数

Solidity 提供了常用的加密函数,包括:

  • keccak256(bytes memory) returns (bytes32) 计算输入的Keccak-256散列。
  • sha256(bytes memory) returns (bytes32) 计算输入的SHA-256散列。
  • ripemd160(bytes memory) returns (bytes20) 计算输入的RIPEMD-160散列。
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) 从椭圆曲线签名中恢复与公钥相关的地址,或在出错时返回零。函数参数对应于签名的ECDSA值: r – 签名的前32字节; s: 签名的第二个32字节; v: 签名的最后一个字节。这个方法返回一个地址。

下面的例子说明了加密函数的用法。

示例

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

contract Test {
function callKeccak256() public pure returns(bytes32 result){
return keccak256("ABC");
}
}

运行上述程序,输出:

1
0: bytes32: result 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
谢谢你的支持哦,继续加油.