Solidity programming essentials-读书笔记4

Solidity

有多种语言可以用于 EVM。其中一些语言已被弃用,另一些则被不同程度地接受使用。Solidity 是目前为止最流行的 EVM 语言。

从本章开始,本书将重点介绍 Solidity 及其概念,以及有助于编写高效智能合约的结构。

在本章中,我们将直接深入理解 Solidity,包括其结构、数据类型和变量。本章将涵盖以下主题:

Solidity and Solidity files
Structure of a contract
Data types in Solidity
Storage and memory data locations
Literals
Integers
Boolean
The byte data type
Arrays
Structure of an array
Enumeration
Address
Mappings

以太坊虚拟机ethereum virtual machine

Solidity 是一种面向以太坊虚拟机 (EVM) 的编程语言。以太坊区块链通过编写和执行称为智能合约的代码来扩展其功能。我们将在后续章节中详细介绍智能合约,但现在只需知道智能合约类似于用 Java 或 C++ 编写的面向对象类即可。

EVM 执行智能合约中的代码。智能合约是用 Solidity 编写的;然而,EVM 无法理解 Solidity 的高级结构。EVM 理解称为字节码的底层指令。

Solidity 代码需要一个编译器来将其转换为 EVM 可以理解的字节码。Solidity 自带一个编译器来完成这项工作,称为 Solidity 编译器或 solc。我们在上一章中使用 Node.js 的 npm 命令下载并安装了 Solidity 编译器。

整个过程如下图所示,从编写 Solidity 代码到在 EVM 中执行代码。
solidity代码 ——》 solidity编译器 ——》 二进制代码 ——》 配置与执行以太坊虚拟机EVM

Solidity 和 Solidity 文件

Solidity 是一种与 JavaScript 非常接近的编程语言。

Solidity 中存在一些 JavaScript 和 C 语言的相似之处。

Solidity 是一种静态类型、区分大小写且面向对象的编程语言。虽然它是面向对象的,但它支持的面向对象特性有限。这意味着变量的数据类型必须在编译时定义并确定

函数和变量的编写方式应与它们的定义方式相同,并以 OOP 的方式编写

在 Solidity 中,Cat 与 CAT、cat 或任何其他 cat 的变体都不同

Solidity 中的语句终止符是分号;

Solidity 代码编写在扩展名为 .sol 的 Solidity 文件中。

它们是人类可读的文本文件,可以在包括记事本在内的任何编辑器中作为文本文件打开

一个 Solidity 文件由以下四个高级
结构组成:
pragma
comments
import
contracts/library/interface

1. pragma

Pragma 指令通常是 Solidity 文件的第一行代码。

pragma 指令用于指定当前 Solidity 文件使用的编译器版本。

Solidity 是一门新兴语言,并且会持续改进。每当引入新功能或改进时,都会发布一个新版本。

撰写本文时的最新版本为 0.4.19。

借助 pragma 指令,您可以选择编译器版本,并据此调整代码,如下面的代码示例所示:
pragma solidity ^0.8.20;

虽然并非强制要求,但将 pragma 指令声明为 Solidity 文件中的第一条语句是一种良好的实践。

pragma 指令的语法如下:
prama Solidity <<version number>>;

另请注意指令区分大小写。pragma 和 Solidity 都使用小写字母,并包含有效的版本号,语句以分号结尾。
版本号由两个数字组成——主版本号和次版本号。

最佳实践是,最好使用指定的编译器版本来编译 Solidity 代码,而不是使用 ^。因为新版本中的一些更改可能会导致在 pragma 中使用 ^ 时代码失效。例如,throw 语句已被弃用,新版本建议使用 assert、require 和 revert 等新结构。您肯定不希望某天突然发现代码行为异常,感到措手不及。

2. comments

solidity的comments规则是

1
2
3
//这是注释
/* 这是多行注释
这是多行注释 */

3. import

import可以导入其他的solidity文件,这帮助我们模块化solidity代码。

import 'CommonLibrary.sol'

4. contracts合约

除了编译指示 (pragma)、导入语句 (import) 和注释之外,我们还可以定义全局或顶层级别的契约 (contract)、库 (library) 和接口 (interface)。我们将在后续章节中深入探讨契约、库和接口。

对比 Solidity 的 contractPython 的 def

