0%

solidity基础1-3

Solidity 简介

Solidity 是一种用于编写以太坊虚拟机(EVM)智能合约的编程语言。我认为掌握 Solidity 是参与链上项目的必备技能:区块链项目大部分是开源的,如果你能读懂代码,就可以规避很多亏钱项目。

Solidity 具有两个特点:

  1. “基于对象”:学会 Solidity 之后,可以助你在区块链领域找到好工作,挣钱找对象。
  2. “高级”:不会 Solidity,在币圈会显得很 low。

test

开发工具:Remix

本教程中,我们将使用 Remix 运行 Solidity 合约。Remix 是以太坊官方推荐的智能合约集成开发环境(IDE),适合新手,可以在浏览器中快速开发和部署合约,无需在本地安装任何程序。

网址:https://remix.ethereum.org

Remix 中,左侧菜单有三个按钮,分别对应文件(编写代码)、编译(运行代码)和部署(将合约部署到链上)。点击“创建新文件”(Create New File)按钮,即可创建一个空白的 Solidity 合约。

Remix 面板

第一个 Solidity 程序

这个简单的程序只有 1 行注释和 3 行代码:

1
2
3
4
5
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract HelloWeb3{
string public _string = "Hello Web3!";
}

我们拆解程序,学习 Solidity 代码源文件的结构:

  1. 第 1 行是注释,说明代码所使用的软件许可(license),这里使用的是 MIT 许可。如果不写许可,编译时会出现警告(warning),但程序仍可运行。Solidity 注释以“//”开头,后面跟注释内容,注释不会被程序执行。

    1
    // SPDX-License-Identifier: MIT
  2. 第 2 行声明源文件所使用的 Solidity 版本,因为不同版本的语法有差异。这行代码表示源文件将不允许小于 0.8.21 版本或大于等于 0.9.0 的编译器编译(第二个条件由 ^ 提供)。Solidity 语句以分号(;)结尾。

    1
    pragma solidity ^0.8.21;
  3. 第 3-4 行是合约部分。第 3 行创建合约(contract),并声明合约名为 HelloWeb3。第 4 行是合约内容,声明了一个 string(字符串)变量 _string,并赋值为 “Hello Web3!”。

    1
    2
    3
    contract HelloWeb3 {
    string public _string = "Hello Web3!";
    }

后续我们会更详细地介绍 Solidity 中的变量。

编译并部署代码

在 Remix 编辑代码的页面,按 Ctrl + S 即可编译代码,非常方便。

编译完成后,点击左侧菜单的“部署”按钮,进入部署页面。

