🐱2 状态变量

[TOC]

状态变量

Solidity 是面向对象的高级语言,用于实现智能合约。

  • Solidity 是静态类型的、支持继承、库和复杂的用户定义类型等功能。

  • Solidity 版本差异比较大,0.7 到 0.8 版本差异不大,但是 0.4 到 0.7 就无法兼容,因此用哪个版本的 Solidity 编译就要按照那个版本的语法编写。

  • 关于 Solidity 0.8 更新的部分 https://github.com/ethereum/solidity/releases/tag/v0.8.0 。

在 Solidity 语言中,合约类似于其它面向对象编程语言中的类。

  • 每个合约中可以包含状态变量、 函数 、事件 Event、 结构体、和枚举类型的声明,且合约可以从其他合约继承。

  • 还有一些特殊的合约,如: 库 和 接口.

状态变量是永久地存储在合约中的值,是定义在合约 contract{ 状态变量 } 中的全局变量。

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.0;

contract demo {
    uint public totalSupply = 10000;    // 状态变量
    constructor(){
        uint a = 10;    // 局部变量
    }
}

类型

  • Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指定变量的类型。

  • Solidity 提供了几种基本类型,并且基本类型可以用来组合出复杂类型。

  • 除此之外,类型之间可以在包含运算符号的表达式中进行交互。

  • undefinednil 值的概念在 Solidity 中不存在,但是新声明的变量总是有一个默认值,具体的默认值跟类型相关。

  • 要处理任何意外的值,应该使用错误处理来恢复整个交易,或者返回一个带有第二个参数的 bool 值表示成功失败。

值类型

变量始终按值来传递的称为值类型,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。

布尔类型 bool

  • 能取值为字面常量值 true(真) 和 false(假)。

  • Solidity 中的 bool 值 true != 1false != 0

  • 支持的运算符:逻辑运算符。

整型 int / uint

  • int 有符号整数可以有负数,uint 无符号整数。

  • 支持 uint8 到 uint256 (无符号,从 8 位到 256 位)以及 int8 到 int256,以 8 位为步长递增。

  • uint 和 int 分别是 uint256 和 int256 的别名。

  • 支持的运算符:比较运算符、位运算符、移位运算符、算数运算符。

定长浮点型 fixed / ufixed

  • Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。

  • fixed 有符号定长浮点型,ufixed 无符号定长浮点型。

  • 在关键字 ufixedMxN 和 fixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixed 和 fixed 分别是 ufixed128x19 和 fixed128x19 的别名。

  • 支持的运算符:比较运算符、算术运算符(+、 -、*、/、%)

  • 在许多语言中的 float 和 double 浮点型,更准确地说是 IEEE 754 浮点型和定长浮点型之间最大的不同点是,在前者中整数部分和小数部分需要的位数是灵活可变的,而后者中这两部分的长度受到严格的规定。一般来说,在浮点型中,几乎整个空间都用来表示数字,但只有少数的位来表示小数点的位置。

地址类型 address

  • 地址类型有两种,address 和 address payable。

  • address,保存一个20字节的值(以太坊地址的大小)。

  • address payable,支付地址,比 address 多了两个成员函数 transfer 和 send 。

  • address 和 address payable 的区别是,address payable 是可以发送ether,而 address 不能。

  • 类型转换: address payable 到 address 可以隐式转换,而从 address 到 address payable 必须显示的转换, 通过 payable() 进行转换。

合约类型 contract

  • contract 定义合约类型,可以隐式地将合约转换为继承的合约。

  • 合约可以显式转换为 address 类型,只有当合约具有接收 receive 函数 或 payable 回退函数时,才能显式和 address payable 类型相互转换,转换仍然使用 address(x) 执行, 如果合约类型没有接收或 payable 回退功能,则可以使用 payable(address(x)) 转换为 address payable 。

  • 声明一个合约类型的局部变量 MyContract c ,则可以调用该合约的函数, 但要赋相同合约类型的值给它。

  • 合约不支持任何运算符,合约类型的成员是合约的外部函数及 public 的 状态变量,通过 type(合约实例) 可以获取合约的类型信息。

定长字节数组 bytes1 ~ bytes32

  • 定长字节数组可以通过索引访问,索引从0开始:如果 x 是 bytesI 类型,那么 x[k] (其中 0 <= k < I)返回第 k 个字节(只读)。

  • 支持运算符:比较运算符、位运算符、移位运算符。

  • 成员变量 .length 是字节数组的长度,这是只读的。

  • 将 byte[] 当作字节数组使用非常浪费存储空间,在传入调用时,每个元素会浪费 31 字节空间,使用 bytes 更好。

  • 在 0.8.0 之前, byte 用作为 bytes1 的别名。

