Solidity入门(3)

引用类型:

引用类型可以通过多个不同的名称修改它的值,而值类型的变量,每次都有独立的副本。因此,必须比值类型更谨慎地处理引用类型。 目前,引用类型包括结构,数组和映射,如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:

  • 内存memory 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。存贮在内存中
  • 存储storage 状态变量保存的位置,只要合约存在就一直存储.存储在区块链中
  • 调用数据calldata 用来保存函数参数的特殊数据位置,是一个只读位置。调用数据是不可修改的

赋值行为:

  • 在 存储storage 和 内存memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
  • 从 内存memory 到 内存memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
  • 从 存储storage 到本地存储变量的赋值也只分配一个引用。
  • 其他的向 存储storage 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面 ArrayContract 合约 更容易理解)。
  • pragma solidity >=0.5.0 <0.9.0;
    
    contract Tiny {
        uint[] x; // x 的数据存储位置是 storage, 位置可以忽略
    
        // memoryArray 的数据存储位置是 memory
        function f(uint[] memory memoryArray) public {
            x = memoryArray; // 将整个数组拷贝到 storage 中,可行
            uint[] storage y = x;  // 分配一个指针(其中 y 的数据存储位置是 storage),可行
            y[7]; // 返回第 8 个元素,可行
            y.pop(); // 通过 y 修改 x,可行
            delete x; // 清除数组,同时修改 y,可行
    
            // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,
            // 但 storage 是“静态”分配的:
            // y = memoryArray;
            // 下面这一行也不可行,因为这会“重置”指针,
            // 但并没有可以让它指向的合适的存储位置。
            // delete y;
    
            g(x); // 调用 g 函数,同时移交对 x 的引用
            h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
        }
    
        function g(uint[] storage ) internal pure {}
        function h(uint[] memory) public pure {}
    }

    数组

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

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组(二维数组),应声明为 uint[][5]  。

数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反。

如:如果有一个变量为 uint[][5] memory x, 要访问第三个动态数组的第二个元素,使用 x[2][1],要访问第三个动态数组使用 x[2]。 同样,如果有一个 T 类型的数组 T[5] a , T 也可以是一个数组,那么 a[2] 总会是 T 类型。

访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push() 方法在末尾追加一个新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。

bytes 和 string 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 调用数据calldata 和 内存memory 中会被“紧打包” .string 与 bytes 相同,但不允许用长度或索引来访问。如果使用一个长度限制的字节数组,应该使用一个 bytes1 到 bytes32 的具体类型,因为它们GAS便宜得多。

如果想要访问以字节表示的字符串 s,请使用 bytes(s).length 或者bytes(s)[7] = 'x';

创建内存数组

pragma solidity >=0.4.16 <0.9.0;

contract TX {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);

//assert断言函数,用于在调试过程中捕捉程序的错误
        assert(a.length == 7);
        assert(b.length == len);

        a[6] = 8;
    }
}

数组成员:

length:

表示当前数组的长度。 一经创建,内存数组的大小就是固定的(但却是动态的,也就是说,它可以根据运行时的参数创建)。

push():

动态的存储数组以及 bytes 类型( string 类型不可以)都有一个 push() 的成员函数,它用来添加新的零初始化元素到数组末尾,并返回元素引用. 因此可以这样:  x.push().t = 2 或 x.push() = b.  

通过push增加数组长度会消耗gas费用

push(x):

动态的存储 数组以及 bytes 类型( string 类型不可以)都有一个 push(x) 的成员函数,用来在数组末尾添加一个给定的元素,这个函数没有返回值.

pop:

变长的存储 数组以及 bytes 类型( string 类型不可以)都有一个 pop 的成员函数, 它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用delete

数组切片

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

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

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

切片数组可以隐式转换为其“背后”类型的数组,并支持索引访问。 索引访问也是相对于切片的开始位置。 数组切片没有类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。

实例1:

pragma solidity >=0.6.99 <0.9.0;

contract Proxy {
    /// 被当前合约管理的 客户端合约地址
    address client;

    constructor(address _client) {
        client = _client;
    }

    /// 在进行参数验证之后,转发到由client实现的 "setOwner(address)"
    function forward(bytes calldata _payload) external {
      // 由于 ABI 解码要求填充的数据(padded data)不能使用
        // abi.decode(_payload[:4], (bytes4)).
        bytes4 sig =
            _payload[0] |
            (bytes4(_payload[1]) >> 8) |
            (bytes4(_payload[2]) >> 16) |
            (bytes4(_payload[3]) >> 24);

        if (sig == bytes4(keccak256("setOwner(address)"))) {
            address owner = abi.decode(_payload[4:], (address));
            require(owner != address(0), "Address of owner cannot be zero.");
        }
        (bool status,) = client.delegatecall(_payload);
        require(status, "Forwarded call failed.");
    }
}

结构体

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

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

简化的众筹实例:

pragma solidity >=0.6.0 <0.9.0;

    
  // 定义的新类型包含两个属性。
  // 在合约外部声明结构体可以使其被多个合约共享。 在这里,这并不是真正需要的。
  struct Funder {
      address addr;
      uint amount;
  }

  contract CrowdFunding {

    // 也可以在合约内部定义结构体,这使得它们仅在此合约和衍生合约中可见。
    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }

    uint numCampaigns;
    mapping (uint => Campaign) campaigns;

    function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID 作为一个变量返回

        // 不能使用 "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
        // 因为RHS会创建一个包含映射的内存结构体 "Campaign"
        Campaign storage c = campaigns[campaignID];
        c.beneficiary = beneficiary;
        c.fundingGoal = goal;
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 以给定的值初始化,创建一个新的临时 memory 结构体,
        // 并将其拷贝到 storage 中。
        // 注意你也可以使用 Funder(msg.sender, msg.value) 来初始化。
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

映射:

映射类型在声明时的形式为 mapping(_KeyType => _ValueType)

其中 _KeyType 可以是任何基本类型,即可以是任何的内建类型, bytes 和 string 或合约类型、枚举类型。 而其他用户定义的类型或复杂的类型如:映射、结构体、即除 bytes 和 string 之外的数组类型是不可以作为 _KeyType 的类型的。

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

映射可以视作哈希表,它们在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的默认值 。然而下面是映射与哈希表不同的地方: 在映射中,实际上并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。 正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有其他信息键的信息是无法被删除

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

可见性和 getter 函数

由于 Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为“消息调用”,而外部调用则会产生一个 EVM 调用), 函数和状态变量有四种可见性类型。 函数可以指定为 external ,public ,internal 或者 private。 对于状态变量,不能设置为 external ,默认是 internal 。

  • external

外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。 当收到大量数据的时候,外部函数有时候会更有效率,因为数据不会从calldata复制到内存.

  • public

public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数(见下面)。

  • internal

这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用。

  • private

private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

        在下面的示例中,MappingExample 合约定义了一个公共balances映射,键类型为 address,值类型为 uint, 将以太坊地址映射为 无符号整数值。 由于uint是值类型,因此getter返回与该类型匹配的值, 可以在 MappingLBC 合约中看到合约在指定地址返回该值。 

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingLBC {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(this);
    }
}

实例2【ERC20token简单版】 

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract MappingExample {

    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }

    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        _transfer(sender, recipient, amount);
        approve(sender, msg.sender, amount);
        return true;
    }

    function approve(address owner, address spender, uint256 amount) public returns (bool) {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

        _balances[sender] -= amount;
        _balances[recipient] += amount;
        emit Transfer(sender, recipient, amount);
    }
}

涉及LValues的运算符:

a+=e 等同于 a=a+e。 其它运算符 -=, *=, /=, %=, |=, &= 以及 ^= 都是如此定义的。 a++ 和 a-- 分别等同于 a+=1 和 a-=1,但表达式本身的值等于 a 在计算之前的值。 与之相反,--a 和 ++a 虽然最终 a 的结果与之前的表达式相同,但表达式的返回值是计算之后的值。

基本类型转化:

例如, uint8 可以转换成 uint16int128 转换成 int256,但 int8 不能转换成 uint256 (因为 uint256 不能涵盖某些值,例如,-1)。

//如果一个类型显式转换成更小的类型,相应的高位将被舍弃
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678

//如果将整数显式转换为更大的类型,则将填充左侧(即在更高阶的位置)。 转换结果依旧等于原来整数
uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now
assert(a == b);

//定长字节数组转换则有所不同, 他们可以被认为是单个字节的序列和转换为较小的类型将切断序列
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b 为 0x12

//如果将定长字节数    组显式转换为更大的类型,将按正确的方式填充。 以固定索引访问转换后的字节将在和之前的值相等 (如果索引仍然在范围内):
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b 为 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

//因为整数和定长字节数组在截断(或填充)时行为是不同的, 如果整数和定长字节数组有相同的大小,则允许他们之间进行显式转换, 如果要在不同的大小的整数和定长字节数组之间进行转换 ,必须使用一个中间类型来明确进行所需截断和填充的规则:
bytes2 a = 0x1234;
uint32 b = uint16(a); // b 为 0x00001234
uint32 c = uint32(bytes4(a)); // c 为 0x12340000
uint8 d = uint8(uint16(a)); // d 为 0x34
uint8 e = uint8(bytes1(a)); // e 为 0x12

地址类型:

通过校验和测试的正确大小的十六进制字面常量会作为 address 类型。没有其他字面常量可以隐式转换为 address 类型。

从 bytes20 或其他整型显示转换为 address 类型时,都会作为 address payable 类型。

一个地址 address a 可以通过``payable(a)`` 转换为  address payable 类型.

猜你喜欢

转载自blog.csdn.net/weixin_49489840/article/details/124643736