【区块链】Solidity学习

1 Hello Web3

开发工具:Remix

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

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

在 Remix 中,左侧菜单有按钮,对应文件(编写代码)、编译(运行代码)和部署(将合约部署到链上)。

点击“创建新文件”(Create New File)按钮,即可创建一个空白的 Solidity 合约。

将文件命名为HelloWeb3.sol

第一个 Solidity 程序

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

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

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

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

编译并部署代码

扫描二维码关注公众号,回复: 17535892 查看本文章

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

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

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

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

2 值类型

  1. 值类型(Value Type) :包括布尔型,整数型等等,这类变量赋值时候直接传递数值。

  2. 引用类型(Reference Type) :包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。

  3. 映射类型(Mapping Type) : Solidity中存储键值对的数据结构,可以理解为哈希表

1. 布尔型

布尔型是二值变量,取值为 true 或 false

// 布尔值
bool public _bool = true;

布尔值的运算符包括:

  • ! (逻辑非)
  • && (逻辑与,"and")
  • || (逻辑或,"or")
  • == (等于)
  • != (不等于)

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

2. 整型

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

// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 无符号整数
uint256 public _number = 20220330; // 256位无符号整数

常用的整型运算符包括:

  • 比较运算符(返回布尔值): <=, <==, !=, >=, >
  • 算术运算符: +, -, *, /, %(取余),**(幂)

3. 地址类型

地址类型(address)有两类:

  • 普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
  • payable address: 比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。
// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额

4. 定长字节数组

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

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

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

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

5. 枚举 enum

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

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

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

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

在 Remix 上运行

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

enum 和 uint 转换的示例:

3 函数

我们先看一下 Solidity 中函数的形式(方括号中的是可写可不写的关键字):

function <function name>([parameter types[, ...]]) {internal|external|public|private} [pure|view|payable] [virtual|override] [<modifiers>]
[returns (<return types>)]{ <function body> }
  1. ([parameter types[, ...]]):圆括号内写入函数的参数,即输入到函数的变量类型和名称。
  2. {internal|external|public|private}:函数可见性说明符,共有4种。

    • public:内部和外部均可见。
    • private:只能从本合约内部访问,继承的合约也不能使用。
    • external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
    • internal: 只能从合约内部访问,继承的合约可以用。

    注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。

    注意 2public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal

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

  4. [virtual|override]: 方法是否可以被重写,或者是否是重写方法。virtual用在父合约上,标识的方法可以被子合约重写。override用在自合约上,表名方法重写了父合约的方法。

  5. <modifiers>: 自定义的修饰器,可以有0个或多个修饰器。

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

到底什么是 Pure 和View

刚开始学习 solidity 时,pure 和 view 关键字可能令人费解,因为其他编程语言中没有类似的关键字。solidity 引入这两个关键字主要是因为 以太坊交易需要支付气费(gas fee)。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas。包含 pure 和 view 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。

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

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

将合约中的状态变量(存储在链上)比作碧琪公主,三种不同的角色代表不同的关键字。

代码

1. pure 和 view

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

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

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

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

}

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

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

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

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

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

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

2. internal v.s. external

// internal: 内部函数
function minus() internal {
    number = number - 1;
}

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

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

4 函数输出

返回值:return 和 returns

Solidity 中与函数输出相关的有两个关键字:returnreturns。它们的区别在于:

  • returns:跟在函数名后面,用于声明返回的变量类型及变量名。
  • return:用于函数主体中,返回指定的变量。
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
    return(1, true, [uint256(1),2,5]);
}

在上述代码中,我们利用 returns 关键字声明了有多个返回值的 returnMultiple() 函数,然后我们在函数主体中使用 return(1, true, [uint256(1),2,5]) 确定了返回值。

这里uint256[3]声明了一个长度为3且类型为uint256的数组作为返回值。因为[1,2,3]会默认为uint8(3),因此[uint256(1),2,5]中首个元素必须强转uint256来声明该数组内的元素皆为此类型。数组类型返回值默认必须用memory修饰.

命名式返回

我们可以在 returns 中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些变量的值,无需使用 return

// 命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
    _number = 2;
    _bool = false;
    _array = [uint256(3),2,1];
}

在上述代码中,我们用 returns(uint256 _number, bool _bool, uint256[3] memory _array) 声明了返回变量类型以及变量名。这样,在主体中只需为变量 _number_bool_array 赋值,即可自动返回。

当然,你也可以在命名式返回中用 return 来返回变量:

// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
    return(1, true, [uint256(1),2,5]);
}

解构式赋值

Solidity 支持使用解构式赋值规则来读取函数的全部或部分返回值。

  • 读取所有返回值:声明变量,然后将要赋值的变量用,隔开,按顺序排列。

  • uint256 _number;
    bool _bool;
    uint256[3] memory _array;
    (_number, _bool, _array) = returnNamed();
    

    读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。在下面的代码中,我们只读取_bool,而不读取返回的_number_array

  • bool _bool2;
    (, _bool2, ) = returnNamed();
    
// 读取返回值,结构式赋值
    function readReturn() public pure{
        uint256 _number;
        bool _bool;
        bool _bool2;
        uint256[3] memory _array;
        (_number, _bool, _array) = returnNamed();

        // 读取部分返回值,解构式赋值
        (, _bool2, ) = returnNamed();
    }

在 Remix 上运行 

部署合约后查看三种返回方式的结果

5 变量数据存储和作用域

引用类型(Reference Type) :包括数组(array)和结构体(struct),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。

数据位置

Solidity数据存储位置有三类:storagememorycalldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memorycalldata类型的临时存在内存里,消耗gas少。整体消耗gas从多到少依次为:storage > memory > calldata。大致用法:

  1. storage:合约里的状态变量默认都是storage,存储在链上。

  2. memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。尤其是如果返回数据类型是变长的情况下,必须加memory修饰,例如:string, bytes, array和自定义结构。

  3. calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。例子:

function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
    //参数为calldata数组,不能被修改
    // _x[0] = 0 //这样修改会报错
    return(_x);
}