引用类型

引用类型可以通过多个不同的变量名修改它的值,而值类型的变量,每次都有独立的副本。

  • 引用类型包括结构、数组和映射,如果使用引用类型,则必须明确指明数据存储在哪个位置(空间)。

  • 内存 memory 位置:数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。

  • 存储 storage 位置:状态变量保存的位置,只要合约存在就一直存储。

  • 调用数据 calldata : 用来保存函数参数的特殊数据位置,是一个只读位置。

  • 更改数据位置或类型转换将始终自动产生一份拷贝,而在同一数据位置内(对于 存储storage 来说)的复制仅在某些情况下进行拷贝。

数据位置与赋值行为

  • 数据位置影响着赋值行为。

  • 在存储 storage 和 内存 memory 之间两两赋值(或者从 调用数据 calldata 赋值 ),都会创建一份独立的拷贝。

  • 从内存 memory 到 内存 memory 的赋值只创建引用,更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。

  • 从存储 storage 到本地存储变量的赋值也只分配一个引用。

  • 其他的向存储 storage 的赋值,总是进行拷贝。 如对状态变量或存储 storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝。

数组

  • 数组可以在声明时指定长度,也可以动态调整大小。

  • 固定长度声明 类型[长度],动态长度数组声明为 类型[]

  • 二维数组声明为 uint[][5] ,跟其它语言相比,数组长度的声明位置是反的,另外二维数组还无法使用于外部调用和动态数组(只能使用一维的动态数组)。

  • 数组成员变量和函数:length、push()、push(x)、pop。

  • length 获取长度。

  • push() 动态的存储 storage 数组以及 bytes 类型添加新的零初始化元素到数组末尾并返回元素引用(x.push().t = 2 或 x.push() = b)。

  • push(x) 动态的存储 storage 数组以及 bytes 类型用来在数组末尾添加一个给定的元素没有返回值。

  • pop 变长的存储 storage 数组和 bytes 类型用来从数组末尾删除元素同样会在移除的元素上隐含调用 delete。 通过 push() 增加存储 storage 数组的长度具有固定的 gas 消耗,因为 存储storage 总是被零初始化,而通过 pop 减少长度则依赖移除与元素的大小,如果元素是数组成本很高,因为它包括已删除的元素的清理,类似于在这些元素上调用 delete 。

  • 如果在外部(external)函数中使用多维数组,这需要启用 ABI coder v2。 公有(public)函数中是支持的使用多维数组。

bytes 和 strings

  • bytes 和 string 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 调用数据 calldata 和 内存 memory 中会被“紧打包”(将元素连续地存在一起,不会按每32字节一单元的方式来存放)。 string 与 bytes 相同,但不允许用长度或索引来访问。

  • Solidity 没有字符串操作函数,但可以使用第三方字符串库,比较两个字符串可以通过计算他们的 keccak256-hash ,使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) ,拼接字符串使用 abi.encodePacked(s1, s2)。

  • bytes 的 Gas 费用比 byte[] 低 ,因为 byte[] 会在元素之间添加31个填充字节。一般对任意长度的原始字节数据使用 bytes,对任意长度字符串(UTF-8)数据使用 string。

  • 如果使用一个长度限制的字节数组,可以使用一个 bytes1 到 bytes32 的具体类型,因为它们 Gas 便宜得多。

  • 另外,string 不支持中文,尽可能使用英文。

数组切片

  • 数组切片是数组连续部分的视图,用法如:x[start:end] , start 和 end 是 uint256 类型(或结果为 uint256 的表达式)。 x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end - 1] 。

  • 如果 start 比 end 大或者 end 比数组长度还大,将会抛出异常。

  • start 和 end 都可以是可选的: start 默认是 0, 而 end 默认是数组长度。

  • 数组切片没有任何成员。它们可以隐式转换为其“背后”类型的数组,并支持索引访问。 索引访问也是相对于切片的开始位置。

  • 数组切片没有类型名称,没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。

结构体 struct

  • Solidity 支持通过构造结构体的形式定义新的类型。

  • struct 关键字定义结构体,结构体可以定以在合约外部也可以定义在合约内部,合约外部的结构体是所有合约共享的,合约内部的是在此合约和衍生合约中可用。

  • 结构体定义如 struct Cat{ string name; uint256 age;},实例化如 Cat memory c = Cat(name,age);

  • 结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。

  • 尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身,因为结构体的大小是有限的。

  • 在函数中使用结构体时,一个结构体赋值给一个存储位置是存储 storage 的局部变量,在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。

  • 也可以直接访问结构体的成员而不用将其赋值给一个局部变量。

