变量

上一章我们学习了 Solidity 中的数据知识,数据是编程语言的生产生活资料,而变量是数据的代言人。我们在 Solidity 中很多时候不直接使用数据,而是使用变量外表示数据。数据作为最基础的生产资料,而变量作为代言人,同样非常重要。

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

变量基础知识

回顾一下前面两章提到的变量:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract DataTypes {
    string public myString = "hello world";
    bool public b = true;
    uint256 public u = 123;

    int256 public i = -123;
    int256 public minInt = type(int256).min; // 获取最小值
    int256 public maxInt = type(int256).max; // 获取最大值
    address public ads = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
    bytes32 public bys32 = "abc";
}

初始默认值

Solidity 是一种静态类型语言,这意味着需要在声明期间指定变量类型。

在 Solidity 中没有 null 或者 undefined 的概念,但是新声明的变量总是有一个默认值,具体的默认值跟类型相关,比如 int 类型的默认值为 0。每个变量声明时,都有一个基于其类型的默认值。

默认值的两个要点

  1. Solidity 智能合约中所有的变量,都有默认值,没有 null 或者 undefined 的概念。

  2. 这些变量在没有被赋值之前,它的值已默认值的形式存在。

例子演示

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract DefaultValues {
    string public str; // ""
    bool public b; // false
    int256 public intValue; // 0
    uint256 public count; // 0

    address public ads; // 0x0000000000000000000000000000000000000000

    // 0x0000000000000000000000000000000000000000000000000000000000000000
    bytes32 public bt32;

    // array
    uint256[] public arr;
    // enum
    enum Status {
        None,
        Pending,
        Shiped,
        Completed,
        Rejected,
        Canceled
    }
    Status public status;

    function getArr() external view returns (uint256[] memory) {
        return arr;
    }
}

默认值总结

  • string: ""

  • bool: false

  • int256: 0

  • uint256: 0

  • address: 0x0000000000000000000000000000000000000000

  • bytes32: 0x0000000000000000000000000000000000000000000000000000000000000000

  • enum: 0

  • 动态数组: []

  • 定长数组: 每个元素的默认值

  • mapping / strucr 均为所在类型的默认值

上述的这些值也可以通过 delete 操作符来实现,详细参考 delete 操作赋,在数据那一章也多次提到通过delete 操作符来使指定元素恢复默认值。

作用域和声明

Solidity 中的作用域规则遵循了 C99:

作用域的规则

  • 变量将会从它们被声明之后可见,直到一对 {} 块的结束。

  • 对于参数形式的变量(例如:函数参数、修饰器参数、catch 参数等等)在其后接着的代码块内有效。

    • 这些代码块是函数的实现,catch 语句块等。

    • 有一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到 for 循环的结束。

  • 那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。

    • 意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。

例子演示

基于上面总结规则,下边的例子两个变量虽然名字一样,但却在不同的作用域里。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract C {
    function minimalScoping() public pure returns (uint256) {
        uint256 same;
        {
            uint256 same;
            same = 1;
        }

        {
            uint256 same;
            same = 3;
        }
        return same;
    }
}

作为 C99 作用域规则的特例,请注意在下边的例子里,第一次对 x 的赋值会改变上一层中声明的变量值。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract C {
    function f() public pure returns (uint256) {
        uint256 x = 1;
        {
            // This declaration shadows an existing declaration.
            x = 2; // 这个赋值会影响在外层声明的变量

            // Unused local variable.
            uint256 x;
        }
        return x; // x has value 2
    }
}

在 Solidity 中,如果在内部作用域中使用和外层相同的变量名,会收到警告信息。这种警告是告诉开发者外层声明的变量被“覆盖”了,谨慎检查下是不是期望的。

上面例子中的 f 函数的 return 值 可以改写为下面的:

function f() public pure returns (uint256 x) {
    x = 1;
    {
        x = 2;
        uint256 x;
    }
}

必须先声明再赋值

在 Solidity 现在的版本中,变量必须先声明再赋值,顺序不能倒。

0.5.0 版本之前,一个变量声明在函数的任意位置,都可以使他在整个函数范围内可见。从 0.5.0 版本开始以后就不能这样了。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract C {
    // ✅
    function t() public pure returns (uint256) {
        uint256 x;
        x = 2;
        return x;
    }

    // ❌
    function f() public pure returns (uint256) {
        // DeclarationError: Undeclared identifier.
        // "x" is not (or not yet) visible at this point.
        x = 2;
        uint256 x;
        return x;
    }
}

