blockchain solidity基础
在线智能合约环境:https://remix.ethereum.org/
Reference
基本结构
使用solidity
编写的简单的智能合约
1 | // SPDX-License-Identifier: MIT |
一个典型的智能合约,通常由 4 个部分组成,分别是:声明部分
、合约定义部分
、状态变量部分
和 函数部分
。其中,状态变量部分
和 函数部分
是智能合约的主体。
solidity 基本数据类型
在 Solidity
中,数据可分为两种类型:基础类型
和 复合类型
。
基础类型
包括:整型、布尔型、地址型、字节型、浮点型、枚举型等。
复合类型
包括:数组、映射、结构体等。复合类型是由基础类型组合而成,它比基础类型略微复杂。
注意:数据类型还有其它的分类方法,比如按照数据在传递和使用时的特征不同,又可以分为 值类型(value types)
和 引用类型(reference types)
两大类。
整型
1 | // 无符号整型: uint 的内部存储长度是 256 位 |
算数运算符
1 | // SPDX-License-Identifier: MIT |
点击部署的合约中变量 a_add_b
、a_sub_b
、a_mul_b
、a_div_b
、a_mod_b
,就会显示运算后的变量值。
除了 uint 和 int 外,Solidity
中按照存储长度,还定义了一系列特定长度的 整型
。它们的长度从 8 位一直到 256 位,按照 8 的倍数递增。
无符号整型 uint8、uint16、uint24、uint32……uint256,
有符号整型 int8、int16、int24、int32……int256
获取某种整型的最大值和最小值,可以使用 type
函数。这在有些 ERC20
代币合约中会用到
注意事项:如果不对变量初始化,默认值为 0
布尔型
1 | // SPDX-License-Identifier: MIT |
运算符 ||
和 &&
遵循短路( short-circuiting )规则。
布尔型变量默认值为 false
地址型
地址型
是 Solidity
中常用的数据类型之一,用来存储以太坊中的账户地址,它使用关键字 address
来声明。
地址型
变量的内部存储长度为160位,也就是20个字节,通常使用一个十六进制字符串来表示,并以前缀 0x
开头
账户地址是通过公钥按照一定算法计算得到的。
计算过程如下:
- 首先,将未压缩格式的公钥作为输入值,使用 keccak-256 哈希算法,生成了一个256位的哈希值。
- 然后,截取256位哈希值右边的160位作为账户地址。
- 最后,为了便于显示,账户地址使用十六进制字符串来表示,这个字符串的长度为40( 1个字节由两个16进制字符来表示),并以前缀
0x
开头。
地址之间可以进行比较运算,使用的操作符有:==
、!=
、 <=
、 <
、>=
、>
1 | // SPDX-License-Identifier: MIT |
上面这个程序比较两个账户地址是否相同,可以看到在上面的例子中,两个值并不相同,最终返回值为 false
地址属性和方法
地址
具有 .balance 属性,用于返回该账户中以太坊的余额,这也是 地址
最常用的方法
1 | // SPDX-License-Identifier: MIT |
地址
也经常用于转账,我们可以使用地址的 .transfer()
和 .send()
方法进行转账。
1 | // SPDX-License-Identifier: MIT |
浮点型
浮点型
共有两种:fixed
和 ufixed
,分别代表 有符号定长浮点数 和 无符号定长浮点数。
还定义了一系列特定长度的浮点数,分别使用关键字 fixedMxN
和 ufixedMxN
表示,其中 M 表示该类型占用的总位数,N 表示可用的小数位数。
M 可以取值 8 到 256 位,但必须能够被 8 整除;N 可以是从 0 到 80 之间的任意数。
fixed
和 ufixed
分别是 fixed128x18 和 ufixed128x18 的别名。
示例
1 | // 浮点型常量可以直接赋值 |
重要
在 Solidity
中,wei
是以太币的基本计量单位,也是默认的计量单位,而不是 ETH
。
1 个以太币 ETH
等于 10**18(10的18次方)wei
。
在 Solidity
中,对某地址 address 进行一笔转账,可以使用如下的方式
1 | payable(address).transfer(100); |
表明向 address 转了 100 wei,而不是100个比特币。
枚举型
1 | // 定义枚举类型 |
注意事项:
- 枚举列表不能为空,至少要包含一个成员,否则编译器将会报错
- 定义枚举型的语句,后面不需要跟着分号。一般大括号的后面都不需要跟着分号,如果跟着分号,编译器会报错
- 枚举型只能在全局空间内声明,不能在函数内声明。通常放在状态变量前面的位置声明
枚举型在编译后就会转换为无符号整数 uint8
,在 Solidity
中,枚举值可以转换为整数,它的第一个成员的值默认是 0,后面的值依次递增。
1 | enum status {normal, deleted} |
下面是一个类型转换的例子
1 | // SPDX-License-Identifier: MIT |
结果如下
solidity 变量
Solidity
提供了 3 种类型的变量:状态变量
、局部变量
、全局变量
。
状态变量
状态变量是指在智能合约中声明的持久化存储的变量。它存在于合约的整个生命周期,直到合约被销毁。
状态变量的变动是要记录在区块链上的,永久存储,也就是通常所说的“数据上链”。
状态变量在合约的不同函数之间共享,可以通过调用函数来读取或修改它的数据值。
状态变量类似于其它编程语言中“类”的成员变量。
1 | // SPDX-License-Identifier: MIT |
局部变量
局部变量如上代码中所示,不难理解其作用。
1 | // SPDX-License-Identifier: MIT |
全局变量
全局变量是指在合约的顶层预先定义的特殊变量,用于提供有关区块链和交易属性的信息。
全局变量是由 Solidity
语言本身提供,用户无权定义或者修改,但可以直接在任何位置使用。
1 | // SPDX-License-Identifier: MIT |
可见性
Solidity
为状态变量提供了 3 种可见性修饰符,分别是 public
、private
和 internal
,用于限制状态变量的访问权限。
1 | uint256 public delta = 8; // 可见性声明为 public 合约内部和外部都可以访问这个状态变量 |
internal
1 | // SPDX-License-Identifier: MIT |
注意事项:如果状态变量在声明的时候,没有指定可见性,那么它的可见性就为默认值 internal
。
默认值
Solidity
中有一个 delete
操作符,它可以对变量重新赋值,从字面意思来看,似乎是要删除一个变量,其实不是,delete
操作符只是对变量重新初始化,使其值变为默认值。
1 | uint a = 100; |
常量
在智能合约中,如果一个状态变量的值恒定不变,就可以使用关键字 constant
或者 immutable
进行修饰,把它定义为常量。
1 | string constant SYMBOL = "WETH"; |
状态变量一旦声明为 constant
和 immutable
后,就不能更改它的值了。
constant
和 immutable
区别:
constant
关键字修饰的状态变量,必须在声明时就立即显式赋值,然后就不再允许修改了immutable
关键字修饰的状态变量,既可以在声明时显式赋值,还可以在合约部署时赋值,也就是在合约的构造函数constructor
中赋值适用的数据类型
constant
可以修饰任何数据类型。immutable
只能修饰值类型,比如:int、uint、bool、address 等,不能修饰 string、bytes 等引用类型。
注意事项:
使用常量比变量更节省 gas
成本,这也是非常重要的一点。常量的值在编译时就已知,且不可改变,编译器会将其值直接嵌入到合约代码中,避免了在运行时进行存储和访问的开销。
solidity 函数
在 Solidity
中,函数是合约中的可执行代码块,用于定义合约的行为和操作,函数和状态变量是 Solidity
智能合约中最重要的组成部分。
函数定义如下:
1 | function function_name(<parameter list>) <visibility> <state mutability> [returns(<return type>)] { |
举例如下(一个加法函数):
1 | function add(uint a, uint b) public pure returns(uint) { |
函数可见性共有 4 种,分别是 private
、public
、internal
、external
,用于限制函数的使用范围。
在 Solidity
的新版本中,函数定义中必须显式地指定可见性,不能省略,否则无法通过编译。
1 | // SPDX-License-Identifier: MIT |
如图可以看到只有 div 和 sub 可以从外部调用
返回值
在 Solidity
中,函数可以没有返回值,也可以返回一个或多个值,返回值可以是任何有效的数据类型,在函数声明中,需要使用 returns
关键字指定返回值的类型。
在函数体中,可以有两种方式来返回结果值:
- 使用
return
关键字指定返回值; - 使用返回值的参数名称指定返回值。
1 | // SPDX-License-Identifier: MIT |
多返回值
1 | // SPDX-License-Identifier: MIT |
solidity 支持函数重载
状态可变性 visibility
函数的状态可变性有 4 种:pure
、view
、payable
、未标记状态。
1. pure
状态可变性为 pure
的函数,也称为纯函数,是指函数不会读取和修改合约的状态,换言之,pure
函数不会读取和修改链上的数据,例如:
1 | // SPDX-License-Identifier: MIT |
由于上面的代码只使用了局部变量,并不涉及到全局变量和状态变量,因此可以将可变性设置为 pure。
在下面的情况下就不能使用 pure,不然不能通过编译
- 读取状态变量
- 访问
<address>.balance
- 访问任何区块、交易、msg等全局变量
- 调用了任何不是纯函数的函数
- 使用包含特定操作码的内联汇编
2. view
状态可变性为 view
的函数,也称为视图函数,是指函数会读取合约的状态,但不会进行修改。
1 | // SPDX-License-Identifier: MIT |
上面的代码只是对状态变量进行了读取,但并没有修改其值,因此可以将可变性设置为 view。
对于下列情况就不适用于 view:
- 修改状态变量
- 触发事件
- 创建其它合约
- 使用了自毁函数
selfdestruct
- 调用发送以太币
- 调用任何不是
view
或pure
的函数 - 使用了底层调用
- 使用包含特定操作码的内联汇编
3. 未标记状态可变性
如果一个函数定义中没有标记任何状态可变性,也就是说,函数既没标记为 view
也没标记为 pure
,那么就意味着这个函数是要改变状态的
1 | // SPDX-License-Identifier: MIT |
4. payable 状态可变性
如果一个函数的状态可变性标记为 payable
,那么就表示它可以接收以太币,这些以太币是由调用者在调用函数时支付的。
1 | // SPDX-License-Identifier: MIT |
设置状态可变性的作用:
- 安全性
- 可靠性:
view
、pure
修饰的函数在调用时不会产生副作用,因此可以安全地被其它函数调用 - 互操作性:通过标记函数的状态可变性,可以提供给其它合约和工具有关函数的重要信息
调用 view 、pure 函数,无需支付 gas,而调用非 view 、pure 函数就需要支付一定的 gas
构造函数
1 | // SPDX-License-Identifier: MIT |
对于构造函数不需要设置为 pure 或者 view,因为构造函数常用来初始化合约,会改变合约的状态,如果在部署一个合约的时候,同时需要向合约内存入一些以太币,那么就需要将构造函数的可见性设置为 payable
。
1 | // SPDX-License-Identifier: MIT |
经典示例
1 | // SPDX-License-Identifier: MIT |
这段合约代码在部署的时候,将调用构造函数 constructor
,把合约部署者的地址 msg.sender 记录在了状态变量 _owner 中, _owner 类似于合约的管理员,比普通用户拥有更大的权限。
比如,下面的函数 operate,就只能由合约部署者使用,而其它用户不能使用。
1 | require(msg.sender == _owner, "caller is not the owner"); |
这里判断调用者是不是合约部署者 _owner。 如果是合约部署者的话,就继续向后执行;如果不是的话,就会输出错误信息。
点击函数 owner,可以获取合约部署者地址,
接受函数 receive
在以太坊区块链中存在两种类型的账户:外部账户
和 合约账户
,它们在以太坊上有着不同的特性和用途。
1. 外部账户
外部账户,英文为 Externally Owned Account,缩写为 EOA
,外部账户也就是平常使用的用户账户,用于存储以太币 ETH
。这些账户可以向其它账户发送以太币,或者从其它账户接收以太币。
在钱包里管理的账户,通常就是外部账户。比如,在小狐狸钱包 Metamask
里添加或者生成的 Account
就是外部账户。外部账户会有一个与之相关的以太坊地址,这个地址是一个以 “0x” 开头,长度为20字节的十六进制数,比如:0x7CA35…9C6F。外部账户都有一个对应的私钥,只有持有私钥的人才能对交易进行签名,所以,外部账户非常适用于资金管理。
常说的以太坊账户,在不特别指明的情况下,一般是指外部账户。
上图就是外部账户,同时拥有一个对应的私钥。
2. 合约账户
合约账户,英文为 Contract Account
,缩写为 CA
。在以太坊区块链上部署一个智能合约后,都会产生一个对应的合约地址,这个地址称为合约账户。合约账户主要用于托管智能合约,它里面包含着智能合约的二进制代码和状态信息。合约账户地址的格式与外部账相同:以 “0x” 开头,长度为20字节的十六进制数。合约账户没有私钥,只能由智能合约中的代码逻辑进行控制。
它在一定条件下,也可以用来存储以太币 ETH
。
3. receive 函数
在以太坊区块链上部署智能合约时产生的合约账户,并不都是可以存入以太币ETH
的。一个智能合约如果允许存入以太币,就必须实现 receive
或者 fallback
函数。如果一个智能合约中这两个函数都没有定义,那么它就不能接收以太币。
如果只是为了让合约账户能够存入以太币,按照 solidity
语言规范,推荐使用 receive
函数。因为 receive
函数简单明了,目的明确,而 fallback
函数的用途相对复杂一些。
格式如下:
1 | receive() external payable { |
receive
函数有如下几个特点:
- 无需使用
function
声明。 - 参数为空。
- 可见性必须设置为
external
。 - 状态可变性必须设置为
payable
。
当外部地址向智能合约地址发送以太币时,将触发执行 receive
函数,可以在函数体内不写任何自定义的处理逻辑,它依然能够接收以太币,这也是最常见的使用方式。
如果必须在 receive
的函数体内添加处理语句的话,最好不要添加太多的业务逻辑。
因为外部调用 send
和 transfer
方法进行转账的时候,为了防止重入攻击,gas
会限制在 2300。如果 receive
的函数太复杂,就很容易会耗尽 gas
,从而触发交易回滚。
receive
函数里通常会执行一些简单记录日志的动作,比如触发 event
。
1 | // SPDX-License-Identifier: MIT |
回退函数 fallback
在 Solidity
语言中,fallback
是一个预定义的特殊函数,用于在处理未知函数和接收以太币 ETH 时调用。
定义 fallback
函数的格式如下:
1 | fallback () external [payable] { |
fallback
函数有如下几个特点:
- 无需使用
function
声明。 - 参数为空。
- 可见性必须设置为
external
。 - 状态可变性可以为空,或者设置为
payable
。
调用条件
fallback
会在两种情况下,被外部事件触发而执行:
外部调用了智能合约中不存在的函数
在这种情况下,函数声明中无需设置状态可变性,函数形式如下:
1
2fallback () external {
}外部向智能合约中存入以太币,并且当前合约中不存在 receive 函数
在这种情况下,函数声明中必须设置状态可变性为
payable
,函数形式如下:1
2fallback () external payable {
}
如果合约中已经定义了 receive
函数,那么向这个合约中存入以太币,将会优先调用 receive
函数,而不会执行 fallback
函数。
所以,如果一个智能合约允许存入以太币,那么它就必须实现
receive
或者fallback
函数,而且函数的状态可变性设置为payable
。如果一个智能合约没有定义这两个函数中的任何一个,那么它就不能接收以太币
receive 和 fallback 工作流程
当参数 msg.data
为空时,就意味着:外部向合约进行转账,存入以太币。
当参数 msg.data
不为空时,就意味着:外部在调用合约中的函数。
在左边的分支可以看到,receive
和 fallback
函数都能够用于接收以太币 ETH
。
一个智能合约在接收 ETH
时:
- 如果存在着
receive
函数,就会触发receive
; - 当不存在
receive
函数,但存在fallback
函数时,就会触发fallback
; - 而当两者都不存在时,交易就会
revert
,存入ETH
失败
测试和验证
第一种情况
智能合约中只定义 fallback
函数,而且状态可变性为 payable
。
1 | // SPDX-License-Identifier: MIT |
转了 100wei,由于没有receive函数,所以触发fallback函数。
第二种情况
智能合约中同时定义了 receive
和 fallback
函数,而且两者的状态可变性都为 payable
。
1 | // SPDX-License-Identifier: MIT |
果不其然,两者同时存在的情况下,会优先调用 receive 函数。
第三种情况
依然使用上面的合约,当调用一个不存在的函数,将会触发 Fallback
事件。
这里依然使用第二种情况中的例子,在 “CALLDATA” 栏中填入随意编写的 msg.data
数据,使之不为空,再点击 **”Transact”**,可以看到交易成功,并且触发了 Fallback
事件。
可以看到在 msg.data 不为空的情况下,这里触发了 fallback 函数。
solidity 流程控制语句
运算符
与 C 语言大体一致
条件语句
与 C 语言大体一致
循环语句
与 C 语言大体一致
断言语句
Solidity
提供了断言语句,用于在合约执行过程中进行条件检查和错误处理,Solidity
支持两种断言语句:require
和 assert
。
1. require
require
语句用于检查函数执行的先决条件,也就是说,确保满足某些特定条件后才能继续执行函数,如果条件不满足,则会中止当前函数的执行,并回滚(revert
)所有的状态改变
1 | // SPDX-License-Identifier: MIT |
调用 transfer 函数,然后将 to 设置为 0 然后调用,可以发现报错。
同时将 amount 设置为 0,也会触发报错
require
完全可以使用 revert
语句来代替。比如:
1 | require(amount > 0, "`amount` is zero"); |
就可以使用 revert
语句改写为:
1 | if(amount <= 0) { |
以上两种写法完全等价。关于 revert
实现原理,将在后面的章节中详细讲解。
2. assert
assert
语句的行为 require
非常类似,通常用于捕捉合约内部编程错误和异常情况。
如果捕捉到了异常,则会中止当前函数的执行,并回滚(revert
)所有的状态改变。
1 | // SPDX-License-Identifier: MIT |
合约中有一个除法函数 divide,在调用的时候,需要首先检测除数 divisor 是否为 0,如果为 0 ,触发异常,函数终止运行。只有除数 divisor 不为 0 ,才会执行除法操作。
require
和 assert
都能终止函数的执行,并回滚交易,但两者有一些区别。
require
和assert
参数不同,require
可以带有一个说明原因的参数,assert
没有这个参数。require
通常用于检查外部输入是否满足要求,而assert
用于捕捉内部编程错误和异常情况。require
通常位于函数首部来检查参数,assert
则通常位于函数内部,当出现严重错误时触发。assert
是Solidity
早期版本遗留下来的函数,不再建议使用,最好使用require
和revert
代替。
require
在实际运行的合约中使用广泛,通常用来检查输入参数的正确性,我们要熟练掌握它的使用方法。
solidity 复合数据类型
数组
Solidity
支持 固定长度数组
和 动态长度数组
两种类型
固定长度数组
格式如下
1 | //type_name arrayName [length]; |
初始化一个 固定长度数组
,可以使用下面的语句:
1 | uint balance[3] = [uint(1), 2, 3]; // 初始化固定长度数组 |
动态长度数组
动态长度数组
与 固定长度数组
相比,就是 动态长度数组
的长度是可以改变的。
声明一个 动态长度数组
,只需要指定元素类型,而不需要指定元素的数量,语法如下:
1 | type_name arrayName[]; |
对于动态长度数组,可以使用 push 和 pop 方法
1 | // SPDX-License-Identifier: MIT |
new、delete 操作
可以使用 new
关键字来动态创建一个数组。
1 | // SPDX-License-Identifier: MIT |
通过 new
关键字创建的数组是一个动态长度数组。它是无法一次性赋予初值的,只能对每一个元素的值分别进行设置。
在前面已经讲过,**delete
操作符只是对变量值重新初始化,使其值变为默认值,而不是删除这个变量**。在数组上使用 delete
操作,效果也是一样的。
delete arr[i],并不是要删除一个数组中的元素,而是将该元素恢为默认值,例如:
1 | uint[] balance = [uint(1), 2, 3]; |
除了动态长度数组的 pop
函数外,Solidity
并没有提供删除某个特定元素的函数。
字节和字符串
字节型
也是 Solidity
语言中重要的数据类型,字节型
用于表示特定长度的字节序列,分为 固定长度字节型
和 动态长度字节型
两种类型,字节型
本质上是一个字节数组。
比如,在智能合约中经常出现的哈希值、数据签名等数据,都会使用 字节型
来定义。
固定长度字节型
固定长度字节型
按照长度分为 32 种小类,使用 bytes1
、bytes2
、bytes3
直到 bytes32
表示,每种类型代表不同长度的字节序列,其中的 bytes32
使用最为普遍。
固定长度字节型
的变量声明如下:
1 | bytes1 myBytes1 = 0x12; // 单个字节 |
动态长度字节型
动态长度字节型
可以存储任意长度的字节序列,它使用关键字 bytes
来声明,在声明变量时,需要指定其长度或初始化值。
1 | bytes myBytes = new bytes(10); // 声明一个长度为 10 的动态长度字节变量 |
示例:
1 | // SPDX-License-Identifier: MIT |
字符串
在 Solidity
中,字符串
是用来存储文本数据的类型。字符串
的值使用双引号 (“) 或单引号 (‘) 包裹,类型用 string
表示。
1 | string public myString = "Hello World"; |
字符串
与固定长度的字节数组非常类似,它的值在声明之后就不可变了,如果想要对字符串
中包含的字符进行操作,通常会将它转换为 bytes
类型。
Solidity
提供了字节数组 bytes
与字符串 string
之间的内置转换。
1 | //bytes 转换 string |
结构体
定义一个结构体类型使用 struct
关键字,它的语法如下:
1 | struct struct_name { |
其中 struct_name
是结构体的名称,typeN
是结构体成员的数据类型,type_name_N
是结构体成员的名字。
这里定义一个结构体类型 Book
,用来表示一本书:
1 | struct Book { |
使用方法
1 | // SPDX-License-Identifier: MIT |
初始化方式
结构体变量共有 3 种初始化数据的方式:按字段顺序初始化、按字段名称初始化、按默认值初始化。
1 | // SPDX-License-Identifier: MIT |
映射
Solidity
中的映射类型 mapping
,用来以键值对的形式存储数据。它的主要作用是提供高效的查找功能,类似于其它编程语言中的哈希表或者字典。
例如,在使用 白名单
的场景中,可以通过 映射类型
将用户地址映射到一个布尔值,以标识哪些地址是被允许的,哪些地址是不被允许的,从而高效地控制访问权限。
映射类型是智能合约中最常用的数据类型之一
语法如下:
1 | mapping(key_type => value_type) |
mapping
类型是将一个键 (key) 映射到一个值 (value)。
其中:key_type 可以是任何基本数据类型,比如:整型、地址型、布尔型、枚举型,以及 bytes
和 string
,但是部分复杂对象不允许使用,比如:动态数组、结构体、映射。
**value_type 可以是任何数据类型 **
1 | struct MyStruct { uint256 value; } // 定义一个结构体 |
在智能合约中,mapping
类型的使用非常普遍的。比如,在 ERC20
代币合约中,经常会使用 mapping
类型的变量作为一个内部账本,用来记录每一个钱包地址拥有的代币余额。
下面例子模拟了一个稳定币 USDT
的合约:
1 | // SPDX-License-Identifier: MIT |
复制上面的合约地址,然后传给下面的 balanceof 函数
返回值为 100
注意:在 mapping
类型的变量中获取一个不存在的键的值,并不会报错,而是会返回值类型的默认值。比如,整型会返回 0,布尔型会返回 false 等。
优缺点
优点:
mapping
可以用来存储数据集,并且提供了高效的查找功能。它可以根据“键”快速定位到特定元素。
缺点:
mapping
最大的问题是无法直接遍历。
值类型和引用类型
当调用一个函数时,如果参数为值类型,那么在函数内部对参数数据进行修改,是不会影响到原始数据的
1 | // SPDX-License-Identifier: MIT |
引用类型
在 Solidity
中引用类型共有三种:数组、结构体 struct
和映射 mapping
。(类似于C中的指针)
1 | // SPDX-License-Identifier: MIT |
数据位置
Solidity
中的数据的存储位置有 3 种:memory
、storage
、calldata
。
1. storage
storage
是指永久保存在区块链上的存储,通常用于存储合约的状态变量。
storage
中的变量在合约部署后会一直存在,直到合约被销毁。
由于它保存在区块链上,需要同步到所有区块链节点,而且永久保存,所以它的使用成本高,gas
消耗多。
1 | contract StorageVar { |
name
是一个状态变量,存储在 storage
中,它的数据会一直保存在区块链上。
在函数中,对于“引用类型”的状态变量,可以通过关键字 storage
来引用它。例如:
1 | // SPDX-License-Identifier: MIT |
为状态变量 data
创建了一个引用 dataRef
,然后修改了 dataRef
的值,dataRef
和 data
实际上指向了同一块数据,也可以说,dataRef
是 data
的别名,所以,修改了 dataRef
指向的数据,也就是修改了 data
指向的数据。
2. memory
memory
是函数调用期间分配的临时内存,通常用于存储引用类型的局部变量。memory
中的变量在函数调用结束后会被销毁。它对应于其它编程语言中的 “堆”。
memory
的使用成本非常低,消耗的 gas
少。
1 | contract MemoryVar { |
函数 name
中的变量 s
,存储在 memory
中。当函数调用结束后,就会从内存中清除。
在函数中,对于“引用类型”的状态变量,可以通过关键字 memory
来创建它的副本。
拿上面 storage
中例子,进行分析,修改了 dataRef
指向的数据,不会修改 data
指向的数据,因为memory会创建一个对应引用类型的副本。
3. calldata
calldata
是外部程序在调用合约函数时,用来保存传入参数的存储位置。
calldata
变量的行为类似于 memory
变量,它在函数调用结束后就会被销毁。两者不同之处在于,calldata
的数据是只读的,不能修改。与 storage
和 memory
相比,calldata
存储的成本最低,gas
消耗最少。
calldata
只能用于函数参数,无法在函数内部声明。
例如:
1 | function setName(string calldata name) external; |
calldata
的使用场景并不多,在函数内部,通常会转为 memory
再去操作。但在某些场景下,出于节省 gas
的目的,也会使用 calldata
变量。
注意:在函数中,值类型的变量通常分配在栈上,不在上面的三种存储中。
solidity 面向对象编程
合约继承
语法如下
1 | contract child_contract is parent_contract { |
示例:
1 | // SPDX-License-Identifier: MIT |
虽然合约 Employee
中没有定义任何状态变量和方法,但是由于它继承了合约 Person
,所以在 Employee
中就自动拥有了状态变量 name
、age
和方法 getSalary
。
注意:子合约只能继承父合约中可见性为 public
、internal
和 external
的状态变量和方法,而可见性为 private
的状态变量和方法,是不能被子合约继承的。
virtual 和 override
Solidity
在合约继承的语法中,引入了 virtual
和 override
两个关键字,用于重写父合约中的函数。
父合约首先需要使用 virtual
关键字声明一个虚函数,然后,在子合约中使用 override
关键字来覆盖父合约的方法。
例如:
1 | // SPDX-License-Identifier: MIT |
子合约 Employee
的 getSalary
方法覆盖了父合约 Person
的 同名方法,调用子合约 Employee
的 getSalary
方法,输出结果是 3000,而不是父合约中的 1000。
注意 :这里如果不使用 virtual 将会报错
构造函数的继承
如果父合约的构造函数带有参数,那么子合约在继承父合约时,就需要在自己的构造函数中显示地调用父合约的构造函数。子合约继承合约时,调用父合约的构造函数有两种方式:直接传值 和 传入输入值。
1 | // SPDX-License-Identifier: MIT |
调用父合约函数
子合约调用父合约的函数有两种方法:使用关键字 super 和 使用合约名称。
1 | // 子合约 Employee |
抽象合约和接口
抽象合约
Solidity
允许在一个合约中只声明函数的原型,而没有具体实现,然后在继承的子合约中再去实现,这样的合约称为抽象合约。
抽象合约使用 abstract
关键字进行声明。
1 | // SPDX-License-Identifier: MIT |
使用 abstract
声明的抽象合约,通常包含至少一个未实现的函数。由于抽象合约并不完整,所以,抽象合约不能单独部署。
接口
定义了合约应该实现的函数名和事件,但没有实现任何函数的具体逻辑。
编写接口有以下限制规则:
- 不能包含状态变量
- 不能定义结构体
- 不能包含构造函数
- 不能继承除接口外的其它合约
- 所有函数的可见性都必须是
external
,而且不能有函数体 - 继承接口的合约必须实现接口定义的所有功能
语法如下:
1 | interface 接口名{ |
下面的代码即是一个接口的实现
1 | // SPDX-License-Identifier: MIT |
使用接口
接口的主要作用是提供了一种约定和规范,用于与其它合约进行交互和通信。
在上面的例子中,编写了一个代币合约 ERC20
,它实现了接口 IERC20
,如果另外有一个合约,要调用已经部署在区块链上的 ERC20
合约,实际上非常简单,只需要知道这个合约的地址,并且知道它实现了 IERC20
接口,就可以在其它合约中使用它。
1 | // SPDX-License-Identifier: MIT |
其中 0xd9145CCE52D386f254917e481eB44e9943F39138 就是上面部署的实现接口的合约地址。
多重继承
语法如下
1 | contract child_contract is parent_contract1, parent_contract2... { |
线性继承
1 | // SPDX-License-Identifier: MIT |
按照线性继承原则,合约的继承顺序是 ContractA
、ContractB
、ChildContract
。
也就是说,ChildContract
先继承 ContractA
的属性和方法,再继承 ContractB
的属性和方法,所以上面例子中的 super 是 ContractB
,调用 super.foo() 的返回结果为 “ContractB”。
多重继承分析
在定义合约多重继承的时候,必须正确地设置父合约的顺序,使之符合线性继承原则。如果顺序设置不正确,那么合约将无法编译。
第一种情况
合约 Y 继承了合约 X,合约 Z 同时继承了 X,Y。如图所示:
1 | X |
这种情况下,我们按照线性继承原则,最基本的合约放在最前面,合约的继承顺序应该为:X,Y,Z。
1 | contract Z is X,Y { |
如果写成:
1 | contract Z is Y,X { |
那么编译器就会报错。
第二种情况
合约 Y 继承了合约 X;合约 A 也继承了合约 X,合约 B 又继承了 A;合约 Z 同时继承了 Y,B。如图所示:
1 | X |
这种情况下,按照线性继承原则,最终理清的合约继承顺序为:X,Y,A,B,Z。
1 | contract Z is Y,B { |
- Title: blockchain solidity基础
- Author: henry
- Created at : 2024-07-30 11:38:55
- Updated at : 2024-07-30 11:40:32
- Link: https://henrymartin262.github.io/2024/07/30/solidity_study/
- License: This work is licensed under CC BY-NC-SA 4.0.