1️⃣ Python 的 def

  • def 用于 定义函数
  • 一个函数是一个可执行的逻辑块,可以接受参数、返回值。
  • 例子:
1
2
3
4
def add(a, b):
return a + b

result = add(2, 3) # result = 5

特点:

  1. 只定义逻辑,不会自动保存状态。
  2. 可以被调用多次,参数不同结果不同。
  3. 函数本身不存储数据(除非你用全局变量或类)。

2️⃣ Solidity 的 contract

  • contract 用于 定义智能合约
  • 一个合约是一个 状态容器 + 函数集合,部署到区块链后会永久存在。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
uint public count; // 状态变量,会存储在区块链上

function increment() public {
count += 1;
}

function decrement() public {
count -= 1;
}
}

特点:

  1. 不仅有函数,还可以有状态变量(存储在链上)和事件
  2. 部署后就存在区块链上,不同用户调用会改变全局状态。
  3. 可以有 构造函数 初始化状态。
  4. 函数和变量都属于这个合约的命名空间。

3️⃣ 主要区别对比表

特性 Python def Solidity contract
定义对象 函数(逻辑块) 合约(状态 + 函数 + 事件)
状态存储 没有默认状态存储 状态变量存储在区块链上
生命周期 程序运行时临时存在 部署后永久存在(除非销毁)
调用/执行 调用函数执行代码 调用函数可以修改链上状态(交易)
作用域 局部/全局变量 合约内部变量和函数属于合约命名空间
关键用途 封装逻辑 定义完整智能合约

💡 一句话理解

  • Python def逻辑块
  • Solidity contract区块链上的“程序实体”,里面可以包含函数 (def 类似函数) 和状态。

contract的结构

Solidity 的主要用途是为以太坊编写智能合约。智能合约是以太坊虚拟机 (EVM) 部署和执行的基本单元。
虽然本书后续章节将专门介绍智能合约的编写和开发,但本章将讨论智能合约的基本结构。

从技术角度来看,智能合约由两种结构组成——变量和函数。变量和函数都有多个方面,这些方面也将在本书中逐一讨论。
本节将使用 Solidity 语言描述智能合约的一般结构。

一个合约由以下结构组成

State variables
Structure definitions
Modifier definitions
Event declarations
Enumeration definitions
Function definitions

一份典型的合同包含以上所有结构。在以下截图中,需要注意的是,这些结构中的每一个又由多个其他结构组成,这些结构将在后续章节详细讨论这些主题时进行阐述:

State variables状态变量

在编程中,变量指的是可以存储值的存储位置。这些值可以在运行时更改。变量可以在代码中的多个位置使用,它们都将指向变量中存储的值。
Solidity 提供了两种类型的变量——状态变量和内存变量。在本节中,我们将介绍状态变量。

Solidity 合约最重要的方面之一是状态变量。这些状态变量由矿工永久存储在区块链/以太坊账本中。合约中声明的、不在任何函数内的变量称为状态变量。状态变量存储合约的当前值。

状态变量分配的内存是静态分配的,并且在合约的生命周期内不能改变(分配的内存大小)。每个状态变量都有一个必须静态定义的类型。Solidity 编译器必须确定每个状态变量的内存分配细节,因此必须声明状态变量的数据类型。

状态变量还附带一些限定符。

它们可以是以下任何一种。

internal: 默认情况下,状态变量带有内部限定符(如果未指定任何内容)。这意味着该变量只能在当前合约函数以及继承自这些函数的任何合约中使用。这些变量不能从外部访问进行修改;但是,可以查看它们。内部状态变量的示例如下。
int internal StateVariable;

private:
此限定符类似于内部限定符,但附加了额外的约束。私有状态变量只能在声明它们的合约中使用。它们甚至不能在
派生合约中使用。私有状态变量的示例如下:
int private privateStateVariable;

public:
此限定符允许直接访问状态变量。
Solidity 编译器会为每个公共状态变量生成一个 getter 函数。
公共状态变量的示例如下:
int public stateIntVariable;

constant:
此限定符使状态变量不可变。
必须在声明时为变量赋值。
实际上,编译器会将代码中所有对该变量的引用替换为已赋值的值。
常量状态变量的示例如下
bool constant hasIncome = true;