合约外定义的类型和函数

  • 合约外面可以定义函数和数据结构

    • 定义在合约外面的函数,叫自由函数

    • 定义在合约外面的类型,可以被多个合约使用

  • 不可以定义变量

    • 但是可以定义常量,常量那一节有介绍

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

// 结构:
//      定义在合约外面
//      可以被多个合约同时使用
struct Book {
    string title;
    string author;
    uint256 book_id;
}

// 自由函数:
//      定义在合约外面
//      没有可见性
function getBalance() view returns (uint256) {
    return address(msg.sender).balance;
}


function add(uint256 a_, uint256 b_) pure returns (uint256) {
    return a_+b_;
}

contract Demo1 {
    Book public book1 = Book("Solidity", "Anbang", 1);
    function f() public view returns (uint256) {
        return getBalance();
    }
    function test() public pure returns (uint256) {
        return add(100,200);
    }

}

contract Demo2 {
    Book public book2 = Book("Solidity 2", "Anbang", 2);
}

变量的三种状态

按作用域划分,状态可以分为下面三种状态:

  • 状态变量:

    • 变量值永久保存在智能合约存储空间中,相当于属于已经写入到区块链中,可以随时调用,除非该条链消失。

    • 特点:定义在智能合约的存储空间中

  • 局部变量:

    • 变量值仅在函数执行过程中有效,供函数内部使用;调用函数时,在虚拟机的内存中;函数退出后,变量无效。类似”闭包”的特性。

    • 特点: 定义在函数内部

  • 全局变量:

    • 保存在全局命名空间,用于获取区块链相关信息的特殊变量。

    • 特点:存在于 EVM 虚拟机中,不用定义,直接获取即可。

状态变量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Var{
    uint256 public myUint = 123;
    function changeMyUint (uint256 x) external returns (uint256 ){
        myUint = x;
        return myUint;
    }
}

局部变量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Var{
     function test() external pure returns (uint256){
        uint256 local = 2;// 局部变量
        return local;
    }
}

全局变量

  • msg.sender

  • msg.value

  • block.timestamp

  • block.number

先简单了解下,后面会有一节进行详细介绍。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    address public owner = msg.sender; // 在状态变量中使用

    // 在函数内使用
    function global() external view returns(address,uint256,uint256){
        return(msg.sender,block.timestamp,block.number);
    }
}

msg.sender:表示当前调用方法时的发起人。

一个智能合约既可以被合约创建者调用,也可以被其它人调用。合约创建者,即合约拥有者,也就是指合约部署者,如何判断合约的拥有者? 第一次部署的时候进行定义,即在构造函数中定义:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Test {
    address public _owner;

    constructor() {
        _owner = msg.sender;
    }
}

Constant 常量

  • 普通变量与常量:普通的状态变量,添加 constant 关键词即可声明为常量

  • 与常规状态变量相比,常量的 gas 要低很多。

  • 常量名字一般使用全大写。

  • 常量赋值后不可以修改。

  • 常量必须声明和初始化一起做掉,否则编译不通过。

  • 常量的值储存原理

    • 常量的值在编译器确定,因为在编译器确定,所以不能定义在函数内。

    • 编译器并不会为 constant 常量在 storage 上预留空间,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。

    • 因为不是储存在storage 上,所以函数内读取常量不算view,可以使用 pure

    • 因为不是储存在storage 上,所以可以在任意位置定义常量,比如在合约外面

    • 也可以在文件级别定义 constant 变量(0.7.2 之后的特性)。

  • 引用类型只支持字符串

    • 不是所有的类型都支持常量,当前支持的仅有值类型(包括地址类型)/字符串

  • 可以使用内建函数赋值常量

普通变量与常量

普通的状态变量,添加 constant 关键词即可声明为常量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    string name1 = "Anbang";
    string constant name2 = "Anbang";
}

常量相比变量更省钱

与常规状态变量相比,常量的 gas 要低很多。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    // 24465
    string public name1 = "Anbang";

    // 21793
    string public constant name2 = "Anbang";
}

常量名字一般使用全大写

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    string public constant NAME = "Anbang";
}

常量赋值后不可以修改

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    string public name = "Anbang";
    string public constant NAME = "Anbang";

    function set1() external{
        name = "Anbang Chu";
    }

    // function set2() external{
    //     // Cannot assign to a constant variable.
    //     NAME = "Anbang Chu";
    // }
}

