值得信赖的区块链资讯!
比推数据  |  比推终端  |  比推英文  |  比推 APP  | 

下载比推 APP

值得信赖的区块链资讯!
iPhone
Android

学习 Solidity——智能合约开发手册(一)

Chainlink预言机

作者:Zubin Pratap (英语)

译者:Frank Kong


本文是《学习 Solidity——智能合约开发手册》的第一部分。


当我在 2018 年从律师转行到软件工程师时,我从未想过我会像现在这样喜欢做开发工作。我也从没想过我最终会为谷歌和 Chainlink Labs 优秀的公司工作。


在从事法律和其他工作 15 年之后,我经历了许多工作、国家、公司和职业道路。它们都无法与我从编码工作中获得的快乐和兴奋相提并论。


不足之处?掌握新的编码技能可能会令人困惑、沮丧且耗时。而且很容易忘记一些微小但重要的细节。


所以我写了这本手册。它旨在让您尽快开始编写 Solidity 代码。它遵循帕累托法则(又名 80/20 法则),手册将会专注于 20% 的信息,而这 20% 的信息将满足你 80% 需求。


作为我在 Chainlink Labs 工作的一部分,我在学习 Solidity 时就开始了解这些概念,并且把它们总结在一起。我在 38 岁的时候转为程序员,这次我也使用了许多当时的自学方法。


这个手册是我经常会用到的参考文件。它旨在为初学者和中级开发人员提供参考,以便快速解答在你深入学习该语言时的问题。


我会不断更新这本手册,但我真的需要你的帮助!如果我需要更新这本手册,请发推给我 @ZubinPratap 告诉我。


我要感谢我出色的同事 Kevin Ryu、Andrej Rakic、Patrick Collins 和 Richard Gottleiber,他们为本手册提供了宝贵的指导和意见。


(译者注:感谢 luojiyin 参与翻译本手册,并提供详细的校对建议。)


目录


  • 这本手册是为谁而写的

  • 必要的前置知识

  • 什么是 Solidity

  • 什么是智能合约

  • 怎样在 Solidity 中声明变量和函数

  • 智能合约中的变量作用域

  • 如何使用可见性标识符(visibility specifier)

  • 什么是构造函数

  • 接口和抽象合约

  • 智能合约案例 #2

  • 什么是合约状态

  • 状态可变性关键字(修饰符:modifier)

  • 数据存储类型 – storage/memory/stack

  • 数据类型原理

  • Solidity 数据类型

  • Solidity 中数组如何声明和初始化数组

  • 函数修饰符(function modifier)是什么

  • Solidity 中的异常处理 – require/assert/revert

  • Solidity 中的继承

  • 继承与构造函数参数

  • Solidity 中的类型转换

  • Solidity 中如何使用浮点数

  • 哈希、ABI 编码(encoding)和解码(decoding)

  • 如何调用合约并且使用 fallback 函数

  • 如何发送和接收 Ether

  • Solidity 库(library)

  • Solidity 中的事件(events)和日志(logs)

  • Solidity 中的时间逻辑

  • 总结和更多资源


这本手册是为谁而写的


本手册适用于有兴趣探索 “Web3”背后的愿景,并希望学习相应的技能以实现该愿景所需的人。


不要死记硬背!阅读它,然后将其用作“参考文件”。当你学习任何一门新语言时,你会发现概念、习语和用法会变得有些混乱,或者你的记忆会随着时间的推移而消失。没关系!这就是本手册旨在帮助你随时查阅到所需的知识。


随着时间的推移,我可能会为此添加一些更高阶的内容,或者创建一个单独的教程。但就目前而言,这本手册将为你提供所需的大部分知识,以开始创建几个 Solidity DApp。


本手册假定你至少有几个月的编程经验,我的意思是至少你用 JavaScript 或 Python 或一些编译语言写过程序(HTML 和 CSS 实际上不是“编程”语言,所以只知道它们是不够的)。


唯一的其他要求是你要有好奇心、坚定不移,不要给自己限定任何的停止学习的时间点。


只要你有一台笔记本电脑和一个可以连接互联网的浏览器,你就可以运行 Solidity 代码。您可以在浏览器中使用 Remix 来编写本手册中的代码。不需要其他 IDE!


必要的前置知识


