学区块链做的笔记Day6,大部分内容来自《精通以太坊》和WTF-Solidity

之前的笔记感觉抄书的东西还是太多了,以后尽量自己总结了。

什么是智能合约

  • 计算机程序
    智能合约就是计算机程序。
  • 不可改变
    一旦部署之后,智能合约的代码就不能被改变。更改智能合约的唯一办法就是部署一个新的实例。
  • 确定性的
    对于触发智能合约执行的交易上下文,或执行时的以太坊区块链状态,智能合约执行结果的输出对于每一个运行或调用它的人来说都是一样的。
  • 以太坊虚拟机上下文
    智能合约运行在一个非常有限的执行环境中。它们可以访问自己的状态,调用合约交易的上下文信息,以及有关最近区块的信息。
  • 去中心化的世界计算机
    EVM作为每一个以太坊节点的本地实例运行,但是因为所有EVM都是运行在相同的初始状态,并且会输出完全相同的最终状态,所以整个系统就像是一台世界计算机。

合约的生命周期

每一个合约实例都通过以太坊地址来表示,这个地址由合约的创建交易在创建账户和随机数时生成。合约没有私钥,作为合约的创建者们不会在协议层获得相对其他用户而言的任何特权(但是可以把这种特权写进合约内部)。

合约只有在被交易调用时才会被执行。一个合约可以调用另一个合约。在没有交易触发执行的情况下,合约永远处在等待调用的状态。任何情况下智能合约的“并发执行”都是没有意义的:以太坊世界计算机可以被认为是一台单线程的计算机。

交易是原子化的,交易的执行是一个整体,对于全局状态的修改仅会在所有执行都确定成功之后才会进行。如果程序执行因为错误而终止,它之前进行的所有操作都会被回滚,就像这个执行从来没有发生过一样。失败的交易仍旧会被作为一次失败的尝试而记录在案,执行所花费的gas将从发起账户中被扣除,除此之外,它不会对合约或者账户状态产生任何影响。

合约实例可以被删除,从它的地址把代码和合约实例的内部状态(存储)清空,让这个地址变成一个空账户。要删除合约实例,需要执行名为SELFDESTRUCT(之前称为SUICIDE)的EVM字节码。这个操作会产生负的gas消耗,也就是系统会提供gas退款,这也会激励人们通过删除存储状态的方式释放资源。删除合约并不会清除这个合约之前交易的历史记录,因为区块链本身是不可变的。需要注意,只有合约开发者在代码中编写了对应的删除功能,SELFDESTRUCT字节码才会起作用。如果合约的代码不包含SELFDESTRUCT字节码,或者合约实例是不可执行的,那么这个合约实例就无法删除

以太坊高级编程语言

智能合约运行在一个高度隔离并且极其简单的执行环境(EVM)中。EVM是一个运行字节码这种特殊形式机器码的虚拟机,类似运行x86_64指令集的计算机CPU。大多数常见的用户界面,操作系统接口和硬件接口在EVM环境中都不复存在。

编程语言可分为两大类:声明式的指令式的,也对应称为函数式的和过程式的。指令式程序代码更容易编写和阅读,但是却很难用于编写那些严格按部就班执行的代码,并为非预期的副作用和错误引入了许多机会。

智能合约为程序员设定了一个很高的门栏:如果有bug,可能会损失大量的金钱。因此,编写智能合约就需要极力避免任何可能的副作用。可见,声明式编程语言在智能合约中的作用要大于在通用软件中的作用。然而,智能合约(Solidity)最为广泛使用的语言却是指令式的。

目前用于智能合约编写的高级语言如下:

  • LLL
    一种函数式(声明式)编程语言,语法类似Lisp,这是首款可以在以太坊上编写智能合约的编程语言。
  • Serpent
    一种过程式(指令式)编程语言,语法类似Python。也可以用来编写函数式(声明式)代码。
  • Solidity
    一种过程式编程语言,语法类似JavaScript、C++或者Java。这是最流行也是最常用的以太坊智能合约编程语言。
  • Vyper
    语法类似SerpentPython。目标在于达到比Serpent更加纯函数式的类Python语言,但不是为了替代Serpent
  • Bamboo
    Erlang启发,引入了显式状态转换,并去掉了递归式的循环。旨在减少副作用并提升可审计性。

使用Solidity编写智能合约