常量必须声明和初始化一起做掉

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    //  Uninitialized "constant" variable.
    string public constant NAME;
}

常量的值储存原理

  • 常量的值在编译器确定,因为在编译器确定,所以不能定义在函数内。

  • 编译器并不会为 constant 常量在 storage 上预留空间,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。

  • 因为不是储存在storage 上,所以函数内读取常量不算view,可以使用 pure

  • 因为不是储存在storage 上,所以可以在任意位置定义常量,比如在合约外面

  • 也可以在文件级别定义 constant 变量(0.7.2 之后的特性)。

不能定义在函数内

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    // The "constant" keyword can only be used for state variables
    //  or variables at file level.
    function getName() external pure returns(string memory){
        string memory constant NAME = "Anbang";
        return NAME;
    }
}

函数内读取常量不属于 view

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
    string public constant NAME = "Anbang1";
    string public name = "Anbang2";

    function getName() external view returns(string memory){
        return name;
    }

    function getNAME() external pure returns(string memory){
        return NAME;
    }
}

可以在合约外面定义常量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

string constant NAME = "Anbang1";

contract Demo1 {
    function getNAME() external pure returns(string memory){
        return NAME;
    }
}

常量可以定义在文件中

info.sol 文件

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

string constant NAME1 = "Anbang1";
string constant NAME2 = "Anbang2";

demo.sol 文件:引用 info 文件。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "./info.sol" as INFO;

contract Demo1 {
    function getNAME() external pure returns(string memory){
        return INFO.NAME1;
    }
}

引用类型支持 string 和 bytes

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    // string <=> bytes
    string public constant NAME1 = "Anbang1";
    bytes public  constant NAME2 = "Anbang1";

    // Only constants of value type and byte array type are implemented.
    // uint256[] public constant  NAME3 = [1,2,3];
}

常见的赋值方式

如果状态变量声明为 constant(常量)。在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。

  • 允许可能对内存分配产生 side effect(副作用)的表达式,但那些可能对其他内存对象产生副作用的表达式则不允许。

    • 内建(built-in)函数 keccak256sha256ripemd160ecrecoveraddmodmulmod是允许的(即使他们确实会调用外部合约, keccak256 除外)。

    • 允许内存分配器的副作用的原因是它可以构造复杂的对象,例如:查找表(lookup-table)。 此功能尚不完全可用。

运算符赋值

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo1 {
    uint256 public constant VERSION = 1+1;

    // 100个ETH
    // 100000000000000000000
    uint256 public  constant VALUE = 100 * 10**18; // 运算符赋值

    // 1小时
    uint256 public  constant H = 60 * 60;
}

使用 address

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    address public constant ZERO = address(0);
}

密码学函数赋值

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract C {
    uint256 public constant a = addmod(4, 5, 3);
    uint256 public constant b = mulmod(4, 5, 3);
    bytes32 public constant c1 = sha256("Hello");
    bytes32 public constant c2 = ripemd160("Hello");
    bytes32 public constant myBytes32 = keccak256("Hello");
}

禁止的一些赋值

不允许使用状态变量/区块链数据来赋值,也不允许外部合约调用来赋值。

  • 任何通过访问 storage,区块链数据(例如 block.timestamp,address(this).balance 或者 block.number)或执行数据( msg.valuegasleft() ) 或对外部合约的调用来给它们赋值都是不允许的。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    address public ads1 = msg.sender;
    // address public constant ads2 = msg.sender;
}

Immutable 不可变量

上一节我们学习了常量,常量的值是在编译器确定,因为在编译器确定,常量必须声明和初始化一起做掉,否则编译不通过。这就给很多只需要一次赋值,但是又必须需要动态赋值的场景带来了不变。比如合约的 owner 地址等场景。

然后上面的苦恼,可以通过不可变量来解决。如果我们想要一个变量,赋值后就不可以修改,而且值是部署时候动态赋值,那么可以使用不可变量的类型。通过 immutable 关键字可以声明为不可变量,不可变量的限制要比声明为常量(constant) 的变量的限制少:

不可变量可以声明和赋值一起做掉,也可以 storage 中声明,在合约的构造函数中赋值。无论在哪里赋值,只能赋值一次,也带来更多的安全性。

  • 原理: 在部署的时候确定变量的值,它是一个运行时赋值。

    • 扩展了解: 如果要将编译器生成的运行时代码与实际存储在区块链中的代码进行比较,需要明白:编译器生成的合约创建代码将在返回合约之前修改合约的运行时代码,方法是将对不可变量的所有引用替换为分配给它们的值。

  • 特点:它既有 constant 常量不可修改和 Gas 费用低的优势,又有变量动态赋值的优势。

    • Solidity immutable 是另一种常量的表达方式。

  • 原则:

    • immutable 可以声明和初始化一起做掉,也可以部署时在constructor中做掉。

    • immutable 必须在constructor运行截止时就赋值

    • immutable 不能用在引用数据类型上

      • 当前constant支持字符串,immutable不支持字符串

  • 应用场景

    • 在创建不可转移的 owner

    • 在创建 ERC20 的 name,symbol,decimals

声明和初始化一起做掉

像常量一样赋值:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    uint256 public constant U1 = 123;
    uint256 public immutable u2 = 456;
}

声明后在 constructor 中赋值

constructor 正常赋值

address public immutable adsImmut;
constructor() {
    adsImmut = msg.sender;
}

constructor 参数赋值

赋值 owner 例子:

address public immutable adsImmut;
constructor(address _owner) {
    adsImmut = _owner;
}

使用参数的属性:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract C {
    uint256 public immutable decimals;
    uint256 public immutable maxBalance;

    constructor(uint256 decimals_, address ref) {
        decimals = decimals_;
        // 可以访问属性
        maxBalance = ref.balance;
    }
}

constructor 内使用函数的结果赋值

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract C {
    uint256 public immutable decimals;

    constructor() {
        decimals = get();
    }

    function get() internal pure returns(uint256){
        return 18;
    }
}

部署后不可以修改

注意点:不可变量只能赋值一次,以后就不能再次改变了

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract A {
    address public immutable adsImmut;
    address public immutable ads = address(0);

    constructor() {
        adsImmut = msg.sender;

        // Immutable state variable already initialized.
        // ads = msg.sender;
    }

    // immutable 部署后不可以修改,如果尝试修改immutable类型变量,会报错
    // Cannot write to immutable here: Immutable variables can only
    // be initialized inline or assigned directly in the constructor.
    // function changeImmutable() external {
    //     adsImmut = msg.sender;
    // }
}

不可以在赋值前读取

并且在赋值之后才可以读取 immutable 变量,如果赋值之前读取会报错。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract A {
    address public immutable adsImmut;
    address public ads;

    constructor() {
        // Immutable variables cannot be read before they are initialized.
        // ads = adsImmut;

        adsImmut = msg.sender;
        ads = adsImmut;
    }
}

不可以在 constructor 之外赋值其他不可变量

不可变量可以在声明时赋值,不过只有在合约的构造函数执行时才被视为视为初始化。这意味着,你不能用一个依赖于不可变量的值初始化另一个不可变量。

因为不可变量在构造函数中才真正赋值,所以在合约的构造函数中这样做。这样的机制是为了防止对状态变量初始化和构造函数顺序的不同解释,特别是继承时,出现问题。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    uint256 public a1 = 1;
    uint256 public a2 = a1;

    uint256 public immutable b1 = 1;
    // Immutable variables cannot be read before they are initialized.
    // uint256 public b2 = b1;
    uint256 public b2;

    uint256 public immutable b3;

    constructor() {
        b2 = b1;
        b3 = b1;
    }
}

核心:在部署的时候确定变量的值,它是一个运行时赋值。

immutable 不能用在引用数据类型

当前constant支持引用类型中的 stringbytesimmutable不支持任何引用数据类型

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    string public constant TEXT1 = "abc";
    // string public immutable TEXT2 = "abc"; // ❌

    bytes public constant BYTES1 = "abc";
    // bytes public immutable BYTES2 = "abc"; // ❌

    // Immutable variables cannot have a non-value type.
    // mapping(address => uint256) public immutable mp;

    struct Book {
        string title;
        string author;
        uint256 book_id;
    }

    // Immutable variables cannot have a non-value type.
    // Book public immutable book1; // 一本书
}

不同状态变量的 gas 对比

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract A {
    // 23597 gas
    address public ads1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

    // 21486 gas
    address public constant MY_ADDRESS =
        0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

    // 21442 gas
    address public immutable adsImmut1 =
        0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

    // 21420 gas
    address public immutable adsImmut2;

    constructor() {
        adsImmut2 = msg.sender;
    }
}
  • 状态变量: 23597 gas

  • 常量: 21486 gas

  • 不可变量: 21442 gas

备注: 修改变量顺序,或者声明变量的数量,也会影响 gas 的消耗。可以自己亲自测试下。

