深入理解Solidity——引用类型

引用类型(Reference Types)

引用类型比上述的值类型需要更加小心地处理。因为复制拷贝他们可能开销相当大, 我们必须考虑把它们存储在内存(这不是持久化)或者存储器(状态变量存放的地方)。

数据存储位置(Data location)

每一个复杂类型,即数组Array和结构体Struct,有一个额外的注解——“数据存储位置”,关系到它是放在在内存还是存储器中。根据上下文会产生一个默认存储位置,但是可以通过storagememory 关键字修饰该类型来改变。

  • 函数参数,包括返回值的默认数据存储位置是memory
  • 局部变量的默认数据存储位置是storage
  • 状态变量默认数据存储位置强制为storage

其实还有第三个数据存储位置calldata,它是不可修改非持久化函数参数存储区域。外部函数的函数参数(不包括返回值)被强存储在calldata,其表现与memory很相似。

数据存储位置很重要,因为它们会改变赋值的方式

  • 存储和内存之间的赋值甚至状态变量总是会创建独立副本。
  • 本地存储变量的赋值只能指定一个引用,并且该引用始终指向状态变量,即使后者在此期间发生更改。
  • 将一个内存存储的引用类型赋值给另一个内存存储引用类型不会创建副本。
pragma solidity ^0.4.0;

contract C {
    uint[] x; // x的数据存储位置是storage

    // memoryArray的数据存储位置是memory
    function f(uint[] memoryArray) public {
        x = memoryArray; // 可以将整个数组拷贝到storage
        var y = x; // 赋值一个指针,y的数据存储位置是storage
        y[7]; // 这样写没有问题, 返回第8个元素
        y.length = 2; // 这样写没有问题, 通过y修改x
        delete x; // 这样写没有问题, 清空一个数组, 并且改变y
        // 以下不起作用; 它需要在storage中创建一个新的临时的 
        // 未命名数组, 但是storage需要"静态"分配:
        // y = memoryArray;
        // T这个也不起作用, 因为这会"重置"指针, 但是没有明显
        // 的位置可以指向.
        // delete y;
        g(x); // 调用g,移交对x的引用
        h(x); // 调用h并在内存中创建一个独立的临时副本
    }

    function g(uint[] storage storageArray) internal {}
    function h(uint[] memoryArray) public {}
}

总结

强制数据存储位置:

  • 外部函数参数(不包括返回值):calldata
  • 状态变量:storage

默认数据存储位置:

  • 函数参数和返回值:memory
  • 其他所有局部变量:storage

数组(Arrays)

数组可以声明时指定长度,也可以是动态长度。对storage存储的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,结构体等。但对于memory的数组来说。如果作为public函数的参数,它不能是映射类型的数组,只能是支持ABI的类型。

一个元素类型为T,固定长度为k的数组,可以声明为T[k],而一个动态大小(变长)的数组则声明为T[]
还可以声明一个多维数组,如声明一个类型为uint的数组长度为5的变长数组(5个元素都是变长数组),可以声明为uint[][5]。(注意,相比非区块链语言,多维数组的长度声明是反的。)

为了访问第三动态数组中的第二个元素, 必须使用x[2][1](下标是从0开始的,访问模式和声明模式正好相反, 即x[2]是从右边剔除了一阶)

bytesstring是一种特殊的数组。bytes类似byte[],但在外部函数作为参数调用中,bytes会进行压缩打包。string类似bytes,但目前不提供长度和下标的访问方式。

所以应该尽量使用bytes而不是byte[],因为开销更小。

注解
如果你想访问字符串s的某个字节,要使用bytes(s).length/bytes(s)[7]='x';。注意此时通过下标访问获取到的不是对应字符,而是UTF-8编码!

类型为数组的状态变量,可以标记为public,从而让Solidity创建一个getter,如果要访问数组的某个元素,指定下标就好了。

创建内存数组(Allocating Memory Arrays)

可使用new关键字创建一个memory的数组。与stroage数组不同的是,你不能通过.length的长度来修改数组大小属性。