ethereum/solidity: Solidity, the Smart Contract Programming Language (github.com)

Solidity项目的产出主要是Solidity编译器(solc),它用于把Solidity语言编写的代码编译为字节码。这个项目也管理以太坊是智能合约的应用程序二进制接口(ABI)标准。

Solidity的版本模型采用语义化版本,也就是说,通过MAJOR.MINOR.PATCH三组数字的方式来制定版本。

  • MAJOR主版本号:当你做了不兼容的 API 修改(大版本更新)
  • MINOR次版本号:当你做了向下兼容的功能性新增(小版本更新)
  • PATCH修订号:当你做了向下兼容的问题修正(修bug)

以太坊合约的应用程序二进制接口(ABI)

在计算机软件中,应用程序二进制接口(ABI)是指两个程序模块之间的接口,通常,一个在操作系统层面,另外一个在用户程序层面。ABI定义了数据结构和函数如何在机器指令中被访问

需要注意,这并不是我们常说的API,API定义了高级的,供程序员阅读和使用的源代码接口。

ABI是向机器指令层面编码和解码并传送数据的主要方式。

在以太坊中,ABI用来编码合约中对EVM的调用和从交易中获取数据的调用。ABI的目的是定义合约中哪一个函数可以被调用,并且描述这个函数接收的参数和返回的数据

合约ABI使用JSON格式表示,其中包含描述合约中函数和事件的数组。

在JSON中,有关函数的属性描述字段包括typenameinputsoutputsconstantpayable。有关事件的属性描述字段包括typenameinputsanonymous

应用与合约的交互完全依赖于ABI和应用实例部署的以太坊地址。

(ABI大概可以看作一个程序的描述,比如我要调用什么,输出什么,知道这些后我们才能对合约进行操作,不然我们就不知道该发啥)

使用Solidity进行编程

Solidity文档Solidity — Solidity 0.8.20 文档 (soliditylang.org)

推荐:

Solidity by Example | 0.8.20 (solidity-by-example.org)

AmazingAng/WTF-Solidity: 我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用,每周更新1-3讲。Now supports English! 官网: https://wtf.academy (github.com)

以下内容主要参考WTF-Solidity

软件许可声明

一般在代码的第一行注释中写代码所用的软件许可(license),这里用的是MIT license。

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

如果不写许可,编译时会警告(warning),但程序可以运行。solidity的注释由“//”开头,后面跟注释的内容。

Solidity中的变量

  • 数值类型(Value Type):包括布尔型整数型等等,这类变量赋值时候直接传递数值。
  • 引用类型(Reference Type):包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
  • 映射类型(Mapping Type): Solidity里的哈希表。
  • 函数类型(Function Type)Solidity文档里把函数归到数值类型,但我觉得他跟其他类型差别很大,所以单独分一类。

变量数据存储和作用域

数据位置

solidity数据存储位置有三类:storagememorycalldata不同存储位置的gas成本不同storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memorycalldata类型的临时存在内存里,消耗gas少。大致用法:

  1. storage:合约里的状态变量默认都是storage,存储在链上。
  2. memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。
  3. calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable)(类似const?),一般用于函数的参数。例子:
1
2
3
4
5
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}

数据位置和赋值规则

在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)。规则如下:

  1. storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。例子:
1
2
3
4
5
6
7
uint[] x = [1,2,3]; // 状态变量:数组 x