通过对比可以发现,它既有 constant 常量不可修改和 Gas 费用低的优势,又有变量动态赋值的优势。

不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。很多时候常量的 gas 更低。下面是 uint8 类型的例子

contract Demo {
    // 23532 gas
    uint8 public u1 = 1;
    // 21377
    uint8 public constant U2 = 1;

    // 21421
    uint8 public immutable u2 = 1;

    // 21443
    uint8 public immutable u3;

    constructor(){
        u3  = 1;
    }
}

constant 和 immutable 总结

值的确定时机不同

状态变量声明为 constant (常量)或者 immutable (不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。

  • 对于 constant 常量, 他的值在编译器确定.

  • 对于 immutable, 它的值在部署时确定。

gas 不同

与常规状态变量相比,常量和不可变量的 gas 成本要低得多。

  • 对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。这样可以进行本地优化。

  • 不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。很多时候常量的 gas 更低。

如果可以使用常量的时候,推荐使用常量。

支持的数据不同

不是所有类型的状态变量都支持用 constantimmutable 来修饰

  • 当前constant仅支持值类型和引用类型中的 string 和 bytes

  • immutable仅支持值类型

变量名的命名规则

在为变量命名时,请记住以下规则:

  • 禁止使用保留关键字作为变量名。

  • 变量名首字母禁止使用数字,必须以字母或下划线开头。

  • 变量名大小写敏感。

禁止使用保留关键字作为变量名

禁止使用保留关键字作为变量名。例如:break / boolean / contract 这些是无效的变量名。

  • abstract

  • after

  • alias

  • apply

  • auto

  • case

  • catch

  • copyof

  • default

  • define

  • final

  • immutable

  • implements

  • in

  • inline

  • let

  • macro

  • match

  • mutable

  • null

  • of

  • override

  • partial

  • promise

  • reference

  • relocatable

  • sealed

  • sizeof

  • static

  • supports

  • switch

  • try

  • typedef

  • typeof

  • unchecked

变量名必须以字母或者下划线开头

禁止使用数字作为变量名的开始,例如:123abc 是一个无效的变量名,但是 _123abc 是一个有效的变量名。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    uint8 public abc = 1;
    uint8 public _123abc = 1;

    // Expected identifier but got 'ILLEGAL'
    uint8 public 123abc = 1;
}

变量名大小写敏感

例如:Name 和 name 是两个不同的变量。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    string public name = "Anbang";
    string public constant Name = "Anbang";
}

变量的可见性

可见性仅存在于状态变量和函数中

  • 局部变量的可见性仅限于定义它们的函数,函数有四可见型,分别是 private / external / internal / public

  • 状态变量可以有三种可见型,分别是 private / internal / public

我们这里重点介绍状态变量可见型。

internal 和 private 类型的变量不能被外部访问,而 public 变量能够被外部访问。

警告: 设置为 privateinternal,只能防止其他合约读取或修改信息,但它仍然可以在链外查看到。你不要想着通过设置可见型,让别人看不到你的代码。反编译可以得到大部分需要的逻辑。

private

private: 私有,仅在当前合约中可以访问

在继承的合约内不可访问,私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    uint8 private a = 1;

    // 这里使用 view 不是 pure
    // 不能因为 private 对外不可见就想着用pure,
    // 这个状态可变形是根据存储空间来决定的
    function getA() public view returns(uint8){
        return a;
    }
}

internal

internal: 内部可视(合约内部和被继承的子合约中都可视)

  • 状态变量如果不显示声明,默认是 internal 权限

  • 内部可见性状态变量只能在它们所定义的合约和派生合同中访问。 它们不能被外部访问。 这是状态变量的默认可见性。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    uint8 internal a = 1;

    function getA() public view returns(uint8){
        return a;
    }
}

external

external: 外部可视(合约外部可视,在内部是不可见)

external 不能声明在状态变量上,只能标识在函数上,因为如果一个状态变量在合约自身如果没有办法读取的话,那就没有存在的必要了。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
    uint8 external a = 1;
}

public

public: 公开可视(合约内部,被继承的,外部都可以调用)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    uint8 public a = 1;

    function getA() public view returns (uint8) {
        return a;
    }
}

public 自动生成 getter 函数

编译器自动为所有 public 状态变量创建 getter 函数.

值类型

例子:通过其他合约访问

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
    uint8 public a = 1;
}
contract Caller {
    Demo demo = new Demo();
    function f() public view returns(uint8 local){
        local = demo.a();
    }
}