我还假设你了解区块链技术的基础知识,尤其是以太坊的基础知识以及什么是智能合约,提示:智能合约是在区块链上运行的程序,因此具备信任最小化(Trust-minimized)的优势!


虽然你不太可能需要他们来理解本手册,但实际上,拥有像 Metamask 这样的浏览器钱包并了解以太坊合约账户和外部账户(EOA 账户)之间的区别将帮助你充分利用这本手册。


什么是 Solidity


现在,让我们开始了解什么是 Solidity。Solidity 是一种受 C++、JavaScript 和 Python 影响的面向对象的编程语言。


Solidity 旨在编译(从人类可读代码转换为机器可读代码)以太坊虚拟机 (EVM) 上运行的字节码。这是 Solidity 代码的运行时环境,就像你的浏览器是 JavaScript 代码的运行时环境一样。


所以,你通过 Solidity 编写智能合约,编译器将其转换为字节码。然后该字节码被部署并存储在以太坊(以及其他 EVM 兼容的区块链)上。


你可以在我制作的这个视频 中找到对 EVM 和字节码的基本介绍。


什么是智能合约


这是一个开箱即用的简单智能合约。它可能看起来没什么用,但你将从中了解很多 Solidity 知识!


请先连同每条评论一起阅读,以了解合约在做什么,然后继续学习一些关键知识。