function fStorage() public{
//声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}
  1. storage赋值给memory,会创建独立的副本,修改其中一个不会影响另一个;反之亦然。例子:
1
2
3
4
5
6
7
8
9
10
uint[] x = [1,2,3]; // 状态变量:数组 x

function fMemory() public view{
//声明一个Memory的变量xMemory,复制x。修改xMemory不会影响x
uint[] memory xMemory = x;
xMemory[0] = 100;
xMemory[1] = 200;
uint[] memory xMemory2 = x;
xMemory2[0] = 300;
}
  1. memory赋值给memory,会创建引用,改变新变量会影响原变量。
  2. 其他情况,变量赋值给storage,会创建独立的副本,修改其中一个不会影响另一个。

变量的作用域

Solidity中变量按作用域划分有三种:

  • 状态变量(state variable)
  • 局部变量(local variable)
  • 全局变量(global variable)
状态变量

状态变量是数据存储在链上的变量,所有合约内函数都可以访问 ,gas消耗高。状态变量在合约内、函数外声明:

1
2
3
4
contract Variables {
uint public x = 1;
uint public y;
string public z;

我们可以在函数里更改状态变量的值:

1
2
3
4
5
6
function foo() external{
// 可以在函数里更改状态变量的值
x = 5;
y = 2;
z = "0xAA";
}
局部变量

局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链gas。局部变量在函数内声明:

1
2
3
4
5
6
function bar() external pure returns(uint){
uint xx = 1;
uint yy = 3;
uint zz = xx + yy;
return(zz);
}
全局变量

全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用

1
2
3
4
5
6
function global() external view returns(address, uint, bytes memory){
address sender = msg.sender;
uint blockNum = block.number;
bytes memory data = msg.data;
return(sender, blockNum, data);
}

在上面例子里,我们使用了3个常用的全局变量:msg.sender, block.numbermsg.data,他们分别代表请求发起地址,当前区块高度,和请求数据。下面是一些常用的全局变量,更完整的列表请看这个链接

交易/消息的调用上下文

msg对象是指触发合约执行的交易,这个对象包含了一些属性:

  • msg.sender
    发起合约调用的以太坊地址。如果这个合约是由来自外部账户的交易触发的,那么这个地址就是对交易进行签名的地址,否则就是另外一个合约的地址(合约之间互相调用)。
  • msg.value
    调用中发送的以太币数量(以wei为单位)。
  • msg.gas
    执行环境中当前可用的gas数量,这个属性已经被废弃,将会被gasleft函数取代。
  • msg.data
    调用合约时传入的数据。
  • msg.sig
    传输数据的前四个字节,这是一个函数选择器。
交易的上下文

tx对象提供了访问交易相关信息的办法。

  • tx.gasprice
    调用交易的gas价格。
  • tx.origin
    发起这个交易的外部账户的地址。警告:这是个不安全的做法
区块的上下文

block对象包含了当前区块的信息:

  • block.blockhash(blockNumber)
    指定区块的哈希值,仅限于当前区块之前不超过256个区块。目前已经弃用,被blockhash方法取代。
  • block.coinbase
    当前区块的矿工地址,用于接收这个区块的出块奖励和所有交易费。
  • block.difficulty
    当前区块工作量证明的难度。
  • block.gaslimit
    可以在当前区块中包含的所有事物中花费的最大gas数量。
  • block.number
    当前的区块编号(区块在链中的高度)。
  • block.timestamp
    由狂高写入的当前区块的时间戳。
地址对象

不论是来自于外部输入,还是从合约对象中获取,地址类对象都包含如下属性和方法:

  • address.balance
    当前地址的余额,以wei为单位。
  • address.transfer(amount)
    转账一定数量(以wei为单位)的以太币到指定的地址,遇到任何错误都将抛出异常。
  • address.send(amount)
    与上面的address.transfer(amount)类似,但是不抛出异常。
  • address.call(payload)
    一种底层CALL函数,可以构建一个包含自定义数据的调用,出错时会返回false
  • address.callcode(payload)
    一种底层CALLCODE函数。
  • address.delegatecall()
    一种底层DELEGATECALL函数。
内建函数

其他值得注意的函数是:

  • addmodmulmod
    模数的加法和乘法,列如:addmod(x,y,k)计算
  • keccak256sha256sha3ripemd160
    多种算法的哈希计算函数
  • ecrecover
    从数字签名的数据中计算,获取签名地址。
  • selfdestrunct(recipient_address)
    删除当前的合约,把合约中剩余的比特币转账到recipient_address这个地址。
  • this
    当前执行合约账户的以太坊地址。

数值类型

布尔型(bool)

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

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

布尔值的运算符,包括:

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

整型(int,unit)

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

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

常用的整型运算符包括:

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

固定浮点数(fixed、ufixed)

固定小数点的浮点数,使用(u)fixedMxN定义,其中M是比特的位数(从8到256),N用来表示小数点之后有多少位(最多18),列如ufixed32x2

地址类型

地址类型(address)存储一个 20 字节的值(以太坊地址的大小)。地址类型也有成员变量,并作为所有合约的基础。有普通的地址和可以转账ETH的地址(payable)。其中,payable修饰的地址相对普通地址多了transfersend两个成员。在payable修饰的地址中,send执行失败不会影响当前合约的执行(但是返回false值,需要开发人员检查send返回值)。balancetransfer(),可以用来查询ETH余额以及安全转账(内置执行失败的处理)。

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

定长字节数组

字节数组bytes分两种,一种定长(byte, bytes8, bytes32),另一种不定长。定长的属于数值类型,不定长的是引用类型(之后讲)。 定长bytes可以存一些数据,消耗gas比较少。

1
2
3
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];

MiniSolidity变量以字节的方式存储进变量_byte32,转换成16进制为:0x4d696e69536f6c69646974790000000000000000000000000000000000000000

_byte变量存储_byte32的第一个字节,为0x4d

枚举 (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的一个比较冷门的变量,几乎没什么人用。

函数类型

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

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

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

function

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

<function name>

函数名。

(<parameter types>)

圆括号里写函数的参数,也就是要输入到函数的变量类型和名字。

{internal|external|public|private}

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

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

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

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

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

[pure|view|payable]

决定函数权限/功能的关键字。

  • view:当函数被标注为view时,它将承诺不对任何状态进行修改。包含view关键字的函数,能读取但也不能写入状态变量。
  • purepure函数表示这个函数不会在区块链存储中读取或者写入任何数据。这样的函数只能处理参数,然后返回值给调用方,无法在区块链上读取或者存储任何数据。
  • payablepayable函数用于接收外部的支付。未声明为payable的函数不能接收任何以太币支付。

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

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

[returns ()]

函数返回的变量类型和名称。

返回值 return和returns

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

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

上面这段代码中,我们声明了returnMultiple()函数将有多个输出:returns(uint256, bool, uint256[3] memory),接着我们在函数主体中用return(1, true, [uint256(1),2,5])确定了返回值。

命名式返回

我们可以在returns中标明返回变量的名称,这样solidity会自动给这些变量初始化,并且自动返回这些函数的值,不需要加return

1
2
3
4
5
6
// 命名式返回
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来返回变量:

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

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

  • 读取所有返回值:声明变量,并且将要赋值的变量用,隔开,按顺序排列。
1
2
3
4
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
  • 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。下面这段代码中,我们只读取_bool,而不读取返回的_number_array
1
(, _bool2, ) = returnNamed();

引用类型

数组array

数组(Array)是solidity常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种:

  • 固定长度数组:在声明时指定数组的长度。用T[k]的格式声明,其中T是元素的类型,k是长度,例如:
1
2
3
4
// 固定长度 Array
uint[8] array1;
bytes1[5] array2;
address[100] array3;
  • 可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如:
1
2
3
4
5
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;

注意bytes比较特殊,是数组,但是不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytesbytes1[]。在gas上,bytesbytes1[]便宜。因为bytes1[]memory中要增加31个字节进行填充,会产生额外的gas。但是在storage中,由于内存紧密打包,不存在字节填充。

创建数组的规则

在solidity里,创建数组有一些规则:

  • 对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:
1
2
3
// memory动态数组
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);
  • 数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如[1,2,3]里面所有的元素都是uint8类型,因为在solidity中如果一个值没有指定type的话,默认就是最小单位的该type,这里int的默认最小单位类型就是uint8。而[uint(1),2,3]里面的元素都是uint类型,因为第一个元素指定了是uint类型了,我们都以第一个元素为准。 下面的合约中,对于f函数里面的调用,如果我们没有显式对第一个元素进行uint强转的话,是会报错的,因为如上所述我们其实是传入了uint8类型的array,可是g函数需要的却是uint类型的array,就会报错了。
1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f() public pure {
g([uint(1), 2, 3]); //这里传入的第一个要uint()
}
function g(uint[3] memory) public pure { //因为这里用的是uint数组
// ...
}
}
  • 如果创建的是动态数组,你需要一个一个元素的赋值。