例子:合约自身的两种访问

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
    uint8 public a = 1;
    function x() public returns(uint8 val){
        a = 3; // 内部访问
        val = this.a(); // 外部访问
    }
}

getter 函数具有外部(external)可见性。

  • 如果在内部访问 getter(即没有 this. ),它被认为一个状态变量。

  • 如果使用外部访问(即用 this. ),它被认作为一个函数。

引用类型

数组:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
  uint256[] public myArray=[1,2,3];

  // 指定生成的Getter 函数
//   function myArray(uint256 i) external view returns (uint256) {
//       return myArray[i];
//   }
}

字符串:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
  string public str = "abc";
  // 指定生成的Getter 函数
//   function str() external view returns(string memory s_){
//       return str;
//   }
}

mapping 和 struct 的 getter 函数

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    struct Data {
        uint256 a;
        bytes3 b;
        mapping (uint256 => uint256) map;
        uint256[3] c;
        uint256[] d;
        bytes e;
    }
    mapping (uint256 => mapping(bool => Data[])) public data;

    // function data(uint256 arg1, bool arg2, uint256 arg3)
    //     external
    //     returns (uint256 a, bytes3 b, bytes memory e)
    // {
    //     a = data[arg1][arg2][arg3].a;
    //     b = data[arg1][arg2][arg3].b;
    //     e = data[arg1][arg2][arg3].e;
    // }
}

在结构体内的映射和数组(byte 数组除外)被省略了,因为没有好办法为单个结构成员或为映射提供一个键。

全局:时间单位

在做时间相关业务时候可以使用 分钟,小时,天,周的概念。

基本用法

秒是缺省时间单位,可以不写,在时间单位之间,数字后面带有 secondsminuteshoursdaysweeks 可以进行换算,基本换算关系如下:

  • 1 == 1 seconds

  • 1 minutes == 60 seconds

  • 1 hours == 60 minutes

  • 1 days == 24 hours

  • 1 weeks == 7 days

之前老版本的合约还有 years 的概念,现在已经不再用了,从 0.5.0 版本不支持使用 years 了。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

// Time
contract Demo {
    // 定义全局变量
    uint256 public time;

    constructor() {
        time = 100000000;
    }

    function fSeconds() public view returns (uint256) {
        return time + 1 seconds;
    }

    function fMinutes() public view returns (uint256) {
        return time + 1 minutes;
    }

    function fHours() public view returns (uint256) {
        return time + 1 hours;
    }

    function fDays() public view returns (uint256) {
        return time + 1 days;
    }

    function fWeeks() public view returns (uint256) {
        return time + 1 weeks;
    }

    // 这些后缀不能直接用在变量后边。
    // 如果想用时间单位来将输入变量换算为时间,你可以用如下方式来完成:
    function testVar(uint256 daysValue) public view returns (uint256) {
        return time + daysValue * 1 weeks;
    }
}

两种时间逻辑

注意:由于闰秒造成的每年不都是 365 天、每天不都是 24 小时,所以如果你要使用这些单位计算日期和时间,请注意这个问题。因为闰秒是无法预测的,所以需要借助外部的预言机来对一个确定的日期代码库进行时间矫正。

时间在项目中有两种逻辑

  1. 像之前众筹合约里介绍的那样,使用持续时间来代表时间。比如持续 2 两小时结束,常见于众筹/拍卖合约

  2. 到某个时间点开始抢购活动,比如到 XX 年 XX 月 XX 日 XX 分 XX 秒,开启抢购,这种需要借助预言机才能正确完成

请按照自己的业务需求选择合适的时间方式。

全局:区块和交易属性

分别是 block / msg / tx 三个全局变量,因为功能相似,我们把 blockhash()gasleft() 这两个全局函数也一起介绍。

预览