结构体中使用动态数组想要存放到 storage 是不现实的

  • 如 状态变量 projectList 要存储 voteProject,而 items 是一个动态数组,这将永远无法做到,正确做法还是使用 mapping

映射 mapping

  • 映射是以键值对形式存储数据,但是 Solidity 0.8 目前还不支持迭代映射,如果一定要迭代可以使用数组将映射中所有键保存通过遍历数组达到遍历映射的目的。

  • 声明 mapping(_KeyType => _ValueType)。 其中 _KeyType 可以是任何基本类型、 bytes 和 string 或合约类型、枚举类型。其他用户定义的类型或复杂的类型如:映射、结构体不可以作为 _KeyType 的类型的。

  • _ValueType 可以是包括映射类型在内的任何类型。

  • 映射可以视作哈希表,在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值(类型的默认值)。映射与哈希表不同的地方是在映射中并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。

  • 因此,映射没有长度,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有键的信息是无法被删除。

  • 映射只能存储 storage 的数据位置,因此只允许作为状态变量或作为函数内的 存储 storage 引用或作为库函数的参数。它们不能用合约公有函数的参数或返回值。

  • 可以将映射声明为 public ,然后来让 Solidity 创建一个 getter 函数。 _KeyType 将成为 getter 的必须参数,并且 getter 会返回 _ValueType 。

  • 如果 _ValueType 是一个映射。这时在使用 getter 时将需要递归地传入每个 _KeyType 参数。

字面常量

地址字面常量

  • 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 通过了地址校验和测试的十六进制字面常量会作为 address 类型。

  • 没有通过校验测试, 长度在 39 到 41 个数字之间的十六进制字面常量,会产生一个错误。

十六进制字面常量

  • 十六进制字面常量以关键字 hex 打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex"001122FF" ), 字符串的内容必须是一个十六进制的字符串。

  • 用空格分隔的多个十六进制字面常量被合并为一个字面常量: hex"00112233" hex"44556677" 等同于 hex"0011223344556677"

有理数和整数字面常量

  • 整数字面常量由范围在 0-9 的一串数字组成,表现成十进制。 例如,69 表示数字 69。 Solidity 中是没有八进制的,因此前置 0 是无效的。

  • 十进制小数字面常量带有一个 .,至少在其一边会有一个数字。 比如:1.,.1,和 1.3。

  • 科学计数法,指数必须是整数,但底数可以是小数。 比如:2e10, -2e10, 2e-10, 2.5e1。

字符串字面常量及类型

  • 合约中直接写中文的字符串字面量无法通过编译。

  • 字符串字面常量是指由双引号或单引号引起来的字符串("foo" 或者 'bar')。 它们也可以分为多个连续的部分("foo" "bar" 等效于 "foobar") 相当于 3 个字节而不是 4 个。 和整数字面常量一样,字符串字面常量的类型也可以发生改变,

  • 它们可以隐式地转换成 bytes1,……,bytes32,如果合适的话,还可以转换成 bytes 以及 string。例如: bytes32 samevar = "stringliteral" 字符串字面常量在赋值给 bytes32 时被解释为原始的字节形式。

  • 字符串字面常量只能包含可打印的ASCII字符,这意味着他是介于0x1F和0x7E之间的字符。

  • 字符串字面常量支持下面的转义字符:\<newline> (转义实际换行) ,\\ (反斜杠), \' (单引号), \" (双引号), \b (退格), \f (换页), \n (换行符), \r (回车), \t (标签 tab), \v (垂直标签), \xNN (十六进制转义), \uNNNN (unicode 转义)

状态变量可见性

状态变量可见性有public(公开)、internal(内部) 和 private(私有) 三种,默认是 internal 。

  • public 可见性的状态变量编译器会自动为它生成一个 getter 函数。

  • internal 可见性的状态变量只能是内部访问(从当前合约内部或从它派生的合约访问),不使用 this 调用。

  • private 可见性的状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用,合约中的所有内容对外部是公开的,设置 private 可见性只能阻止其他合约访问和修改这些信息。