1
2
3
4
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
数组成员
  • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
  • push(): 动态数组bytes拥有push()成员,可以在数组最后添加一个0元素。
  • push(x): 动态数组bytes拥有push(x)成员,可以在数组最后添加一个x元素。
  • pop(): 动态数组bytes拥有pop()成员,可以移除数组最后一个元素。

结构体 struct

Solidity支持通过构造结构体的形式定义新的类型。创建结构体的方法:

1
2
3
4
5
// 结构体
struct Student{
uint256 id;
uint256 score;
}
1
Student student; // 初始一个student结构体

给结构体赋值的两种方法:

1
2
3
4
5
6
7
//  给结构体赋值
// 方法1:在函数中创建一个storage的struct引用
function initStudent1() external{
Student storage _student = student; // assign a copy of student
_student.id = 11;
_student.score = 100;
}
1
2
3
4
5
// 方法2:直接引用状态变量的struct
function initStudent2() external{
student.id = 1;
student.score = 80;
}

映射类型

映射mapping

在映射中,人们可以通过键(Key)来查询对应的值(Value),比如:通过一个人的id来查询他的钱包地址。

声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType_ValueType分别是KeyValue的变量类型。例子:

1
2
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址

映射的规则

  • 映射的_KeyType只能选择solidity默认的类型,比如uintaddress等,不能用自定义的结构体。而_ValueType可以使用自定义的类型
  • 映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量,和library函数的参数(见例子)。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。
  • 如果映射声明为public,那么solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value
  • 给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key_Value对应新增的键值对。