[Deploy配图

默认情况下,Remix 会使用 Remix 虚拟机(以前称为 JavaScript 虚拟机)来模拟以太坊链,运行智能合约,类似在浏览器里运行一条测试链。Remix 还会为你分配一些测试账户,每个账户里有 100 ETH(测试代币),随意使用。点击 Deploy(黄色按钮),即可部署我们编写的合约。

[_string配图

部署成功后,在下方会看到名为 HelloWeb3 的合约。点击 _string,即可看到 “Hello Web3!”。

总结

本节课程中,我们简要介绍了 SolidityRemix 工具,并完成了第一个 Solidity 程序 —— HelloWeb3。接下来,我们将继续深入学习 Solidity

中文 Solidity 资料推荐

  1. Solidity中文文档(官方文档的中文翻译)
  2. 崔棉大师solidity教程 web3技术教学博主,我看他视频学到了很多

WTF Solidity极简入门: 2. 值类型

Solidity中的变量类型

==主要就是这3种类型==

  1. ==**值类型(Value Type)**:包括布尔型,整数型等等,这类变量赋值时候直接传递数值。==
  2. ==引用类型(Reference Type):包括数组结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。==
  3. ==映射类型(Mapping Type): Solidity中存储键值对的数据结构,可以理解为哈希表==
  4. 函数类型(Function Types) : 代表函数的变量

我们将仅介绍常用类型,不常用的类型不会涉及,本篇将介绍值类型。

值类型

1. 布尔型

布尔型是二值变量,取值为 truefalse

1
2
// 布尔值
bool public _bool = true;

布尔值的运算符包括:

  • ! (逻辑非)
  • && (逻辑与,”and”)
  • || (逻辑或,”or”)
  • == (等于)
  • != (不等于)
1
2
3
4
5
6
// 布尔运算
bool public _bool1 = !_bool; // 取非
bool public _bool2 = _bool && _bool1; // 与
bool public _bool3 = _bool || _bool1; // 或
bool public _bool4 = _bool == _bool1; // 相等
bool public _bool5 = _bool != _bool1; // 不相等

在上述代码中:变量 _bool 的取值是 true_bool1_bool 的非,为 false_bool && _bool1false_bool || _bool1true_bool == _bool1false_bool != _bool1true

值得注意的是:&&|| 运算符遵循短路规则,这意味着,假如存在 f(x) || g(y) 的表达式,如果 f(x)trueg(y) 不会被计算,即使它和 f(x) 的结果是相反的。假如存在f(x) && g(y) 的表达式,如果 f(x)falseg(y) 不会被计算。 所谓“短路规则”,一般出现在逻辑与(&&)和逻辑或(||)中。 当逻辑与(&&)的第一个条件为false时,就不会再去判断第二个条件; 当逻辑或(||)的第一个条件为true时,就不会再去判断第二个条件,这就是短路规则。

2. 整型

整型是 Solidity 中的整数,最常用的包括:

1
2
3
4
// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 正整数
uint256 public _number = 20220330; // 256位正整数

常用的整型运算符包括:

  • 比较运算符(返回布尔值): <=<==!=>=>
  • 算数运算符: +-*/%(取余),**(幂)
1
2
3
4
5
// 整数运算
uint256 public _number1 = _number + 1; // +,-,*,/
uint256 public _number2 = 2**2; // 指数
uint256 public _number3 = 7 % 2; // 取余数
bool public _numberbool = _number2 > _number3; // 比大小

大家可以运行一下代码,看看这 4 个变量分别是多少。

3. 地址类型

地址类型(address)有两类:

  • ==普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。==
  • ==payable address: 比普通地址多了 transfersend 两个成员方法,用于接收转账。==

我们会在之后的章节更加详细地介绍 payable address。

1
2
3
4
5
// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
// 地址类型的成员
uint256 public balance = _address1.balance; // balance of address

4. 定长字节数组

==字节数组分为定长和不定长两种:==

  • ==定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为 bytes1, bytes8, bytes32 等类型。定长字节数组最多存储 32 bytes 数据,即bytes32。==
  • 不定长字节数组: 属于==引用类型==(之后的章节介绍),数组长度在声明之后可以改变,包括 bytes 等。
1
2
3
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];

在上述代码中,MiniSolidity 变量以字节的方式存储进变量 _byte32。如果把它转换成 16 进制,就是:0x4d696e69536f6c69646974790000000000000000000000000000000000000000

_byte 变量的值为 _byte32 的第一个字节,即 0x4d

==表示一个16进制需要4位,一个bytes1是8位,所以一个bytes1可以表示2个16进制位==

5. 枚举 enum

枚举(enum)是 Solidity 中用户定义的数据类型。它主要用于为 uint 分配名称,使程序易于阅读和维护。它与 C 语言 中的 enum 类似,使用名称来代替从 0 开始的 uint

1
2
3
4
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;

枚举可以显式地和 uint 相互转换,并会检查转换的正整数是否在枚举的长度内,否则会报错:

1
2
3
4
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}

enum 是一个比较冷门的变量,几乎没什么人用。

在 Remix 上运行

  • 部署合约后可以查看每个类型的变量的数值:

[2-1.png

  • enumuint 转换的示例:

2-2.png

总结

在这一讲,我们介绍了 Solidity 中值类型,包括布尔型、整型、地址、定长字节数组和枚举。在后续章节,我们将继续介绍 Solidity 的其他变量类型,包括引用类型和映射类型。

Solidity极简入门: 3. 函数类型

solidity官方文档里把函数归到数值类型,但我觉得差别很大,所以单独分一类。我们先看一下solidity中函数的形式:

1
function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]

看着些复杂,咱们从前往后一个一个看(方括号中的是可写可不写的关键字):

  1. function:声明函数时的固定用法,想写函数,就要以function关键字开头。

  2. <function name>:函数名。

  3. (<parameter types>):圆括号里写函数的参数,也就是要输入到函数的变量类型和名字。

  4. {internal|external|public|private}:==函数可见性说明符==,一共4种。没标明函数类型的,==默认==public。合约之外的函数,即”自由函数”,始终具有隐含internal可见性。

    • public: 内部外部均可见。
    • private: 只能从本合约内部访问,继承的合约也不能用。
    • external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)。 ==这里this后文有解释==
    • internal: 只能从合约内部访问,继承的合约可以用。

    Note 1: 没有标明可见性类型的函数,默认为public

    Note 2: public|private|internal 也可用于==修饰状态变量==。 ==public变量会自动生成同名的getter函数,用于查询数值。==

    Note 3: 没有标明可见性类型的==状态变量,默认为internal。==

    ==注意这里状态变量和函数可见性的不一样,默认为只能从合约内部访问==

  5. [pure|view|payable]:==决定函数权限/功能的关键字==。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入ETHpureview的介绍见下一节。

==如果没有写这三个关键字就是可读可写==

  1. [returns ()]:函数返回的变量类型和名称。

到底什么是PureView

我刚开始学solidity的时候,一直不理解pureview关键字,因为别的语言没有类似的关键字。solidity加入这两个关键字,我认为是因为gas fee。合约的状态变量存储在链上,gas fee很贵,如果不改变链上状态,就不用付gas。包含pureview关键字的函数是==不改写链上状态的==,因此用户直接调用他们是不需要付gas的(合约中非pure/view函数调用它们则会改写链上状态,需要付gas)。

==这里可太重要了==