名称 (返回值) 返回
block.basefee (uint256) 当前区块的基本费用( EIP-3198EIP-1559
block.chainid (uint256) 当前链 id
block.difficulty (uint256) 当前区块的难度
block.gaslimit (uint256) 当前区块的 gaslimit
block.number (uint256) 当前区块的 number
block.timestamp (uint256) 当前区块的时间戳,为 unix 纪元以来的秒
block.coinbase (address payable) 当前区块矿工的地址
msg.sender (address) 消息发送者 (当前 caller)
msg.value (uint256) 当前消息的 wei 值
msg.data (bytes calldata) 完整的 calldata
msg.sig (bytes4) calldata 的前四个字节
– (function identifier/即函数标识符)
tx.gasprice (uint256) 交易的 gas 价格
tx.origin (address) 交易的发送方 (完整的调用链)
blockhash(uint256 blockNumber) returns (bytes32) 给定区块的哈希值
– 只适用于 256 最近区块, 不包含当前区块
gasleft() returns (uint256) 剩余 gas

block

全局变量 block,除了 coinbase 属性的返回值是 address 类型,其他都是 uint256。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    // payable address 类型
    address payable public ads;

    function blockInfo() public
    returns (
        uint256,
        uint256,
        uint256,
        uint256,
        uint256,
        uint256,
        address
        )
    {
       uint256 basefee = block.basefee;
       uint256 chainid = block.chainid;
       uint256 difficulty = block.difficulty;
       uint256 gaslimit = block.gaslimit;
       uint256 number = block.number;
       uint256 timestamp = block.timestamp;

       // 除了 coinbase 返回值是 payable address 类型,其他都是 uint256
       address coinbase = block.coinbase;
       ads = block.coinbase; // 赋值给 payable address 的变量

       return (
           basefee,
           chainid,
           difficulty,
           gaslimit,
           number,
           timestamp,
           coinbase
       );
    }
}

msg

  • msg.sender: 只能赋值给普通 address 的变量

  • msg.value: 必须用在 payable 函数上

  • msg.data: 如果函数不接受参数 msg.data 等于 sig

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract Demo {
    address payable public ads1;
    address public ads2;

    function msgInfo(uint256 u_)
        public
        payable
        returns (
            uint256,
            bytes memory,
            bytes4
        )
    {
        ads2 = msg.sender; // 只能赋值给普通 address 的变量
        uint256 value = msg.value; // 必须用在 payable 函数上
        bytes memory data = msg.data; // 如果函数不接受参数 msg.data 等于 sig
        bytes4 sig = msg.sig;
        return (value,data,sig);
    }
}

tx

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract Demo {
    address payable public ads1;
    address public ads2;

    function txInfo() public returns (uint256) {
        ads2 = tx.origin; // 只能赋值给普通 address 的变量
        uint256 gasprice = tx.gasprice;
        return gasprice;
    }
}

blockhash()

这是获取最近 256 个区块高度的 hash 值,如果获取当前的区块 hash 是没办法获取到的。(因为当前的数据还没有被打包,没有被确定)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract Demo {
    uint256 public blockNumber;

    function blockInfo1() public returns (uint256, bytes32) {
        blockNumber = block.number;
        bytes32 hashCurrent = blockhash(blockNumber); // 不包含当前区块

        return (block.number, hashCurrent);
    }

    function blockInfo2() public view returns (uint256, bytes32) {
        bytes32 hash = blockhash(blockNumber);
        // errored: Key not found in database [h n]
        return (block.number, hash);
    }
}

gasleft()

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract Demo {
    function gas() public view returns (uint256) {
        return gasleft();
    }
}

已经删除的全局变量

名称 返回
block.blockhash 给定区块的哈希值;在 0.5.0 版本中删除,现使用blockhash()获取
now(uint256) block.timestamp 的别名;在 0.7.0 版中删除
msg.gas 剩余 gas; 在 0.5.0 版中删除,现使用 gasleft() 获取

使用注意

不要依赖 block.timestampblockhash 产生随机数,除非你已经知道后果,并且后果对项目没有影响。

时间戳和区块哈希在一定程度上都可能受到挖矿矿工影响。例如,某个抽奖合约使用 block.timestampblockhash 产生随机数,但是恶意矿工可以进行一直发起,如果他们没有中奖就取消交易,只需重试不同的交易就可以刷出中奖的交易并且发出去。

当前区块的时间戳会严格大于最后一个区块的时间戳,但是这里唯一能确保的: 它会是在权威链上的两个连续区块的时间戳之间的数值。