映射的原理

  • 映射不储存任何键(Key)的资讯,也没有length的资讯。
  • 映射使用keccak256(key)当成offset存取value。
  • 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。

变量初始值

solidity中,声明但没赋值的变量都有它的初始值或默认值

值类型初始值

  • boolean: false

  • string: ""

  • int: 0

  • uint: 0

  • enum: 枚举中的第一个元素

  • address: 0x0000000000000000000000000000000000000000 (或 address(0))

  • function

    • internal: 空白方程
    • external: 空白方程

可以用public变量的getter函数验证上面写的初始值是否正确:

1
2
3
4
5
6
7
8
9
10
11
bool public _bool; // false
string public _string; // ""
int public _int; // 0
uint public _uint; // 0
address public _address; // 0x0000000000000000000000000000000000000000

enum ActionSet { Buy, Hold, Sell}
ActionSet public _enum; // 第1个内容Buy的索引0

function fi() internal{} // internal空白方程
function fe() external{} // external空白方程

引用类型初始值

  • 映射mapping: 所有元素都为其默认值的mapping
  • 结构体struct: 所有成员设为其默认值的结构体
  • 数组array
    • 动态数组: []
    • 静态数组(定长): 所有成员设为其默认值的静态数组

可以用public变量的getter函数验证上面写的初始值是否正确:

1
2
3
4
5
6
7
8
9
10
// Reference Types
uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0]
uint[] public _dynamicArray; // `[]`
mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping
// 所有成员设为其默认值的结构体 0, 0
struct Student{
uint256 id;
uint256 score;
}
Student public student;

delete

delete a会让变量a的值变为初始值。

1
2
3
4
5
// delete操作符
bool public _bool2 = true;
function d() external {
delete _bool2; // delete 会让_bool2变为默认值,false
}

常数

solidity中两个关键字,constant(常量)immutable(不变量)。状态变量声明这个两个关键字之后,不能在合约后更改数值;并且还可以节省gas。另外,只有数值变量可以声明constantimmutablestringbytes可以声明为constant,但不能为immutable

constant

constant变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过

1
2
3
4
5
// constant变量必须在声明的时候初始化,之后不能改变
uint256 constant CONSTANT_NUM = 10;
string constant CONSTANT_STRING = "0xAA";
bytes constant CONSTANT_BYTES = "WTF";
address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;

immutable

immutable变量可以在声明时或构造函数中初始化,因此更加灵活。

1
2
3
4
5
// immutable变量可以在constructor里初始化,之后不能改变
uint256 public immutable IMMUTABLE_NUM = 9999999999;
address public immutable IMMUTABLE_ADDRESS;
uint256 public immutable IMMUTABLE_BLOCK;
uint256 public immutable IMMUTABLE_TEST;

你可以使用全局变量例如address(this)block.number ,或者自定义的函数给immutable变量初始化。在下面这个例子,我们利用了test()函数给IMMUTABLE_TEST初始化为9

1
2
3
4
5
6
7
8
9
10
11
// 利用constructor初始化immutable变量,因此可以利用
constructor(){
IMMUTABLE_ADDRESS = address(this);
IMMUTABLE_BLOCK = block.number;
IMMUTABLE_TEST = test();
}

function test() public pure returns(uint256){
uint256 what = 9;
return(what);
}