如前所述,每个状态变量都有一个关联的数据类型。数据类型有助于我们确定变量的内存需求,并确定可以存储在其中的值。例如,类型为 uint8(也称为无符号整数)的状态变量会被分配预定的内存大小,并且可以存储 0 到 255 之间的值。任何其他值都被视为“外来值”,编译器和运行时都不允许将其存储在该变量中。

Solidity 提供了以下多种开箱即用的数据类型:
bool
uint/int
bytes
address
mapping
enum
struct
bytes/String

使用枚举和结构体,也可以声明自定义的用户定义数据类型。

本章稍后将用一个完整的章节专门介绍数据类型和变量。

Structure definitions

结构体(或结构)有助于实现用户自定义的数据类型。结构体是一种复合数据类型,由多个不同数据类型的变量组成。它们与契约非常相似;然而,它们本身并不包含任何代码,仅由变量构成。

有时您需要将相关数据存储在一起。

假设您想存储有关一名员工的信息,例如:员工姓名、年龄、婚姻状况和银行账号。

为了表示与单个员工相关的这些变量,在 Solidity 中可以使用 struct 关键字声明一个结构体。

结构体中的变量定义在大括号 {} 内,如下图所示:

要创建结构体的实例,请使用以下语法。

无需显式使用 new 关键字。new 关键字

只能用于创建合约或数组的实例,如下图所示:

函数中可以创建结构体的多个实例。

结构体可以包含数组和映射变量,而映射和数组可以存储结构体类型的值。

Modifiers

在 Solidity 中,修饰符总是与一个函数关联。

在编程语言中,修饰符指的是一种结构,它能够改变正在执行的代码的行为。

由于修饰符在 Solidity 中与一个函数关联,因此它可以改变与其关联的函数的行为。

为了便于理解修饰符,可以将其视为一个在目标函数执行之前执行的函数。

假设您想要调用 getAge 函数,但在执行它之前,
您希望执行另一个函数,该函数可以检查合约的当前状态、传入参数的值、状态变量的当前值等等,并据此决定是否执行目标函数 getAge

这有助于编写更简洁的函数,而无需在函数中堆砌验证规则。此外,修饰符可以与多个函数关联。这确保了代码更简洁、更易读、更易于维护。

修饰符的定义方式是:先使用 modifier 关键字,后跟修饰符标识符、它应该接受的任何参数,以及花括号 {} 内的代码。修饰符中的下划线 _ 表示执行目标函数。你可以将其理解为下划线被目标函数替换。payable 是 Solidity 提供的一个开箱即用的修饰符,当应用于任何函数时,它允许该函数接受以太币 (Ether)。

修饰符关键字是在合约级别声明的,如下图所示:

如上图所示,在代码片段的截图中,合约层声明了一个名为 onlyBy() 的修饰符。它使用 msg.sender 来检查传入地址的值,该地址存储在状态变量中。一些诸如 msg.sender 之类的概念可能对读者来说比较复杂,我们将在下一章中详细讲解

该修饰符与 getAge 函数关联,如下图所示:

截图中显示:

只有与合约中 _personIdentifier 状态变量存储的地址相同的账户才能执行 getAge 函数。

如果任何其他账户尝试调用该函数,则不会执行。

需要注意的是,任何人都可以调用getAge函数,但

该函数只会针对单个账户执行。

Events

Solidity 支持事件。Solidity 中的事件与其他编程语言中的事件类似。事件由合约触发,任何感兴趣的人都可以捕获这些事件并执行相应的代码。Solidity 中的事件主要用于通过 EVM 的日志记录功能,将合约的当前状态告知调用应用程序。它们用于通知应用程序合约的变更,应用程序可以利用这些变更来执行其依赖逻辑。应用程序无需像以前那样不断轮询合约以获取状态变更信息;合约可以通过事件来通知它们。

事件在合约的全局级别声明,并在合约的函数中调用。事件使用 event 关键字声明,后跟标识符和参数列表,并以分号结尾。参数中的值可用于记录信息或执行条件逻辑。事件信息及其值作为事务的一部分存储在区块中。在上一章讨论事务属性时,引入了一个名为 LogsBloom 的属性。事务中触发的事件存储在此属性中。

无需显式提供参数变量——只需提供数据类型即可,如下面的屏幕截图所示:

任何函数都可以通过事件名称并传递所需的参数来调用该事件,如下图所示:

截图中显示了这一点:

枚举

enum 关键字用于声明枚举。枚举有助于在 Solidity 中声明自定义的用户定义数据类型。enum由枚举列表组成,该列表是一组预先定义的命名常量。

