来源 | 《Solidity编程》
作者 | Ritesh Modi
责编|乔治
出品 | 区块链大本营(blockchain_camp)
##敲重点,文末送书哟
Solidity 的语法概念最早由Gavin Wood在提出,后期则由 Christian Reitwiessner 所领导的以太坊团队 Solidity 接手开发。该语言是针对以太坊虚拟机(EVM)所设计的5种语言之一(除 Solidity 外,还包括 Serpent、LLL、Vyper(实验中)和 Mutan(已弃用)),是目前 EVM 上最流行的语言。
如果你不懂 Solidity 语言,就谈不上如何「玩转以太坊」了。
本文将重点介绍 Solidity 及其概念,以及如何编写高效的智能合约。别嫌长,都是干货!
以太坊虚拟机
Solidity 是针对以太坊虚拟机(EVM)的编程语言。以太坊区块链通过编写和执行称为智能合约的代码来帮助扩展其功能。
EVM 执行作为智能合约的一部分的代码。智能合约是用 Solidity 语言写的,然而,EVM 并不理解 Solidity 的高级结构,EVM 可以理解的是称为字节码的低级指令。
需要编译器将 Solidity 代码转换为 EVM 可理解的字节码。Solidity 附带的编译器称为 Solidity 编译器或 solc,它负责完成这种转换。
从在 Solidity 中编写代码到在 EVM 中执行代码,整个过程如下图所示:
Solidity 和 Solidity 文件
Solidity 是一种非常接近 JavaScript 的编程语言。在 Solidity 中可以找到 JavaScript 和 C 之间的相似之处。Solidity 是一种静态类型、区分大小写的面向对象编程(OOP)语言。虽然它是面向对象的,但支持有限的面向对象特征。这意味着在编译时,应该定义并且已知变量的数据类型。应该按照面向对象编程的方式来定义函数和变量。在 Solidity 中,Cat 不同于CAT、cat 或任何其他 cat 的变体。Solidity 语句的终结符是分号(;)。
在扩展名为 .sol 的 Solidity 文件中编写 Solidity 代码。Solidity 文件是人类可读的文本文件,可以在任何编辑器(包括记事本)中打开。
Solidity 文件由以下四个高级结构组成:
预编译指令
注释
导入
合约/库/接口
1、预编译指令
预编译指令通常是任何 Solidity 文件中的第一行代码。pragma 是指定当前 Solidity 文件编译器版本的指令。
Solidity 是一种新的语言,并且在持续改进。每当引入新特性或改进时,都会推出新的版本。
在 pragma 指令的帮助下,你可以针对你的代码选择相应的编译器版本,如下面代码所示:
pragma Solidity ^0.4.19;
虽然不是强制性的,但将 pragma 指令作为 Solidity 文件中的第一条语句是一种很好的做法。
pragma 指令的语法如下:
pragma Solidity <<version number>>;
需要注意指令是区分大小写的,随后的有效的版本号和语句以分号结束。
版本号由两个数字组成 ——主版本号和次版本号。
上例中的主版本号是4,次版本号是19。通常,在次版本中很少或没有重大更改,但主版本之间可能会有重大更改。你应该选择最满足需求的版本。
字符也被称为脱字符号,在版本号中是可选的,但在根据以下规则决定版本号方面起着重要作用:
^字符是指主版本中的最新版本。因此,^0.4.0是指版本号为4的最新版本,当前为0.4.19。
^字符除了提供的主版本号之外不针对任何其他主版本。
Solidity文件只能用主版本号为4的编译器进行编译,不能在其他主版本号的编译器上编译。
使用精确的编译器版本而不是使用^来指定编译 Solidity 代码,是一种更好的做法。当在 pragma 指令中使用^字符时,如果编译器的新版本有改动,可能会弃用你的代码。例如,throw 语句已被弃用,并建议在较新版本中使用较新的结构,如 assert、require 和 revert。你不会想在某一天惊讶地发现你的代码表现得和之前不同。
2、注释
任何编程语言都提供了注释代码的功能, Solidity 也是如此。Solidity 有以下3种注释方式:
单行注释
多行注释
以太坊自然规范(Natspec)
单行注释用双斜线表示 //,多行注释用/*和*/表示。Natspec 有两种格式:///用于单行,/**和*/分别用于多行注释的开始和结尾。Natspec 用于文件,它有自己的规范。
可从网址 /ethereum/wiki/wiki/Ethereum-Natural-Specification-Format 获取整个规范。
我们通过以下代码看一下 Solidity 的注释:
// 这是 Solidity 中的单行注释
/* 这是 Solidity 中的多行注释。
多个连续行作为整体来注释 */
在 Remix 中,pragma 指令和注释如下图所示:
3、import 语句
import 关键字帮助我们导入其他 Solidity 文件,以便在当前的 Solidity 文件和代码中访问其代码。这有助于我们编写模块化的 Solidity 代码。使用 import 的语法如下所示:
import<<filename>>;
文件名可以是完全显式或隐式路径。斜杠“/”用于将目录从其他目录和文件中分离出来,“.”用于引用当前目录,“..”用于引用父目录。这与 Linux bash 引用文件的方式非常相似。这里显示了一个典型的 import 语句。另外,请注意以下代码结尾的分号:
import 'CommonLibrary.sol';
4、合约
除了 pragma、import 和注释外,我们还可以定义全局或顶级的合约、库和接口。我们将在随后的章节中深入探讨合约、库和接口。本章假定你知道可以在同一个 Solidity 文件中声明多个合约、库和接口。下图中显示的 library、contract、和 interface 关键字区分大小写:
合约的结构
Solidity 的主要目的是为以太坊编写智能合约。智能合约是 EVM 部署和执行的基本单元。尽管本书后面的多个章节专门用于编写和开发智能合约,但本章将讨论智能合约的基本结构。
从技术上讲,智能合约由两个结构——变量和函数组成。变量和函数都有多个方面,这也是本书将讨论的内容。本节主要介绍使用 Solidity 语言的智能合约的常规结构。
合约由以下多个结构组成:
状态变量
结构定义
修改器定义
事件声明
枚举定义
函数定义
典型的合约由前面提到的所有结构组成。需要注意,在下图中,这些结构中的每一个由多个其他结构组成:
1、状态变量
编程中的变量是指可以包含值的存储单元。这些值可以在运行时更改。可以在代码中的多个位置使用变量,并且它们都将引用存储的值。Solidity 提供了两种类型的变量——状态变量和内存变量。
状态变量是 Solidity 合约最重要的特性之一。状态变量由矿工永久存储在区块链/以太坊账本中。在合约中,没有在任何函数内声明的变量称为状态变量。状态变量存储合约的当前值。状态变量的内存是静态分配的,并且在合约生命周期内不能更改(分配的内存大小)。必须静态定义每个状态变量的类型。Solidity 编译器必须确定每个状态变量的内存分配细节,因此必须声明状态变量的数据类型。
状态变量还有相关联的限定符,可以是以下任何一种:
internal:默认情况下,如果没有指定任何内容,则状态变量具有 internal 限定符。这意味着这个变量只能在当前的合约函数和任何继承它们的合约中使用。这些变量不能被外部访问修改,但是,可以查看它们。
内部状态变量的示例如下所示:int internal StateVariable;
private:这个限定符就像 internal 上附加的约束。私有状态变量只能在声明它们的合约中使用。即使在派生合约中也不能使用它们。
私有状态变量的示例如下所示: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
使用 enum 和 struct,可以声明自定义的用户数据类型。
2、结构
结构或结构体有助于实现自定义的用户数据类型。结构是一种复合数据类型,由多个不同数据类型的变量组成。它们与合约非常相似,但是,它们不包含任何代码。它们只包含变量。
有时你想将相关数据存储在一起。假设你想存储关于员工的信息,比如员工姓名、年龄、婚姻状况和银行账号。为了表示这些与单个员工相关的变量,可以使用 Solidity 的结构中的 struct 关键字进行声明。结构中的变量在花括号{}内定义,如图所示:
使用下面的语法来创建一个结构的实例。不需要显式调用关键字 new。关键字new只用于创建合约或者数组的实例,如图所示:
在函数中可以创建多个结构体的实例。结构体可以包含数组和映射变量,映射和数组可以存储结构体的值。
3、修改器
在 Solidity 中,修改器总是与函数关联。编程语言中的修改器是指改变执行代码行为的结构。由于修改器与 Solidity 中的某个函数相关联,因此它可以改变与其关联的函数的行为。为了便于理解修改器,可以将其视为在执行目标函数之前执行的函数。假设你想调用 getAge 函数,但是在执行它之前,你想要执行另一个函数来检查合约的当前状态、传入参数中的值、状态变量中的当前值等,并相应地决定是否应该执行目标函数 getAge。这有助于编写更整洁的函数,而不会用验证和确认规则混淆它们。此外,修改器可以与多个函数关联。这确保了更清晰、更易读、更易维护的代码。
修改器使用 modifier 关键字后跟修改器标识符、它应该接受的任何参数、花括号{}内编写的代码来进行定义。修改器中的“_”(下划线)表示执行目标函数。你可以将此视为下划线被内联的目标函数替换。payable 是一种由 Solidity 提供的开箱即用的修改器,当应用于任何函数时允许该函数接受以太币。
在合约级别声明了 modifier 关键字,如下图所示:
名为 onlyBy() 的修改器在合约级别被声明。它使用 msg.sender 检查传入地址的值,msg.sender 保存地址并将其存储在状态变量中。
该修改器与 getAge 函数关联,如下图所示:
getAge 函数只能由与合约的 _personIdentifier 状态变量中存储的地址相同的账户执行。如果任何其他账户尝试调用该函数,该函数将不会执行。
需要注意的是,任何人都可以调用 getAge 函数,但执行只会发生在单个账户中。
4、事件
Solidity 支持事件。Solidity 中的事件就像其他编程语言中的事件一样。事件是从合约中触发的,任何对它们感兴趣的人都可以捕获它们并响应执行代码。Solidity 中的事件主要用于通过 EVM 的日志工具向调用应用程序通知合约的当前状态。它们用于通知应用程序有关合约中的改变,并且应用程序可以使用它们来执行相关逻辑。它们不是应用程序,而是轮询合约中特定状态的更改,合约可以通过事件通知它们。
合约中声明的事件在全局域有效,并且被合约中的函数所调用。使用 event 关键字声明一个事件,后跟一个标识符和参数列表并以分号结尾。参数中的值可用于记录信息或执行条件逻辑。事件信息及其值作为交易的一部分存储在区块内。在上一章讨论交易的属性时,引入了一个名为 LogsBloom 的属性。作为交易的一部分引发的事件存储在此属性中。
没有必要显式地提供参数变量——只有数据类型就足够了,如下图所示:
可以从任何函数调用事件的名称并传递所需的参数,如下图所示:
5、枚举
enum 关键字用于声明枚举。枚举用于在 Solidity 中声明用户自定义的数据类型。枚举包含一个枚举列表、一组预定义的常量。
在 Solidity 中,枚举中的常量值可以显式地转换为整数。每个常量值对应一个整数值,第一个值为0,每个连续项的值增加1。
使用 enum 关键字声明枚举,后面跟着枚举标识符和花括号{}内的枚举值列表。值得注意的是,枚举声明没有分号作为终结符,并且列表中至少应该声明一个成员。
枚举的示例如下:
enum gender {male, female}
可以声明并赋值枚举变量,如下面的代码所示:
gender_gender = gender.male;
在 Solidity 合约中并没有强制要求定义枚举。如果如前面的例子所示有一个不变的项目常量列表,则应该定义枚举。这是声明枚举的很好例子。它们使你的代码更具可读性和可维护性。
6、函数
函数是以太坊和 Solidity 的核心。以太坊维护状态变量的当前状态,并执行交易以更改状态变量中的值。当调用或触发合约中的某个函数时,会导致创建一个交易。函数机制是为了从状态变量读取值和向状态变量写入值。函数是一个按需调用执行的代码单元。函数可以接受参数,执行其逻辑,并可选地将值返回给调用者。可以以匿名方式命名函数。Solidity 提供了命名函数,在合约中只能有一个称为 fallback 函数的未命名函数。
在 Solidity 中声明函数的语法如下:
使用 function 关键字后跟其标识符 getAge 来声明函数。它可以接受多个以逗号分隔的参数。参数标识符是可选的,但应该在参数列表中提供数据类型。函数可以附加修改器,比如 onlyBy()。
有几个额外的限定符会影响函数的行为和运行。函数具有可见性限定符以及与可在函数中执行的操作有关的限定符。下面讨论可见性和与函数能力相关的关键字。函数也可以返回数据,并且使用 return 关键字来声明这些信息,然后是返回参数列表。Solidity 可以返回多个参数。
函数具有与状态变量类似的可见性限定符。函数的可见性可以是以下任何一种:
public:这种可见性使得函数可以直接从外部访问。它们成为合约接口的一部分,可以在内部和外部调用。
internal:默认情况下,如果没有指定,则状态变量具有 internal 限定符。这意味着此函数只能用于当前合约以及任何从其继承的合约。这些函数不能从外部访问,它们不是合约接口的一部分。
private:私有函数只能在声明它们的合约中使用,即使在派生合约中也不能使用它们。它们不是合约接口的一部分。
external:这种可见性使得函数可以直接从外部但不是内部访问。这些函数是合约接口的一部分。
函数还可以具有以下附加限定符,这些限定符能够更改合约状态变量:
constant:这些函数不具有修改区块链状态的能力。它们可以读取状态变量并返回给调用者,但不能修改任何变量、触发事件、创建另一个合约、调用其他可以改变状态的函数等。将常函数看作可以读取和返回当前状态变量值的函数。
view:这些函数是常量函数的别名。
pure:pure 函数进一步限制了函数的能力。pure 函数既不能读取也不能写入,即它们不能访问状态变量。使用此限定符声明的函数应确保它们不会访问当前状态和交易变量。
payable:使用 payable 关键字声明的函数能够接受来自调用者的以太币。如果发送者没有提供以太币,则调用将会失败。如果一个函数被标记为 payable,该函数只能接受以太币。
Solidity 中的数据类型
Solidity 数据类型可以大致分为以下两种类型:
值类型
引用类型
这两种类型在变量赋值和存储在 EVM 中的方式方面有所不同。可以通过创建一个新副本或者仅仅通过处理引用来完成变量的赋值。值类型维护变量的独立副本,并且在一个变量中更改值不会影响另一个变量中的值。但是,更改引用类型变量中的值可确保任何引用该变量的地方都会获取更新值。
1、值类型
如果一个类型将数据(值)直接保存在内存中,则称该类型为值类型。值类型在自己的存储空间中保存数值。下图说明了这一点。在此示例中,数据类型为无符号整数(uint)的变量声明13作为其数据(值)。变量a具有由 EVM 分配的存储空间 0x123,并且该位置具有存储的值13。访问这个变量将直接得到值13:
值类型是大小不超过32字节内存的类型。Solidity 提供以下值类型:
bool:可以保存 true 或 false 作为其值的布尔值
uint:这是无符号整数,只能保存0和正值
int:这是可以保存负值和正值的有符号整数
address:这表示以太坊环境中的账户地址
byte:这表示固定大小的字节数组(byte1 到 bytes32)
enum:可以保存预定义的常量值的枚举
值传递
如果将值类型变量赋给另一个变量,或者将值类型变量作为参数传送给函数,则 EVM 会创建一个新变量实例并将原始值类型的值复制到目标变量中。这被称为值传递。更改原始或目标变量中的值不会影响另一个变量中的值。这两个变量将保持其独立的值,并且它们可以在其他变量不知道的情况下更改值。
2、引用类型
与值类型不同,引用类型不直接将其值存储在变量本身中。它们存储的不是值,而是值存储位置的地址。该变量保存了指向另一个实际存储数据的内存位置的指针。引用类型可以占用大于32字节的内存。下面通过图示显示引用类型。
在以下示例中,声明了一个数据类型为 uint 的大小为6的数组变量。Solidity 中的数组是从0开始计数的,所以此数组可以包含7个元素。变量a由 EVM 分配存储空间 0x123,该位置保存了指针值 0x456。该指针指向存储数组数据的实际内存位置。访问该变量时,EVM 将引用该指针的值并显示数组索引中的值,如下图所示:
Solidity 提供以下引用类型:
数组:这是固定大小或动态大小的数组。
结构:这是自定义的即用户定义的结构。
字符串:这是字符序列。在 Solidity 中,字符串最终被存储为字节。
映射:与存储键值对的其他语言中的散列表或字典相似。
引用传递
当引用类型变量被赋给另一个变量时,或者当引用类型变量作为参数传送给函数时,EVM 会创建一个新变量实例并将指针从原始变量复制到目标变量中。这被称为引用传递。这两个变量都指向相同的地址位置。改变原始或目标变量中的值也会改变其他变量的值。这两个变量将共享相同的值,并且一个变量的变化反映在另一个变量中。
存储和内存数据位置
在合约中声明和使用的每个变量都有一个数据位置。EVM 提供以下4种用于存储变量的数据结构:
存储:这是可以被合约内所有函数访问的全局内存变量。是以太坊将其存储在环境中每个节点上的永久存储。
内存:合约中的每个函数都可以访问的本地内存。它是生命周期的短暂的内存,当函数执行完成后会被销毁。
调用数据:存储所有传入的函数执行数据,包括函数参数。这是一个不可修改的内存位置。
堆栈:EVM 维护用于加载变量和使用以太坊指令集的变量和中间值的堆栈。这是 EVM 的工作集内存。在 EVM 中,堆栈的深度为1024层,任何超过此数量的深度都会引发异常。
变量的数据位置取决于以下两个因素:
变量声明的位置
变量的数据类型
基于上述两个因素,有规则规定了如何管理和决定变量的数据位置。数据位置也会影响赋值运算符的工作方式。赋值和数据位置的管理是由规则来解释的。
1、规则1
状态变量始终存储在存储数据位置。
2、规则2
声明为函数参数的变量始终存储在内存数据位置。
3、规则3
在默认情况下,在函数中声明的变量存储在内存数据位置。但是,有以下几点警告:
函数中的值类型变量的存储位置是内存,而引用类型变量的缺省位置是存储。请注意,在函数内声明的引用类型变量默认保存在存储中。但是,它可以被覆盖。
通过覆盖默认位置,引用类型变量可以位于内存数据位置。引用的类型是数组、结构体和字符串。
在函数中声明的引用类型不会被覆盖,应该始终指向一个状态变量。
在函数中声明的值类型变量不能被覆盖,也不能存储在存储位置。
映射总是在存储位置声明,这意味着不能在函数内声明它们。映射不能被声明为内存类型。但是,函数中的映射可以引用声明为状态变量的映射。
4、规则4
调用者提供的函数参数始终存储在调用数据位置中。
5、规则5
状态变量,通过另一个状态变量赋值,会创建一个新副本。声明了两个值类型状态变量 stateVar1 和 stateVar2。在 getUInt 函数中,stateVar2 被赋值给 stateVar1。在这个阶段,这两个变量的值都是40。下一行代码将 stateVar2 的值更改为50并返回 stateVar1。返回值为40,说明每个变量保持其自己的独立值,如下图所示:
声明了两个数组类型的状态变量 stateArray1 和 stateArray2。在 getUInt 函数中,stateArray2 被赋值给 stateArray1。在这个阶段,两个变量的值是相同的。下一行代码将 stateArray2 中的一个值更改为5,并返回 stateArray1 数组同一位置的值。返回值为4,说明每个变量保持各自的独立值,如下图所示:
6、规则6
将内存变量的值赋给存储变量时总是会创建一个新副本。
声明一个无符号数固定数组状态变量 stateArray。在 getUInt 函数中定义并初始化位于本地内存的固定数组 localArray。下一行代码将 localArray 赋值给 stateArray。在这个阶段,两个变量的值是相同的。下一行代码将 localArray 中的一个值更改为10,并返回 stateArray1 数组相同位置的元素。返回值为2,说明每个变量保持各自的独立值,如下图所示:
声明一个值类型的状态变量 stateVar 并初始化值为20。在 getUInt 函数中,声明局部变量 localVar 且值为40。在下一行代码中,将局部变量 localVar 的值赋给 stateVar。在这个阶段,这两个变量的值都是40。下一行代码将 localVar 的值更改为50并返回 stateVar。返回值为40,说明每个变量保持各自的独立值,如下图所示:
7、规则7
将状态变量的值赋给内存变量时始终创建一个新副本。声明一个值类型的状态变量 stateVar 并初始化为值20。在 getUInt 函数中,声明类型为 uint 的局部变量并初始化为40。将变量 stateVar 的值赋给变量 localVar。在这个阶段,这两个变量的值都是20。下一行代码将 stateVar 的值更改为50并返回 localVar。返回的值是20,说明每个变量保持各自的独立值,如下图所示:
声明一个固定的状态变量数组 stateArray。在 getUInt 函数中,定义位于本地内存的固定数组 localArray 并使用 stateArray 变量的值进行初始化。在这个阶段,两个变量的值是相同的。下一行代码将 stateArray 中的一个值更改为5,并返回 localArray1 数组中相同位置的元素。返回值为2,说明每个变量保持各自的独立值,如下图所示:
8、规则8
将内存变量赋给内存变量不会创建副本;但是,确实为值类型创建了一个新副本。下图中显示的代码清单说明了内存中的值类型变量是按值复制的。localVar1 的值不受变量 localVar2 的值更改影响:
下图中显示的代码清单说明了内存中的引用类型变量通过引用被复制。otherVar 的值受 someVar 变量的影响:
字面量
Solidity 为变量的赋值提供了字面量。字面量没有名字,它们本身就是值。变量可以在程序执行期间改变它们的值,但是字面量始终保持相同的值。看看下面的各种字面量的例子:
整数字面量的例子是1、10、1 000、-1和-100。
字符串文字的例子是 “Ritesh” 和 “Modi”。字符串字面量可以用单引号或双引号。
地址字面量的例子是:
0xca35b7d915458ef540ade6068dfe2f44e8fa733c
0x1111111111111111111111111111111111111111。
十六进制字面量以前缀 hex 作为关键字。十六进制字面量的一个例子是 hex“1A2B3F”。
Solidity 支持使用点的十进制字面量。例子包括4.5和0.2。
整型
整数有助于将数字存储在合约中。Solidity 提供以下两种类型的整数:
有符号的整数:带符号的整数可以同时具有负值和正值。
无符号整数:无符号整数只能保持正值和零。除正值和零值以外,它们也可以保持负值。
对于每种类型,Solidity 都有多种类型的整数。Solidity 提供了 uint8 类型来表示8位无符号整数,并且以8的倍数表示,直到达到256。总之,可以声明32个不同的具有8的倍数的无符号整数,例如 uint8、uint16、unit24、uint256 位。同样,有符号整数的数据类型也是相同的,如 int8、int16,直到 int256。
根据要求,应选择适当大小的整数。例如,当存储0〜255之间的值时,uint8 是合适的,而存储介于-128〜127 之间则 int8 更合适。对于更高的值,可以使用更大的整数。
有符号和无符号整数的缺省值为零,在声明时它们会自动初始化。整数是值类型;然而,当用作数组时,它们被称为参考类型。
可以对整数执行数学运算,例如加法、减法、乘法、除法、指数、否定、后增量和预增量。下图显示了其中一些示例:
布尔型
像任何编程语言一样,Solidity 提供了一种布尔数据类型。bool 数据类型可用于表示具有二进制结果的场景,例如 true 或 false、1或0等。此数据类型的有效值为 true 和 false。值得注意的是,Solidity 中的布尔不能转换为整数,就像它们在其他编程语言中一样。它是一个值类型,任何赋值给其他的布尔变量都会创建一个新副本。Solidity 中 bool 的默认值为 false。
声明和赋值 bool 数据类型的代码如下:
bool isPaid = true;
可以在合约中修改它,并可用于传入和传出参数以及返回值,如下图所示:
字节数据类型
字节是指8位有符号整数。内存中的所有内容都存储在由二进制值0和1组成的位中。Solidity 还提供字节数据类型以存储二进制格式信息。通常,编程语言只有一种数据类型来表示字节。但是,Solidity 具有多种字节类型。它提供的数据类型范围为 bytes1〜bytes32(含),以根据需要表示不同的字节长度。这些被称为固定大小的字节数组,并被实现为值类型。bytes1 数据类型代表1个字节,bytes2 代表2个字节。字节的默认值是 0x00,并用此值初始化。Solidity 也有一个 byte 类型,它是 bytes1 的别名。
一个字节可以以十六进制格式赋值,如下所示:
bytes1 aa = 0x65;
一个字节可以被赋值为十进制格式的整数值,如下所示:
bytes1 bb = 10;
一个字节可以被赋值为十进制格式的负整数值,如下所示:
bytes1 ee = -100;
一个字节可以赋值为字符值,如下所示:
bytes1 dd = 'a';
在下面的代码片段中,256不适合放入单个字节,需要更大的字节数组:
bytes1 cc = 256;
下图中的代码显示了如何在固定大小的字节数组中存储二进制、正整数和负整数以及字符字面量。
我们还可以对字节数据类型执行按位操作,例如 and、or、xor、not 和左右移位操作:
数组
数组是数据类型,但更具体地说,它们是依赖于其他数据类型的数据结构。数组是指相同类型的数值组。数组有助于将这些值存储在一起,并简化迭代、排序和搜索该组中元素或子元素的过程。Solidity 提供了丰富的数组结构,可以满足不同的需求。
Solidity 中的数组示例如下:
uint [5] intArray
Solidity 中的数组可以是固定的或动态的。
1、固定数组
固定数组是指声明了预定大小的数组。固定数组的例子如下:
固定数组无法使用 new 关键字进行初始化。它们只能以内联方式初始化,如下面的代码所示:
它们也可以稍后在函数中内联初始化,如下所示:
2、动态数组
动态数组是指在声明时没有预定大小的数组,但是,它们的大小是在运行时确定的。看看下面的代码:
动态数组可以内联初始化或使用 new 运算符初始化。可以在声明时初始化,如下所示:
稍后,也可以在函数中以两个不同步骤初始化:
3、特殊数组
Solidity 提供了以下两个特殊数组:
字节数组
字符串数组
字节数组
字节数组是一个动态数组,可以容纳任意数量的字节。它与 byte [] 不同。byte [] 数组每个元素占用32个字节,而字节数组紧紧地将所有字节保存在一起。
字节可以声明为具有初始长度大小的状态变量,如以下代码所示:
这也可以分成与以前讨论的数组类似的以下两条代码行:
字节数组可以直接赋值,如下所示:
此外,如果数据位于存储位置,则可以将值压栈其中,如下面的代码所示:
字节数组还提供读/写长度属性,如下所示:
请看下面的代码:
字符串数组
字符串是基于上一节讨论的字节数组的动态数据类型。它们与附加约束的字节数组非常相似。字符串不能被索引或压栈,也不具有 length 属性。要对字符串变量执行任何这些操作,应首先将其转换为字节,然后在操作后将其转换回字符串。
字符串可以由单引号或双引号内的字符组成。字符串可以直接声明并赋值,如下所示:
它们也可以转换为字节,如下所示:
4、数组属性
数组支持一些基本的属性。在 Solidity 中,由于有多种类型的数组,并非每种类型都支持所有这些属性。
这些属性如下所示:
index:除了字符串类型外,所有类型的数组都支持用于读取单个数组元素的 index 属性。仅动态数组,固定数组和字节类型支持用于写入单个数组元素的 index 属性。字符串和固定大小的字节数组不支持写入。
push:仅动态数组支持此属性。
length:除了字符串类型外,此属性由读取透视图中的所有数组支持。只有动态数组和字节支持修改长度属性。
数组的结构
我们已经简要介绍了结构的主题。结构有助于用户自定义数据结构。结构有助于将不同数据类型的多组变量转换为单一类型。结构不包含任何用于执行的编程逻辑或代码;它仅包含变量声明。结构是引用类型,在 Solidity 中被视为复杂类型。
结构可以定义为状态变量,如下代码所示。定义了一个由 string、uint、bool 和 uint 数组组成的结构体。有两个状态变量。在存储中保存它们。当第一个 stateStructure1 状态变量在声明时被初始化时,另一个 stateStructure1 状态变量留待稍后在函数中初始化。内存位置的局部结构在 getAge 函数中声明并初始化。声明另一个结构,作为指向 stateStructure 状态变量的指针。声明了第三个本地结构,它指向之前创建的 localStructure 本地结构。在先前声明的状态结构被初始化的同时执行 localStructure 属性的改变,并最终返回来自 pointerLocalStructure 的年龄。它返回分配给 localStructure 的新值,如下图所示:
枚举
在本章前面讨论 Solidity 文件的布局时,我们简要地谈到了枚举的概念。枚举是包含一个预定义的常量值列表的值类型。它们通过值传递,每个副本都维护自己的值。不能在函数内声明枚举,并在合约的全局域命名空间内声明。
预定义的常量是连续赋值的,从零开始增加整数值。
下面显示的代码插图声明了一个枚举,该枚举被标识为由五个常量值组成的状态——created、approved、provisioned、rejected 和 deleted。将整数值0、1、2、3、4赋值给它们。
使用初始值 provisioned,创建了一个名为 myStatus 的枚举实例。
returnEnum 函数返回状态并返回整数值。值得注意的是,Web3 和去中心化应用(DApp)不理解合约中声明的枚举。它们将得到一个对应于枚举常量的整数值。
returnEnumInt 函数返回一个整数值。
passByValue 函数展示了枚举实例维护自己的本地副本并且不与其他实例共享。
assignInteger 函数显示了一个例子,其中一个整数被赋值为一个枚举实例的值:
地址
地址是20字节的数据类型。它是为了存储以太坊中的账户地址而特别设计的,其大小为160位或20字节。它可以保存合约账户地址以及外部拥有的账户地址。地址是一种值类型,它被赋值给另一个变量时会创建一个新副本。
地址具有 balance 属性,该属性返回账户可用的以太币数量,并具有一些用于账户间交易以太币和调用合约函数的功能。
它提供以下两个函数来交易以太币:
transfer
send
当向一个账户发送以太币时,更应该选择 transfer 函数而不是send函数。send 函数返回一个布尔值,具体取决于以太币发送是否成功执行,而 transfer 函数引发异常并将以太币返还给调用者。
它还提供了以下三个用于调用合约函数的函数:
Call
DelegateCall
Callcode
映射
映射是 Solidity 中最常用的复杂数据类型之一。映射类似于其他语言中的散列表或字典。它们存储键值对,并允许根据提供的键来检索值。
使用 mapping 关键字声明映射,后跟由=>表示法分隔的键和值的数据类型。映射具有与任何其他数据类型一样的标识符,并且它们可用于访问映射。
一个声明映射的例子如下:
在前面的代码中,uint 数据类型用于存储键而 address 数据类型用于存储值。Names 用作映射的标识符。
虽然它类似于散列表和字典,但 Solidity 不允许迭代映射。如果键已知,则可以检索映射中的值。下一个示例说明如何使用映射。合约中维护有一个 uint 类型的计数器作为映射的键,并且在函数的帮助下存储和检索地址详细信息。
要访问映射中的任何特定值,相关键应与映射名一起使用,如下所示:
要在映射中存储值,请使用以下语法:
如下图:
虽然映射不支持迭代,但有一些方法可以解决这个限制。下一个示例说明了迭代映射的方法之一。请注意,在以太坊的 gas 使用方面,通常应该避免迭代和循环这类昂贵的操作。在此示例中,维护单独的计数器以跟踪映射中存储的条目的数量。此计数器还充当映射中的键。可以构造局部数组以存储来自映射的值。可以使用计数器执行循环,并可以将映射中的每个值提取并存储到本地数组中,如下图所示:
只能将映射声明为 storage 类型的状态变量。不能在函数内将映射声明为内存映射。但是,如果映射引用状态变量中声明的映射,则可以在函数中声明映射,如以下示例所示:
这样的语法是合法的,因为 localNames 映射引用了 Names 状态变量:
也可以使用嵌套映射,即由映射组成的映射。下一个例子说明了这一点。在此示例中,有一个显式的映射,将 uint 映射到另一个映射。子映射存储为第一个映射的值。子映射的键为 address 类型,值为 string 类型。有一个映射标识符,可以使用此标识符访问子映射或内部映射,如以下代码所示:
要向此类嵌套映射添加条目,可以使用以下语法:
这里,accountDetails 是映射标识符,counter 是父映射的键。accountDetails [counter] 映射标识符从父映射中检索值,返回的恰好是另一个映射。将键添加到返回值,我们可以设置内部映射的值。同样,可以使用以下语法检索内部映射的值:
如下图:
以上介绍了 Solidity、Solidity 文件的布局,包括可以在其顶层声明的元素。从布局视角讨论了结构、预编译指令、合约和合同元素。深入讨论了值类型和引用类型以及 int、uint、固定大小的字节数组、字节、数组、字符串、结构、枚举、地址、布尔值和映射等类型,并结合示例进行了详细讨论。
福利时间到!营长免费送书啦!
本期话题:你最喜欢的开发语言是什么?为什么?(50字以上哟)。
营长将从精选留言用户中按留言点赞数抽取排名前5位的幸运者,免费送书一本!
截止时间3月26日(下周二)下午2点!
通过阅读本书,你将:
学习 Solidity 和以太坊的基础知识和基本概念
深入探究Solidity 语言及其独特性
详细了解如何使用Solidity 编写智能合约
学习开发和部署智能合约的主要工具
使用异常处理和错误检查编写防御性代码
了解 Truffle 的基本知识和调试过程
推荐阅读:
马云:蚂蚁金服这样做区块链!
理工男的网红生意, 6000万月活50万条日更的背后, 内容链还能这样操作?
天才程序员: "那些年我偷懒没敲的EOS代码, 让我失去了一切, 如果..."
小米“祭出” AIoT 神器!| 技术头条
硬核接亲!程序员被新娘要求现场写代码,结果万万没想到……
68款大规模机器学习数据集,涵盖CV、语音、NLP | 十年资源集
工作量不断增加的微软Azure,正缩小与亚马逊AWS的差距
曝光!月薪 5 万的程序员面试题:73% 人都做错,你敢试吗?
猛戳"阅读原文"有惊喜哟!
老铁在看了吗??