getter 函数

  • 编译器自动为所有 public 状态变量创建 getter 函数用于读取状态变量的值,函数名称和状态变量同名,并且函数会被标记为 view 类型。

  • uint public a = 100; 编译器会生成一个名为 a 的函数, 该函数没有参数,返回值是一个 uint 类型,即状态变量 a 的值。

  • 结构体、数组、映射等类型的状态变量生成的getter函数是有异于其它类型的状态变量的。

数组类型状态变量

  • 如果有状态变量是数组,编译器生成的 getter 函数是有一个索引形参的,访问这个函数只能获得数组索引位置的元素,这是为了避免返回整个数组的高成本gas,因此如果要获得整个数组就要编写一个获得数组的函数。

状态变量储存结构

固定大小的变量(除映射 mapping 和动态数组之外的所有类型)都从位置 0 开始连续放置在存储 storage 中。存储大小少于 32 字节的多个变量会按照规则被打包到一个存储插槽 storage slot 中:

  • 规则1:存储插槽 storage slot 的第一项会以低位对齐(即右对齐)的方式储存。

  • 规则2:基本类型仅使用存储它们所需的字节。

  • 规则3:存储插槽 storage slot 中的剩余空间不足以储存一个基本类型,会移入下一个存储插槽 storage slot 存储。

  • 结构体(struct)和数组数据总是会占用一整个新插槽,但结构体或数组中的元素,都会以这些规则进行打包。

  • 使用了继承的合约,状态变量的排序由C3线性化合约顺序, 顺序从最基类合约开始确定。来自不同的合约的状态变量符合规则的也会共享一个存储插槽 storage slot。 -结构体和数组中的成员变量会存储在一起,就像它们在显式声明中的一样。

  • 在使用小于 32 字节的变量时,合约的 gas 使用量可能会高于使用 32 字节的遍历。因为以太坊虚拟机 Ethereum Virtual Machine(EVM) 每次操作 32 个字节, 所以如果元素比 32 字节小,以太坊虚拟机Ethereum Virtual Machine(EVM) 必须执行额外的操作以便将其大小缩减到到所需的大小。

  • 在处理状态变量时,只有当编译器会将多个元素打包到一个存储插槽 storage slot 中,使用缩减的大小(小于32字节)的变量才更有益处。因为它会将多个读或写合并为单次操作。而在处理函数参数或内存 memory 中的值时,因为编译器不会打包这些值,所以没有什么益处。

  • 为了允许以太坊虚拟机 Ethereum Virtual Machine(EVM) 对此进行优化,要确保 存储 storage 中的变量和 struct 成员的书写顺序允许它们被紧密地打包。 例如,应该按照 uint128,uint128,uint256 的顺序来声明状态变量,而不是使用 uint128,uint256,uint128 的顺序,因为前者只占用两个存储插槽 storage slot,而后者会占用三个。

  • 由于存储 storage 中的指针可以传递给库(library),所以存储 storage 中状态变量的布局被认为是 Solidity 外部接口的一部分。

映射和动态数组

  • 由于映射mapping 和动态数组的大小是不可预知的,他们使用 Keccak-256 哈希计算来找到值的位置或数组的起始位置。这些起始位置本身的数值总是会占满堆栈插槽。

  • 映射mapping 或动态数组本身会根据 Keccak-256 哈希在某个位置 p 处占用一个空的存储 storage 中的插槽。

  • 对于动态数组,此插槽中会存储数组中元素的数量(字节数组 bytes 和字符串 string 除外)。

  • 对于映射 mapping 即使插槽未被使用,但它仍会占用,以使两个相同的 映射mapping 在彼此之后会使用不同的散列分布。

  • 数组的数据会位于 keccak256(p);,映射 mapping 中的键 k 所对应的值会位于 keccak256(k . p);. 是连接符。如果该值又是一个非基本类型,则通过添加 keccak256(k . p); 作为偏移量来找到位置。

  • 所以对于以下合约片段data[4][9].b 的位置将是 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1 。

bytes 和 string

  • bytes 和 string 编码相同,会将数据和长度存储在同一个插槽中。

  • 如果数据长度小于等于31字节,则存储在高位字节(左对齐),最低位字节存储 length * 2。 如果数据长度超出 31 字节,则在主插槽存储 length * 2 + 1 ,数据照常存储在 keccak256(slot) 中。

不支持浮点数

EVM 和 Solidity 不支持浮点数。

  • 浮点数是一种特殊的有理数,它的分母必须是二的整数次幂。

  • 如果一定需要用到浮点数可以设计有理数替代(用两个整数一个整数分子和一个整数分母),可以达到和使用浮点数相同的效果。

Last updated