问答题

  • 不同类型的变量初始默认值是什么?

      string public str; // ""
      bool public b; // false
      uint256 public count; // 0
      int256 public intValue; // 0
      address public ads; // 0x0000000000000000000000000000000000000000
      bytes32 public bt32; // 0x0000000000000000000000000000000000000000000000000000000000000000
    
  • 聊一聊变量的作用域

    • Solidity 中的作用域规则遵循了 C99:变量将会从它们被声明之后可见,直到一对 {} 块的结束。对于参数形式的变量(例如:函数参数、修饰器参数、catch 参数等等)在其后接着的代码块内有效。 这些代码块是函数的实现,catch 语句块等。作为一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到 for 循环的结束。

    • 那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。这意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。

  • 聊一聊状态变量/局部变量/全局变量(3 种变量状态)。

    • 状态变量:

      • 变量值永久保存在智能合约存储空间中,相当于属于已经写入到区块链中,可以随时调用,除非该条链消失。

      • 特点:定义在智能合约的存储空间中

    • 局部变量:

      • 变量值仅在函数执行过程中有效,供函数内部使用;调用函数时,在虚拟机的内存中;函数退出后,变量无效。

      • 特点: 定义在函数内部

    • 全局变量:

      • 保存在全局命名空间,用于获取区块链相关信息的特殊变量。

      • 特点:不用定义,直接获取即可。

  • Constant 常量使用时有哪些需要注意的?

    • 普通的状态变量,添加 constant 关键词即可声明为常量

    • 与常规状态变量相比,常量的 gas 要低很多。

    • 常量的名字,一般使用全大写的。

    • 常量赋值后不可以修改。

    • 常量必须声明和初始化一起做掉,否则编译不通过。

      • 对于 constant 常量, 他的值在编译器确定,不能定义在函数内

    • 不是所有的类型都支持常量,当前支持的仅有值类型(包括地址类型)/字符串

    • 编译器并不会为 constant 常量在 storage 上预留空间

      • 编译器不会为这些变量预留存储位,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。

    • 也可以在文件级别定义 constant 变量

      • 注:0.7.2 之后加入的特性。

    • 可以使用内建函数赋值常量

  • Immutable 不可变量使用时有哪些需要注意的?

    • 赋值方式: immutable 修饰的变量是在部署的时候确定变量的值,它在构造函数中赋值一次之后,以后就不不能再次改变了,它是一个运行时赋值。同时也带来更多的安全性。

    • 特点:它既有 constant 常量不可修改和 Gas 费用低的优势,又有变量动态赋值的优势。

      • Solidity immutable 是另一种常量的表达方式。

    • 原则:

      • immutable 可以声明和初始化一起做掉,也可以部署时在constructor中做掉。(必须在constructor运行截止时就赋值,也支持在构造函数内使用普通函数的结果来赋值)

      • immutable 不能用在引用数据类型上(当前constant支持字符串,immutable不支持字符串)

    • 应用场景

      • 在创建不可转移的 owner

      • 在创建 ERC20 的 name,symbol,decimals

  • 状态变量/constant/Immutable 三者的 gas 区别

    • 对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。这样可以进行本地优化。

    • 不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。

    • 状态变量声明为 constant (常量)或者 immutable (不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。

    • 对于 constant 常量, 他的值在编译器确定.

    • 对于 immutable, 它的值在部署时确定。

    • 与常规状态变量相比,常量和不可变量的 gas 成本要低得多。

    • 对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。这样可以进行本地优化。

    • 不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。

    • 不是所有类型的状态变量都支持用 constantimmutable 来修饰,当前constant仅支持 字符串/值类型,immutable仅支持值类型

  • 状态变量的可见性又哪些?

    • 三种,注意 external 不能声明在状态变量上,只能标识在函数上

  • 四种可见性的使用区别

    • 需要注意继承性

    • 需要注意 external 不能声明在状态变量上,只能标识在函数上

    • public 自动生成 getter 函数

  • 不同类型的 getter 函数是什么样子的?

    • 编译器自动为所有 public 状态变量创建 getter 函数。

    • array/mapping/struct 类型的 getter 函数有参数。

  • 全局时间单位有哪些?使用时候有没有需要注意的事情?

    • 由于闰秒造成的每年不都是 365 天、每天不都是 24 小时 leap seconds,如果想要和线下时间堆砌,需要预言机进行时间矫正。

  • 全局的区块和交易属性有哪些?分别返回什么?可以用来做什么?使用时候有没有什么需要注意的?

    • 不要依赖 block.timestampblockhash 产生随机数,除非你知道自己在做什么。

  • 全部的 ABI 编码及解码函数有哪些?分别什么作用?

    • abi.encode 会补零

    • abi.encodePacked 不会补零(不补零,容易导致碰撞错误。(两个参数拼在一起,导致参数不同,结果相同))

    • abi.decode

    • abi.encodeWithSignature

    • abi.encodeWithSelector

    • abi.encodeCallabi.encodeWithSelector 一致;执行完整的类型检查, 确保类型匹配函数签名。