pragma solidity ^0.4.16;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        // Here we have a.length == 7 and b.length == len
        a[6] = 8;
    }
}

数组字面量及内联数组(Array Literals / Inline Arrays)

数组字面量是一个还没有赋值到变量的数组表达式

pragma solidity ^0.4.16;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) public pure {
        // ...
    }
}

通过数组字面量,创建的数组是memory的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如[1, 2, 3]的类型是uint8[3] memory,因为这些常量中的每一个的类型都是uint8

因此,有必要将上例中的第一个元素转换为uint。 请注意,目前固定大小的memory数组不能分配给动态大小的memory数组,即以下情况是不可能的:

// 这将不能编译

pragma solidity ^0.4.0;

contract C {
    function f() public {
    // 下一行发生一个type error
    // 因为固定大小的uint[3],不能转换为动态大小的uint[]。
        uint[] x = [uint(1), 3, 4];
    }
}

已经计划在未来移除这样的限制,但由于数组在ABI中的传递方式,所以现在还比较棘手。

成员

length属性

数组有一个.length属性,表示当前数组中元素的个数。动态数组可以通过改变.length来在storage里来调整size。memory里的动态数组不支持。

不能通过访问超出当前数组的长度的下标来自动实现改变数组长度。‘

memory数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整。但如果是动态数组,则取决于运行时参数。

push方法

storage的动态数组和bytes都有一个push方法(string没有),用于附加新元素到数据末端,返回值为新的length

警告
当前在外部函数中,不能使用多维数组。
警告
由于EVM的局限,不可能在外部函数调用中返回动态内容contract C { function f() returns (uint[]) { ... } } 里的合约函数f 使用web3.js调用,将有返回值,但使用Solidity调用,就没有返回值。现在唯一的解决方法是使用较大的静态尺寸大小的数组
pragma solidity ^0.4.16;

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // 这里不是两个动态数组的数组,而是一个动态数组里,每个元素是长度为二的数组。
    bool[2][] m_pairsOfFlags;
    // newPairs 存在 memory里,因为是函数参数
    function setAllFlagPairs(bool[2][] newPairs) public {
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 访问不存在的index会抛出异常
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 如果新size更小, 移除的元素会被销毁
        m_pairsOfFlags.length = newSize;
    }

    function clear() public {
        // 销毁
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // 同销毁一样的效果
        m_pairsOfFlags.length = 0;
    }

    bytes m_byteData;

    function byteArrays(bytes data) public {
        // byte arrays ("bytes") are different as they are stored without padding,
        // but can be treated identical to "uint8[]"
        m_byteData = data;
        m_byteData.length += 7;
        m_byteData[3] = byte(8);
        delete m_byteData[2];
    }

    function addFlag(bool[2] flag) public returns (uint) {
        return m_pairsOfFlags.push(flag);
    }

    function createMemoryArray(uint size) public pure returns (bytes) {
        // memory动态数组通过new创建:
        uint[2][] memory arrayOfPairs = new uint[2][](size);
        // 创建一个动态字节数组:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(i);
        return b;
    }
}

结构体(Structs)

Solidity提供struct来定义自定义类型,自定义的类型是引用类型。
我们看看下面的例子:

pragma solidity ^0.4.11;

contract CrowdFunding {
    // 定义一个包含两个成员的新类型
    struct Funder {
        address addr;
        uint amount;
    }

    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }

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

    function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID 作为一个变量返回
        // 创建一个结构体实例,存储在storage ,放入mapping里
        campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 用mapping对应项创建一个结构体引用
        // 也可以用 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;
    }
}

上面是一个简化版的众筹合约,但它可以让我们理解structs的基础概念,struct可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。

不能声明一个struct同时将自身struct作为成员,这个限制是基于结构体的大小必须是有限的。但struct可以作为mapping的值类型成员。

注意在函数中,将一个struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值的同时,会影响到原变量。

当然,也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如campaigns[campaignID].amount = 0

上一篇:深入理解Solidity——值类型

下一篇:深入理解Solidity——映射

你可能感兴趣的:(Solidity文档翻译系列,以太坊去中心化应用开发)