枚举中的常量值可以在 Solidity 中显式转换为整数。每个常量值都会被赋予一个整数值,第一个常量的值为 0,后续每个常量的值递增 1。

枚举声明使用 enum 关键字,后跟枚举标识符和花括号 {} 内的枚举值列表。需要注意的是,枚举声明不使用分号作为终止符,

并且列表中至少要声明一个成员。

enum gender {male, female}

枚举变量可以按如下代码所示进行声明和赋值:

gender _gender = gender.male;

在 Solidity 契约中,定义枚举并非强制性的。但如果存在一个不变的常量列表(例如前面的示例),则应该定义枚举。这些示例非常适合用来定义枚举。枚举有助于提高代码的可读性和可维护性。

Functions

函数是以太坊和 Solidity 的核心。以太坊维护状态变量的当前状态,并执行交易来更改状态变量的值。

当合约中的函数被调用时,就会创建一个交易。函数是读取和写入状态变量值的机制。

函数是一段代码单元,可以通过按需调用来执行。函数可以接受参数,执行其逻辑,并可选择性地向调用者返回值。它们可以是命名的,也可以是匿名的。Solidity 允许使用命名函数,但合约中只能有一个未命名的函数,称为回退函数。

我们将在本书后面部分详细了解回退函数。

函数声明使用关键字 function,后跟其标识符——在本例中为 getAge。函数可以接受多个以逗号分隔的参数。参数标识符是可选的,但参数列表中必须提供数据类型。函数可以附加修饰符,例如本例中的 onlyBy()

还有一些额外的限定符会影响函数的行为和执行。

函数有可见性限定符和与函数内部可以执行的操作相关的限定符。

接下来将讨论可见性和与函数功能相关的关键字。

函数还可以返回数据,这些信息使用 return 关键字声明,后跟返回参数列表。

Solidity 可以返回多个参数。

函数具有与状态变量类似的可见性限定符。

函数的可见性可以是以下任一类型:
public
internal
private
external

函数还可以拥有以下附加限定符,这些限定符会

改变其在修改合约状态变量方面的行为:
constant
view
pure
payable

我们将在后续章节中详细讨论前面提到的限定词。

函数可以通过其名称调用。

Solidity中的数据类型

Solidity 数据类型大致可以分为以下两种类型:

Value值类型

reference引用类型

这两种类型在 Solidity 中的区别在于它们赋值给变量以及在 EVM 中存储的方式。将一个变量赋值给另一个变量可以通过创建新副本或直接复制引用来实现。值类型维护变量的独立副本,更改一个变量的值不会影响另一个变量的值。然而,更改引用类型变量的值可以确保任何引用该变量的人都能获得更新后的值。

Value值类型

如果一个类型直接将数据(值)存储在它所拥有的内存中,则该类型被称为值类型。这些类型的值存储在自身内部,而不是存储在其他地方。以下图示说明了这一点。在这个例子中,声明了一个无符号整数 (uint) 类型的变量,其数据(值)为 13。变量 a 拥有 EVM 分配的内存空间,地址为 0x123,并且该地址存储了值 13。访问此变量将直接获得值 13:

solidity提供了以下的值类型
bool
unit
int
Address
byte
enum

passing by value按值传递

当一个值类型变量被赋值给另一个变量,或者当一个值类型变量作为参数传递给函数时,EVM
会创建一个新的变量实例,并将原始值类型的值复制到目标变量中。

这称为按值传递。

更改原始变量或目标变量中的值不会影响

另一个变量中的值。两个变量将保持其
独立的、隔离的值,并且可以相互更改而不会影响对方。

reference引用类型

与值类型不同,引用类型并不直接在变量内部存储其值。它们存储的是值所在的地址,而不是值本身。变量持有指向另一个内存位置的指针,该位置存储着实际的数据。引用类型可以占用超过 32 字节的内存。接下来,我们将通过图示来展示引用类型。

在下面的示例中,声明了一个大小为 6 的 uint 类型数组变量。Solidity 中的数组从零开始计数,因此该数组可以容纳七个元素。变量 a 占用 EVM 分配的内存空间,地址为 0x123,并且该地址存储了一个指针值 0x456。该指针指向实际存储数组数据的内存位置。访问变量时,EVM 会解引用指针的值,并显示数组索引对应的值,如下图所示:

solidity提供了以下的引用类型
Arrays
structs
String
Mappings

Passing by reference通过引用传递

当一个引用类型变量被赋值给另一个变量,或者当一个引用类型变量作为参数传递给一个函数时,EVM 会创建一个新的变量实例,并将指针从原始变量复制到目标变量。这被称为按引用传递。两个变量都指向同一个地址。更改原始变量或目标变量的值也会同时更改其他变量的值。两个变量共享相同的值,并且一个变量所做的更改会反映在另一个变量中。

存储和内存数据位置

合约中声明和使用的每个变量都有一个数据位置。EVM 提供以下四种数据结构用于存储变量:

storage存储:这是合约中所有函数均可访问的全局内存。

这种存储是以太坊在其环境中每个节点上存储的永久存储。

memory内存:这是合约中每个函数均可访问的本地内存。

这种内存生命周期短,会在函数执行完毕后被销毁。

calldata调用数据:所有传入的函数执行数据(包括函数参数)都存储在这里。这是一个不可修改的内存位置。

stack栈:EVM 维护一个栈,用于加载变量和中间值,以便与以太坊指令集交互。
这是 EVM 的工作集内存。EVM 中的栈深度为 1024 层,如果存储的数据超过此深度,则会引发异常。

变量的数据位置取决于以下两个因素:

变量声明位置

变量数据类型

基于前两个因素,存在一些规则来支配和决定变量的数据位置。这些规则在此处列出。

数据位置也会影响赋值运算符的工作方式。赋值运算符和数据位置都由支配它们的规则来解释。

规则 1

声明为状态变量的变量始终存储在存储数据位置。

规则 2

声明为函数参数的变量始终存储在内存数据位置。

规则 3

默认情况下,函数内部声明的变量存储在内存数据位置。但是,需要注意以下几点:

值类型变量的存储位置在函数内部的内存中,

而引用类型变量的默认存储位置是存储区。

请注意,对于在函数内部声明的引用类型变量,默认存储位置是存储区。但是,此默认位置可以被覆盖。

通过覆盖默认位置,引用类型变量可以存储在内存数据位置。引用类型包括数组、结构体和字符串。

在函数内部声明的未覆盖默认位置的引用类型必须始终指向一个状态变量。

在函数内部声明的值类型变量不能被覆盖,也不能存储在存储区。

映射始终声明在存储区。这意味着它们不能在函数内部声明,也不能声明为内存类型。但是,函数内部的映射可以引用声明为状态变量的映射。

规则 4

调用者提供给函数的参数总是存储在 calldata 数据位置中。

规则 5

从一个状态变量赋值给另一个状态变量总是创建一个新的副本。声明了两个值类型状态变量 stateVar1 和

stateVar2。在 getUInt 函数中,stateVar2 被赋值给stateVar1。此时,两个变量的值均为 40。

下一行代码将 stateVar2 的值更改为 50 并返回stateVar1。返回值为 40,这表明每个变量
都保持其自身独立的值,如下面的屏幕截图所示:

规则 6

从另一个内存变量赋值给存储变量始终创建新副本

规则 78 跳过

Literals

跳过

Integers

跳过

Boolean

跳过

the byte data type

Arrays

fixed Arrays

dynamic Arrays

special Arrays

the bytes array

the string array

array properties

structure of an array

enumerations

Address

Mappings

总结

这是深入探讨 Solidity 的第一章。本章介绍了 Solidity,以及 Solidity 文件的布局,包括可以在顶层声明的元素。从布局的角度讨论了构造函数、编译指示、合约和合约元素。本章的核心是对 Solidity 数据类型的全面深入探索。

值类型和引用类型进行了深入讨论,同时还详细讨论了int、uint、固定大小的字节数组、字节、数组、字符串、结构体、枚举、地址、布尔值和映射等类型,并提供了示例。Solidity 还提供了来自复杂类型(例如结构体和数组)的额外数据位置,这些类型也进行了深入讨论,并解释了其使用规则。

在下一章中,我们将重点介绍智能合约的一些开箱即用的变量和函数。 Solidity 提供了大量的全局变量和函数,以简化获取当前交易和区块上下文的任务。

这些变量和函数提供上下文信息和 Solidity 代码,并用于逻辑执行。它们在编写企业级智能合约中发挥着非常重要的作用。

page217