// SPDX-License-Identifier: MITpragma solidity ^0.8.8.0;
contract HotFudgeSauce { uint public qtyCups;
// 获得当前 hot fudge 的数量 function get() public view returns (uint) { return qtyCups; }
// 将 hot fudge 的数量加一的函数 function increment() public { qtyCups += 1; // 与 qtyCups = qtyCups + 1; 一样 }
// 将 hot fudge 的数量减一的函数 function decrement() public { qtyCups -= 1; // 与 qtyCups = qtyCups - 1; 一样 // 当qtyCups = 0 时函数被调用会发生什么? }}


我们将很快了解一些细节,例如 `public` 和 `view` 的含义。


现在,从上面的例子中学习七个关键知识:

  1. 第一个注释是机器可读行 (// SPDX-License-Identifier: MIT),它指定了所包含的代码的许可。

    强烈建议使用 SPDX 许可证标识符,尽管你的代码在没有它的情况下也能编译。在这里阅读更多。此外,你也可以添加注释或“注释掉”任何一行,方法是在其前面加上两个正斜杠“//”。

  2. 任意一个 Solidity 文件中,pragma 指令必须是在代码的第一行。Pragma 是一个指令,它告诉编译器应该使用哪个编译器版本将人类可读的 Solidity 代码转换为机器可读的字节码。

    Solidity 是一门新语言,更新频率很高,所以不同版本的编译器在编译代码时会产生不同的结果。当使用较新的编译器版本编译时,一些较旧的 solidity 文件会抛出错误或警告。

    在较大的项目中,当你使用像 Hardhat 这样的工具时,可能需要指定多个编译器版本,因为导入的 solidity 文件或你依赖的库是为旧版本的 solidity 编写的。在此处阅读有关 Solidity 的 pragma 指令的更多信息。

  3. pragma 指令遵循语义化版本控制 (SemVer),SemVer 是一个系统,其中每个数字表示该版本中包含的更改的类型和范围。如果你想要 SemVer 的实际操作解释,请查看本教程,SemVer 非常有助于理解,并且如今在开发(尤其是 Web 开发)中得到广泛使用。

  4. 分号在 Solidity 中是必不可少的。即使缺少一个,编译器也会失败。Remix 会提醒你!

  5. 关键字 contract 告诉编译器你正在声明一个智能合约。如果你熟悉面向对象编程,那么你可以将契约视为类。

    如果你不熟悉 OOP,那么可以将合约视为保存数据的对象——包括变量和函数。你可以通过智能合约为区块链应用程序提供所需的功能。

  6. 函数是封装单个想法、特定功能、任务等的可执行代码单元。通常我们希望函数一次只做一件事。

    尽管函数可以在智能合约代码块之外的文件中声明,当时它们通常还是出现在智能合约中。函数可以接受 0 个或多个参数,也可以返回 0 个或多个值。输入和输出是静态类型的,这是你将在本手册稍后部分了解的概念。

  7. 在上面的例子中,变量 qtyCups 被称为“状态变量”。它保存了合约的状态——这里的状态指的是程序需要跟踪运行的数据。

    与其他程序不同,智能合约应用程序即使在程序未运行时也会保持其状态。数据与应用程序一起存储在区块链中,这意味着区块链网络中的每个节点都在本地副本维护和同步数据和智能合约。

    状态变量就像传统应用程序中的数据库“存储”,但由于区块链需要在网络中的所有节点之间同步状态,因此使用存储可能非常昂贵!稍后会详细介绍。



怎样在 Solidity 中声明变量和函数


让我们分解一下 HotFudgeSauce 智能合约,以便我们更多地了解这个合约中的内容。


在 Solidity 中定义事物的基本结构/语法类似于其他静态类型语言。函数和变量都有名字。


但是在类型化语言中,我们还需要为创建、输入或输出返回的数据指定数据类型。如果你需要了解什么是类型化数据,可以跳到本手册的类型化数据部分。


下面,我们将看到如何声明“状态变量”。还可以看到如何声明函数。


图片


第一个部分声明了一个名为 qtyCups 的状态变量(我很快会解释这是什么)。这只能存储 uint 类型的值,这意味着无符号整数。“整数”是指零以下(负)和零以上(正)的所有整数。


由于这些数字附有 + 或 – 符号,所以称为有符号整数。无符号整数始终是正整数(包括零)。


在第二个片段中,我们在声明函数时也看到了一个熟悉的结构。最重要的是,我们看到在函数中,必须为其返回的值指定数据类型。


在这个例子中,由于 get() 返回我们刚刚创建的存储变量的值,我们可以看到返回值必须是一个 uint。


public 是可见性标识符。稍后会详细介绍。view 是一个状态可变性修饰符,也会在后面的内容中介绍。


这里值得注意的是,状态变量也可以是其他类型——constant 和 immutable。它们是这样的:


string constant TEXT = "abc";address immutable owner = 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e;


constant 和 immutable 变量的值可以并且仅可以被赋予一次。在赋予了第一个值后,不能再给它们赋另一个值。


因此,如果我们将 qtyCups 状态变量设为 constant 或 immutable,我们将无法再对其调用 increment() 或 decrement() 函数(事实上,调用的话代码将无法编译!)。


constant 的值必须在代码本身中硬编码(hardcode),而immutable 变量可以将它们的值设置一次,通常是通过构造函数中的赋值(我们很快就会讨论构造函数)。你可以在此处的文档中阅读更多内容。


智能合约中的变量作用域


智能合约中的变量有 3 个作用域:

  1. 状态变量:通过将值记录在区块链上,在智能合约中存储永久数据(称为持久状态)。

  2. 局部变量:这些是“暂时性”数据,在运行计算时会在短时间内保存信息。这些值不会永久存储在区块链上。

  3. 全局变量:这些变量和函数由 Solidity“注入”到您的代码中,无需专门创建或从任何地方导入它们即可使用。这些提供了代码运行时的区块链环境信息,还包括程序中会用到的功能性函数。


你可以按如下方式区分变量作用域:

  1. 状态变量通常位于智能合约内部,但位于函数外部。

  2. 局部变量位于函数内部,不能从该函数之外访问。

  3. 全局变量不是由你声明的,当时它们“神奇地”可供你使用。


这是我们的 HotFudgeSauce 示例,稍作修改以显示不同类型的变量。我们给 qtyCups 一个初始值,然后给除了我以外的每个人分一杯 Fudge Sauce(因为我正在节食)。


图片


如何使用可见性标识符

(visibility specifier)


“可见性”这个词的使用有点令人困惑,因为在公共区块链上,几乎所有东西都是“可见的”,因为透明度是一个关键特征。这里的可见性意味着一段代码可以被另一段代码看到和访问的能力。


可见性指定变量、函数或智能合约可以从定义它的代码所在的区域之外访问的程度。可以根据整个系统中的哪些部分需要访问它来调整其可见范围。


如果你是 JavaScript 或 NodeJS 开发人员,那么你已经熟悉可见性——你导出一个对象的时,就是为了使它在声明它的文件之外可见。


可见度类型


在 Solidity 中有 4 种不同类型的可见性:public、external、internal 和 private。


Public 函数和变量可以在合约内部、外部、其他智能合约和外部账户(你 Metamask 钱包中的那种)访问——几乎可以从任何地方访问。这是最广泛、最宽松的可见性级别。


当一个存储变量被赋予 public 可见度时,Solidity 会自动为该变量的值创建一个隐性的 getter 函数。


所以在我们的 HotFudgeSauce 智能合约中,我们不需要 get() 方法,因为 Solidity 会隐式地为我们提供完全一样的功能,只需给 qtyCups 一个 public 可见度修饰符。


Private 函数和变量只能在声明它们的智能合约中访问。但是它们不能在包含它们的智能合约之外访问。private 是四个可见性说明符中限制性最强的。


Internal 可见性类似于 private 可见性,因为内部函数和变量只能从声明它们的合约中访问。但是标记为 internal 的函数和变量也可以从派生合约(即从声明合约继承的子合约)访问,但不能从合约外部访问。稍后我们将讨论继承(和派生/子合约)。


状态变量的默认可见度就是 internal。


图片

4 种可见度标识符的表格


external 可见性说明符不适用于变量 – 只有函数可以指定为 external。


external 函数不能从声明合约或继承自声明合约的合约的内部调用。因此,它们只能从该合约之外调用。


这就是它们与公共函数的不同之处——公共函数也可以从声明它们的合约内部调用,而外部函数则不能。


什么是构造函数


构造函数是一种特殊类型的函数。在 Solidity 中,它是可选的,仅在合约创建时执行一次。


在下面的示例中,我们有一个显式构造函数,它接受一些数据作为参数。你必须在创建智能合约时将此构造函数参数注入到这个智能合约中。


图片

有入参的 Solidity 构造函数


要了解构造函数何时被调用,先了解智能合约创建的几个阶段:

  • 它被编译成字节码(你可以在这里阅读更多关于字节码的信息)。这个阶段称为“编译时间”。

  • 它被创建(构造) – 这是构造函数开始运行的时候。这可以称为“构造时间”。

  • 然后字节码被部署到区块链。这就是“部署”。

  • 部署的智能合约字节码在区块链上运行(执行)。这可以被认为是“运行时”。


在 Solidity 中,与其他语言不同,程序(智能合约)仅在构造函数完成其创建智能合约的工作后才会部署。


有趣的是,在 Solidity 中,最终部署的字节码并不包含构造函数代码。这是因为在 Solidity 中,构造函数代码是创建代码(构造时间)的一部分,而不是运行时代码的一部分。它在创建智能合约时用完了,因为它只会调用一次,在这个阶段过去后,就不需要被调用了,所以不回在最终部署的字节码中。


因此,在我们的示例中,构造函数创建(构造)Person 智能合约的一个实例。我们的构造函数希望我们将一个名为 _name 的字符串值传递给它。


当构建智能合约时,_name 的值将存储在名为 name 的状态变量中(这通常也是将配置信息和其他数据传递到智能合约的方式)。然后当实际部署合约时,状态变量 name 将保存我们传递给构造函数的任何字符串值。


理解为什么这样设计构造函数


你可能想知道为什么我们费心将值注入构造函数。为什么不把它们写进合约呢?


这是因为我们希望合约是可配置的或“参数化的”。我们想要的不是将一个值硬编码(把值写死),而是在需要时注入合适的数据,这样才能给合约带来的灵活性和可重用性。


在我们的示例中,假设 _name 指的是将要部署合约的以太坊网络的名称(如 Rinkeby、Goerli、Kovan、Mainnet 等)。


我们如何将这些信息提供给智能合约?将所有这些值都放入其中会很浪费。这也意味着我们需要添加额外的代码来确定合约在哪个区块链上运行。然后我们必须从我们存储在合约中的硬编码列表中选择正确的网络名称,这将会在部署时使用更多的 gas。


相反,我们可以在将智能合约部署到相关区块链网络时将其注入构造函数。这样,我们就可以编写一个可以使用任意数值作为参数的合约。


另一个常见的用例是当你的智能合约继承自另一个智能合约,并且你需要在创建合约时将值传递给父智能合约。但是继承是我们后面要讨论的。


我提到构造函数是可选的。在 HotFudgeSauce 中,虽然我们没有编写显式构造函数,但是 Solidity 支持隐式构造函数。因此,如果我们不在智能合约中包含构造函数,Solidity 将假定一个默认构造函数,看起来像 constructor() {}。


如果你评估一下它的作用,你会发现它其实什么都不做,这就是为什么它可以被隐藏(被隐式创建)并且让编译器使用默认构造函数。


接口和抽象合约


solidity 中的接口是一个需要理解的基本概念。以太坊上的智能合约是公开可见的,因此你可以通过它们的函数与它们进行交互(在可见度标识符允许的范围内!)。


这就是使智能合约“可组合”的原因,也是为什么如此多的 Defi 协议被称为“金钱乐高”——你可以编写与其他智能合约交互的智能合约,这些智能合约又与其他智能合约交互等等……现在你明白它的意思了。


当你想让智能合约 A 与智能合约 B 进行交互时,你需要 B 的接口。接口为你提供了各种函数的索引,你可以使用这些函数调用某个的智能合约。


接口的一个重要特征是它们不能对定义的任何函数有任何实现(代码逻辑)。接口只是函数名称及其预期参数和返回类型的集合。它们并不是 Solidity 独有的概念。


因此,我们的 HotFudgeSauce 智能合约的接口看起来像这样(请注意,按照惯例,solidity 接口的命名方式是在智能合约的名称前加上“I”前缀((就变成了 IHotFudgeSauce)):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
interface IHotFudgeSauce { function get() public view returns (uint); function increment() public; function decrement() public;}


就是这样!由于 HotFudgeSauce 只有三个函数,因此界面仅显示这些函数。


但这里有一个值得注意的点:接口并不需要包含智能合约中可调用的所有函数,所以你可以删掉一些不必要的函数,让接口包含打算调用的函数的定义就可以!


因此,如果你只想在 HotFudgeSauce 上使用 decrement() 方法,那么你完全可以从接口中删除 get() 和 increment() – 但你也将无法调用合约中的这两个函数。


那么到底发生了什么?好吧,接口只是让你的智能合约知道在你的目标智能合约中可以调用哪些函数,这些函数接受哪些参数(及其数据类型),以及你可以期望返回什么类型的数据。在 Solidity 中,这就是你与另一个智能合约交互所需的全部信息。


在某些情况下,你可以拥有一个类似于但又不同于接口的概念 — 抽象合约(abstract contract)。


抽象合约是使用 abstract 关键字声明的,合约中声明了一个或多个函数但未实现的函数。或者使用另一种定义,至少有一个函数已声明但未实现。


反过来说,抽象合约可以有其实现的函数(不像接口不能有函数实现),但只要有一个函数未实现,合约就必须标记为抽象的:

// SPDX-License-Identifier: MITpragma solidity ^0.8.7;
abstract contract Feline { int public age;
// not implemented. function utterance() public virtual returns (bytes32);
// implemented. function setAge(int _age) public { age = _age; }}


你可能(合理地)想知道这有什么意义。好吧,抽象合约不能直接实例化(创建)。它们只能被继承,继承它的合约可以使用它的函数。


因此,抽象合约通常被用作其他智能合约可以“继承”的模板或“基础合约”,从而迫使继承的智能合约实现抽象(父)合约声明的某些函数。这是在很多情况下的一种很有用的设计模式,即在相关合约之间强制统一结构。


当我们稍后讨论继承时,你会对继承相关的知识更清晰。现在,请记住,你可以声明一个不实现其所有函数的抽象智能合约——但如果你这样做,你将无法实例化它,而未来继承它的智能合约必须完成实现那些未实现函数。


接口和抽象合约之间的一些重要区别是:

  • 接口中不能有实现的函数,而抽象合约可以有任意数量的函数实现,但是至少有一个函数是“抽象的”(即未实现)。

  • 接口中的所有函数都必须标记为 “external”,因为它们只能由实现该接口的其他合约调用。

  • 接口不能有构造函数,而抽象合约可以有。

  • 接口不能有状态变量,抽象合约可以有。


智能合约实例#2


对于接下来的几个 Solidity 概念,我们将使用下面的智能合约。因为这个例子包含了一个在现实世界中实际使用的智能合约,我选择它也是因为我对 Chainlink Labs 有明显的偏好,因为我在那里工作而且 Chainlink Labs 是一家很棒的公司。但这也是我学到很多 Solidity 的地方,除此以外,通过真实世界的例子学习会更好。


因此,请先阅读下面的代码和评论。如果你仔细阅读,你已经了解了理解下面合约所需的 99%。然后我们继续从这份合约中学关键知识。

// SPDX-License-Identifier: MITpragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumerV3 { AggregatorV3Interface internal priceFeed;
/** * Network: Goerli * Aggregator: ETH/USD * Address: 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e */ constructor() { priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e); }
/** * Returns the latest price */ function getLatestPrice() public view returns (int) { ( /*uint80 roundID*/, int price, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = priceFeed.latestRoundData(); return price; }}


该智能合约从实时运行 Chainlink 价格馈送预言机(参见 etherscan 上的预言机)获取 1 ETH 最新的美元价格。该示例使用 Goerli 网络,因此你不会在以太坊主网上花费真钱。


现在,你需要了解 6 个基本的 Solidity 概念:

  1. 在 pragma 语句之后我们有一个 import 语句。这会将现有代码导入我们的智能合约。

    这非常有用,因为这是我们重用他人编写的代码并从中受益的方式。你可以查看在此 GitHub 链接上导入的代码。

    实际上,当我们编译我们的智能合约时,这个导入的代码会被拉入并与它一起编译成字节码。我们马上就会明白为什么我们需要它……

  2. 单行注释是用//标记的。现在你正在学习多行注释。它们可能跨越一行或多行并使用 /* 和 */ 开始和结束注释。

  3. 我们声明了一个名为 priceFeed 的变量,它的数据类型为 AggregatorV3Interface。但是这种奇怪的类型是从哪里来的呢?从导入语句中导入的代码中——我们能够使用 AggregatorV3Interface 类型,因为 Chainlink 定义了它。

    如果你查看 Github 链接,你会看到该类型定义了一个接口(我们刚刚讨论完接口)。所以 priceFeed 是对 AggregatorV3Interface 类型的某个对象的引用。

  4. 看一下构造函数。这个构造函数不接受参数,但我们可以很容易地将 ETH/USD 喂价(price feed)的 oracle 智能合约的地址 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e 作为地址类型的参数传递给它。相反,我们在构造函数中对地址进行硬编码。

    但我们也正在创建对 Price Feed Aggregator 智能合约的引用(使用称为 AggregatorV3Interface 的接口)。

    现在我们可以调用 AggregatorV3Interface 上可用的所有方法,因为 priceFeed 变量引用该智能合约。事实上,我们接下来要做的是……

  5. 让我们跳转到函数 getLatestPrice()。你可以从我们在 HotFudgeSauce 中的讨论中认出它的结构,这个函数正在做一些有趣的事情。

    在这个 getLatestPrice() 函数中,我们调用了存在于 AggregatorV3Interface 接口中的 latestRoundData() 函数。如果你查看此方法的源代码,你会注意到此 latestRoundData() 函数返回 5 种不同类型的整数!


图片


通过我们的智能合约调用其他智能合约的方法


在我们的智能合约中,我们注释掉了 4 个不需要的值。所以这意味着 Solidity 函数可以返回多个值(在这个例子中我们返回了 5 个值),所以我们可以挑选需要的。


另一种使用调用 latestRoundData() 的结果的方法是:( ,int price, , ,) = priceFeed.latestRoundData(),对于 5 个返回值中的 4 个,我们不给它们变量名以忽略它们。


当我们将变量名分配给一个函数返回的一个或多个值时,我们称之为“解构赋值(destructuring assignment)”,因为我们解构返回值(将每个值分开)并在解构时对它们赋值,就像我们对上面的 price 所做的那样。


由于你已经了解了接口,我建议你查看 Chainlink Labs 的 GitHub repo 以检查 Aggregator 合约中已实现的 latestRoundData() 函数以及 AggregatorV3Interface 如何提供与 Aggregator 合约交互的接口。


什么是合约状态


在我们继续之前,重要的是要确保你理解将经常要看到的术语。


计算机科学中的“状态(state)”具有明确的含义。虽然它会变得非常混乱,但状态的关键在于它指的是程序在运行时“记住”的所有信息。此信息可以改变、更新、删除、创建等。而且,如果你在不同时间为其快照,信息将处于不同的“状态”。


所以状态只是程序的当前快照,在其执行期间的某个时间点 – 它的变量持有什么值,它们在做什么,已经创建或删除了哪些对象,等等。


我们之前已经研究了三种类型的变量——状态变量、局部变量和全局变量。状态变量以及全局变量为我们提供了智能合约在任何给定时间点的状态。因此,智能合约的状态是对以下内容的描述:

  1. 它的状态变量持有什么值,

  2. 区块链相关的全局变量在那个时刻有什么值,以及

  3. 智能合约账户余额(如果有的话)。


状态可变性关键字

(修饰符:modifier)


现在我们已经讨论了状态、状态变量和函数,让我们了解一些 Solidity 关键字,这些 Solidity 关键字指定了我们可以对状态执行的操作。


这些关键字称为修饰符。但并非所有这些都允许你修改状态。事实上,其中许多修饰符明确禁止你修改状态。


以下是你将在真正的智能合约中看到的 Solidity 修饰符:


修饰符关键字
适用于…
目的
constant
状态变量

声明的同时进行赋值。硬编码成代码,它给定的值永远不能改变。


当我们知道一个值永远不会改变时使用 – 例如,如果我们永远不会(永远)允许用户购买超过 50 单位的东西,我们可以将 50 声明为常量值(constant)。


immutable
状态变量
这些状态变量是在智能合约的顶部声明的,但在构造时给它们赋值(仅一次!)——即通过构造函数。一旦他们收到值,实际上就变成常量了。并且它们的值实际上存储在代码本身而非存储槽(storage slot)中(storage 将在后面解释)。
view 函数
A你通常会在可见性说明符之后看到它。view修饰符意味着该函数只能“查看”(读取)合约状态,但不能更改它(不能“写入”合约状态)。这实际上是一个只读修饰符。如果该函数需要使用合约状态中的任何值,但不修改该值,则它将是一个 view 函数。
pure 函数 pure 函数不允许写入(修改)合约状态,甚至不允许从中读取!他们做的事情不会以任何方式与区块链状态交互。 通常这些可以是辅助函数,可以进行一些计算或将一种数据类型的输入转换为另一种数据类型等。
payable
函数
该关键字使函数能够接收 Eth。没有这个关键字,你就不能在调用函数时发送 ETH。

请注意,在 Solidity 版本 0.8.17 中,有 重大更新 允许使用 `payable` 作为数据类型。 具体来说,我们现在可以将 address 数据类型转化为 payable address 数据类型,方式是通过执行类型转换,执行代码类似于 payable(0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF).
这样做的目的是使给定的以太坊地址成为可支付的,之后我们可以将 ETH 发送到该地址。

请注意,payable 的这种用法是一种类型转换,与函数修饰符不同,尽管使用了相同的关键字。稍后我们将介绍地址类型,但是你可以先阅读相关内容在此处.
virtual
函数
这是一个稍微高级的话题,在继承部分有详细介绍。这个修饰符允许函数在从它继承的子合约中被“覆盖”。 换句话说,一个带有关键字 virtual 的函数可以在另一个继承自这个合约的子合约中用不同的内部逻辑“重写”。
override
函数
这是 virtual 修饰符的反面。 当子合约“重写”在它继承的基础合约(父合约)中声明的函数时,它会用 override 来标记重写的函数,以表明其实现覆盖了父合约中给出的函数。如果父合约的 virtual 函数没有被子合约覆盖,则父合约的逻辑将应用于子合约。
indexed 事件
我们将在本手册后面介绍事件(event)。它们是由智能合约“发出”的小数据包,通常是为了响应发生的值得注意的事件。 indexed 关键字表示事件中包含的其中一条数据应存储在区块链中,以供以后检索和过滤。一旦我们在本手册后面了解了事件和日志记录,你就会了解它的意义。
anonymous
事件
这里的 文档 说“不将 event signature 存储为 topic”,这对你来说可能意义不大。但是关键字确实表明它正在使 event 的某些部分“匿名”。因此,一旦我们理解了本手册后面的 event 和 topic,你就知道它的意义了。


请注意,不是存储变量的变量(即在给定函数范围内声明和使用的局部变量)不需要状态修饰符。这是因为它们实际上并不是智能合约状态的一部分。它们只是该函数内部局部状态的一部分。那么根据定义,它们是可修改的,不需要对其可修改性进行控制。


数据存储类型 – storage/memory/stack


在以太坊和基于 EVM 的链上,系统内的数据可以在多个“数据位置”存储以被访问。


数据存储位置是 EVM 基本设计和架构的一部分。当你看到 “memory”、“storage” 和 “stack” 等词时,你应该开始思考“数据存储位置”——即数据可以存储(写入)和从中检索(读取)的位置。


数据位置会影响代码在运行时的执行方式。除此以外,它对智能合约在部署和运行期间使用的 gas 数量也有非常重要的影响。gas 的使用需要对 EVM 和称为操作码(opcode)的东西有更深入的了解——我们可以暂时搁置这个讨论。虽然有用,但并不是你了解数据存储位置的充分条件。


虽然到目前为止我已经提到了 3 个 数据位置 ,但还有 2 种其他方式可以在智能合约中存储和访问数据:“calldata” 和 “code”。但这些不是 EVM 设计中的数据位置。它们只是提到过的 3 个数据位置的子集。


让我们从 storage 开始。在 EVM 的设计中,需要永久存储在区块链上的数据被放置在相关智能合约的“storage”区域。这包括任何合约“状态变量”。


由于存储将数据永久保存在区块链上,因此所有数据都需要在网络中的所有节点之间同步,这就是节点必须就数据状态达成共识的原因。这种共识使存储使用起来很昂贵。


你已经看到了存储变量(也称为合约状态变量)的示例,但这里是取自 Chainlink VRF(可验证随机数)的 consumer 智能合约的示例


图片

Storage 数据。将数据放入合约的存储布局中。


创建和部署上述合约时,传递给合约构造函数的任何地址都会永久存储在智能合约的 storage 中,并且可以使用变量 vrfCoodinator 访问。由于此状态变量被标记为immutable,因此在此之后无法更改。


回忆一下上一节关于关键字,我们在上一节讨论了 immutable 变量和 constant 变量,这些值没有放在 storage 里面。在构造合约时,它们成为代码本身的一部分,因此这些值不会像 storage 变量那样消耗那么多的 gas。


现在让我们看 memory。这表示临时存储,你可以在其中读取和写入智能合约运行期间所需的数据。一旦使用该数据的函数执行完毕,该数据将被擦除。


memory 位置空间就像一个临时记事本,每次触发函数时都会在智能合约中提供一个新的,执行完成后,该临时记事本将被删除。


在理解 storage 和 memory 的区别时,您可以将 storage 视为传统计算世界中的一种硬盘,因为它具有“持久”存储数据的意义。但 memory 在传统计算中更接近 RAM。


堆栈(stack)是执行大部分 EVM 计算的数据区域。EVM 遵循基于 stack 的计算模型,而不是基于寄存器的计算模型,这意味着要执行的每个操作都需要使用stack 数据结构进行存储和访问。


stack 的深度——即它可以容纳的项目总数——是 1024,stack 中的每个项目可以是 256 位(32 字节)长。这与存储数据位置中每个键和值的大小相同。


你可以在此处详细了解 EVM 如何控制对 stack 数据存储区域的访问。


接下来说说 calldata。我假设你对以太坊智能合约消息和交易有基本的了解。如果你没有,请先阅读这些链接。


消息和交易是调用智能合约函数的方式,它们包含执行这些函数所需的各种数据。此消息数据存储在 calldata 中,calldata 是 memory 只读部分,其中包含函数名称和参数等内容。


这与外部可调用函数相关,因为 internal 函数和 private 函数不使用 calldata。calldata 仅存储要被“传入”函数执行的数据和函数参数。


请记住,calldata 是内存,只是 calldata 是只读的。你不能向其中写入数据。


最后,代码(code)不属于以上的任何一个存储类型,而是指智能合约的编译字节码,它被永久部署和存储在区块链上。该字节码存储在不可变的 ROM(只读存储器)中,其中加载了要执行的智能合约的字节码。


还记得我们如何讨论 Solidity 中 immutable 变量和 constant 变量之间的区别吗?immutable 值被赋值一次(通常在构造函数中),constant 变量的值被硬编码到智能合约代码中。因为它们是硬编码的,常量值按字面编译并直接嵌入到智能合约的字节码中,并存储在这个代码/ROM 中。


和 calldata 一样,code也是只读的——如果你理解了上一段,你就会明白为什么!


说明:比推所有文章只代表作者观点,不构成投资建议
原文链接:https://www.bitpush.news/articles/3735703

下载比推 APP

24 小时追踪区块链行业资讯、热点头条、事实报道、深度洞察。

邮件订阅

金融科技决策者们都在看的区块链简报与深度分析,「比推」帮你划重点。