在以太坊中,以下语句被视为修改链上状态:

  1. 写入状态变量。
  2. 释放事件。
  3. 创建其他合约。
  4. 使用selfdestruct.
  5. 通过调用发送以太币。
  6. 调用任何未标记viewpure的函数。
  7. 使用低级调用(low-level calls)。
  8. 使用包含某些操作码的内联汇编。

我画了一个马里奥插画,帮助大家理解。在插画里,我把合约中的状态变量(存储在链上)比作碧池公主,三种不同的角色代表不同的关键字。

WTF is pure and view in solidity?

  • pure,中文意思是“纯”,这 里可以理解为”纯打酱油的”。pure 函数既不能读取也不能写入链上的状态变量。就像小怪一样,看不到也摸不到碧琪公主。
  • view,“看”,这里可以理解为“看客”。view函数能读取但也不能写入状态变量。类似马里奥,能看到碧琪公主,但终究是看客,不能入洞房。
  • pureview 的函数既可以读取也可以写入状态变量。类似马里奥里的 boss,可以对碧琪公主为所欲为🐶。

代码

1. pure 和 view

我们在合约里定义一个状态变量 number,初始化为 5。

1
2
3
4
5
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
uint256 public number = 5;
}

定义一个 add() 函数,每次调用会让 number 增加 1。

1
2
3
4
// 默认function
function add() external{
number = number + 1;
}

如果 add() 函数被标记为 pure,比如 function add() external pure,就会报错。因为 pure 是不配读取合约里的状态变量的,更不配改写。那 pure 函数能做些什么?举个例子,你可以给函数传递一个参数 _number,然后让他返回 _number + 1,这个操作不会读取或写入状态变量。

==我的理解就是,参数或者函数内部定义的变量都不是状态变量,函数结束了,这些变量就消失了,也不上链==

1
2
3
4
// pure: 纯纯牛马
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number + 1;
}

[3-3.png

如果 add() 函数被标记为 view,比如 function add() external view,也会报错。因为 view 能读取,但不能够改写状态变量。我们可以稍微改写下函数,读取但是不改写 number,返回一个新的变量。

1
2
3
4
// view: 看客
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}

3-4.png

==重点来了,view和pure的区别就是view可以读取状态变量,pure不可以读取状态变量,他的输出只能由参数决定。==

2. internal v.s. external

  • external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)。
  • internal: 只能从合约内部访问,继承的合约可以用。
1
2
3
4
5
6
7
8
9
// internal: 内部函数
function minus() internal {
number = number - 1;
}

// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}

==重点来了,也就是带有函数可见性说明符internal的,在部署完之后,不可以调用,因为我们部署完合约之后,在remix里面进行函数调用,查看变量,传参都相当于在合约外部调用,所以想要调用带有internal的函数,只能通过另外一个external函数调用==

我们定义一个 internalminus() 函数,每次调用使得 number 变量减少 1。由于 internal 函数只能由合约内部调用,我们必须再定义一个 externalminusCall() 函数,通过它间接调用内部的 minus() 函数。

3-1.png

3. payable

1
2
3
4
5
// payable: 递钱,能给合约支付eth的函数
function minusPayable() external payable returns(uint256 balance) {
minus();
balance = address(this).balance;
}

我们定义一个 external payableminusPayable() 函数,间接的调用 minus(),并且返回合约里的 ETH 余额(this 关键字可以让我们引用合约地址)。我们可以在调用 minusPayable() 时往合约里转入1个 ETH。

==this关键字代表合约自身的地址。address(this).balance会返回当前合约地址所持有的以太币(ETH)数量,以wei为单位==

==在Solidity中,this关键字代表当前合约的实例。它允许你在合约内部访问合约自身的成员,比如函数和状态变量。this可以在合约的任何函数内使用,但通常在需要引用合约自身的上下文中使用,比如转账操作时指向合约自己的地址(address(this))。这使得合约能够操作自己的属性和调用自己的方法。==

==在Solidity中,this除了用于引用合约自身的实例和获取合约的地址外(address(this)),还可以用来在合约内部调用当前合约的其他公共和外部函数。例如,如果你有一个公共函数functionA,你可以通过this.functionA()来调用它,尤其在需要显式地表明函数调用是在合约内部发生时。这种方式通常用于需要通过合约地址调用函数的场景,确保调用遵循EVM的调用规则,如通过消息调用(message call)。==

==一言以蔽之:在合约内部使用this调用函数,实际上是模拟外部调用者的行为。==

external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.8.0;

contract MyContract {
uint256 public myNumber;

function setMyNumber(uint256 _myNumber) external {
myNumber = _myNumber;
}

function updateMyNumber(uint256 _newNumber) public {
// 使用this关键字调用当前合约的另一个函数(external) 必须使用this
this.setMyNumber(_newNumber);
}
}

mirror-image-1

我们可以在返回的信息中看到,合约的余额变为 1 ETH。

mirror-image-2

3-2.png

总结

在这一讲,我们介绍了 Solidity 中的函数。pureview 关键字比较难理解,在其他语言中没出现过:view 函数可以读取状态变量,但不能改写;pure 函数既不能读取也不能改写状态变量。