状态变量与容器

状态变量用于在区块链存储上永久存储状态值,是 Liquid 合约重要的组成部分。在HelloWorld 合约中,我们已经初步接触了状态变量的定义方式及容器的使用方式。 在 Liquid 合约中,状态变量与容器的关系极为密切,我们将在本节中分别对两者进行介绍。

状态变量

Liquid 中使用结构体语法(struct)对状态变量定义进行封装,并且该结构体需要使用liquid(storage)属性进行标注,以告知 Liquid 该结构体中包含了状态变量的定义,例如:

1
2
3
4
#[liquid(storage)]
struct HelloWorld {
    name: storage::Value<String>,
}

在上述代码中可以看出,结构体中每个成员各自对应一个状态变量的定义。状态变量的名称位于冒号:的左侧,而类型位于右侧,状态变量定义之间使用英语逗号,分隔。虽然在合约的设计上,状态变量name的实际类型应当为String,但是在定义时需要实际类型包裹于容器类型storage::Value中。之所以要使用容器类型,是因为状态变量实际上是区块链存储系统某一存储位置的引用,对状态变量的读取、写入都需要转化为对区块链存储系统的读取、写入,这是状态变量区别于其他普通变量最重要的差异。容器是连接智能合约与区块链底层平台的桥梁,Liquid 通过容器替封装了区块链存储系统的访问细节,使得能够像使用普通变量一般使用状态变量。若没有使用容器封装状态变量的实际类型,将会引发编译时报错,例如:

1
2
3
4
#[liquid(storage)]
struct HelloWorld {
    name: String,
}

所有容器的定义均位于liquid_lang库的storage模块中,需要预先引入该模块:

1
2
use liquid_lang as liquid;
use liquid::storage;

用于封装状态变量定义的结构体在合约中能且仅能出现一次,因此不能将状态变量定义分散在不同的、用#[liquid(storage)]属性标注的结构体中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#[liquid::contract]
mod hello_world {
    #[liquid(storage)]
    struct HelloWorld {
        ...
    }

    #[liquid(storage)]
    struct Another {
        ...
    }
}

#[liquid(storage)]属性标注的结构体中至少需要一个状态变量定义,因此不能将其定义为unit 类型;同时,由于每个状态变量均需要一个有效的名称,也不能将其定义为元组类型。此外,不能为被#[liquid(storage)]属性标的结构体声明任何模板参数,即不能在该结构体中使用泛型,也不能为其添加任何可见性声明。下列代码了展示部分错误的使用方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Unit is not allowed.
#[liquid(storage)]
struct HelloWorld();

// Tuple is not allowed.
#[liquid(storage)]
struct HelloWorld(u8, u32);

// Generic is not allowed.
#[liquid(storage)]
struct HelloWorld<T, E> {
    ...
}

// Visibility is not allowed.
#[liquid(storage)]
pub struct HelloWorld {
    ...
}

但是可以在状态变量的定义之前添加pub可见性声明:

1
2
3
4
#[liquid(storage)]
pub struct HelloWorld {
    pub name: storage::Value<String>,
}

pub可见性代表外界可以直接访问该状态变量,Liquid 会自动为此类状态变量生成一个公开的访问器。关于访问器的更多细节可参考合约方法一节。但是除了pub可见性以外,其他种类的可见性声明均不能使用。

容器

Liquid 中的容器包括单值容器(Value)、向量容器(Vec)、映射容器(Mapping)及可迭代映射容器(IterableMapping)。

注意

Liquid中所有容器均没有实现拷贝语义,因此无法拷贝容器。同时,Liquid限制了您不能移动容器的所有权,因此在合约中只能通过引用的方式使用容器。

单值容器

单值容器的类型定义为Value<T>。当状态变量类型定义为单值容器时,可以像使用普通变量一般使用该状态变量。使用单值容器时需要通过模板参数传入状态变量的实际类型,如Value<bool>Value<String>等。基本容器提供下列方法:

  • pub fn initialize(&mut self, input: T)
    
  • 用于在合约构造函数中使用提供的初始值初始化单值容器。此方法应当只在构造函数中使用,且只使用一次。若状态变量初始化后再次调用initialize方法将不会产生任何效果。

  • pub fn set(&mut self, new_val: T)
    
  • 用一个新的值更新状态变量的值。

  • pub fn mutate_with<F>(&mut self, f: F) -> &T
    where
        F: FnOnce(&mut T),
    
  • 允许传入一个用于修改状态变量的函数,在修改完状态变量后,返回状态变量的引用。当状态变量未初始化时,调用mutate_with会引发运行时异常并导致交易回滚。

  • pub fn get(&self) -> &T
    
  • 返回状态变量的只读引用。

  • pub fn get_mut(&mut self) -> &mut T
    
  • 返回状态变量的可变引用,可以通过该可变引用直接修改状态变量的值。

除了上述基本接口外,单值容器还通过实现core::ops中的运算符 trait,对大量的运算符进行了重载,从而能够直接使用容器进行运算。单值容器重载的运算符包括:

运算符 trait 功能 备注
* Deref 解引用 通过容器的只读引用返回 &T ,借助 Rust 语言的 解引用强制多态 ,可以像操作普通变量那样操单值容器。例如:若状态变量 name 的类型为 Value<String>,如需获取 name 的长度,则可以直接使用 name.len()
* DerefMut 解引用 通过容器的可变引用返回 &mut T
+ Add 需要 T 自身支持双目 + 运算,例如:若状态变量 len 的类型为 Value<u8>,则可以直接使用 len + 1
+= AddAssign 加并赋值 需要 T 自身支持 += 运算
- Sub 需要 T 自身支持双目 - 运算
-= SubAssign 减并赋值 需要 T 自身支持 -= 运算
* Mul 需要 T 自身支持 * 运算
*= MulAssign 乘并赋值 需要 T 自身支持 *= 运算
/ Div 需要 T 自身支持 / 运算
/= DivAssign 除并赋值 需要 T 自身支持 /= 运算
% Rem 求模 需要 T 自身支持 % 运算
%= RemAssign 求模并赋值 需要 T 自身支持 %= 运算
& BitAnd 按位与 需要 T 自身支持 & 运算
&= BitAndAssign 按位与并赋值 需要 T 自身支持 &= 运算
| BitOr 按位或 需要 T 自身支持 | 运算
|= BitOrAssign 按位或并赋值 需要 T 自身支持 |= 运算
^ BitXor 按位异或 需要 T 自身支持 ^ 运算
^= BitXorAssign 按位异或并赋值 需要 T 自身支持 ^= 运算
<< Shl 左移 需要 T 自身支持 << 运算
<<= ShlAssign 左移并赋值 需要 T 自身支持 <<= 运算
>> Shr 右移 需要 T 自身支持 >> 运算
>>= ShrAssign 右移并赋值 需要 T 自身支持 >>= 运算
- Neg 取负 需要 T 自身支持单目 - 运算
! Not 取反 需要 T 自身支持 ! 运算
[] Index 下标运算 需要 T 自身支持按下标进行索引
[] IndexMut 下标运算 同上,但是用于可变引用上下文中
==、!=、>、>=、<、<= PartialEq、PartialOrd、Ord 比较运算 需要 T 自身支持相应的比较运算

向量容器

向量容器的类型定义为Vec<T>。当状态变量类型为向量容器时,可以像使用动态数组一般的方式使用该状态变量。在向量容器中,所有元素按照严格的线性顺序排序,可以通过元素在序列中的位置访问对应的元素。使用向量容器时需要通过模板参数传入元素的实际类型,如Vec<bool>Vec<String>等。向量容器提供下列方法:

  • pub fn initialize(&mut self)
    
  • 用于在构造函数中初始化向量容器。若向量容器初始化后再调用initialize接口,则不会产生任何效果。

  • pub fn len(&self) -> u32
    
  • 返回向量容器中元素的个数。

  • pub fn is_empty(&self) -> bool
    
  • 检查向量容器是否为空。

  • pub fn get(&self, n: u32) -> Option<&T>
    
  • 返回向量容器中第n个元素的只读引用。若n越界,则返回None

  • pub fn get_mut(&mut self, n: u32) -> Option<&mut T>
    
  • 返回向量容器中第n个元素的可变引用。若n越界,则返回None

  • pub fn mutate_with<F>(&mut self, n: u32, f: F) -> Option<&T>
    where
        F: FnOnce(&mut T),
    
  • 允许传入一个用于修改向量容器中第n个元素的值的函数,在修改完毕后,返回该元素的只读引用。若n越界,则返回None

  • pub fn push(&mut self, val: T)
    
  • 向向量容器的尾部插入一个新元素。当插入前向量容器的长度等于 232 - 1 时,引发 panic。

  • pub fn pop(&mut self) -> Option<T>
    
  • 移除向量容器的最后一个元素并将其返回。若向量容器为空,则返回None

  • pub fn swap(&mut self, a: u32, b: u32)
    
  • 交换向量容器中第a个及第b个元素。若ab越界,则引发 panic。

  • pub fn swap_remove(&mut self, n: u32) -> Option<T>
    
  • 从向量容器中移除第n个元素,并将最后一个元素移动至第n个元素所在的位置,随后返回被移除元素的引用。若n越界,则返回None;若第n个元素就是向量容器中的最后一个元素,则效果等同于pop接口。

同时,向量容器实现了以下 trait:

  • impl<T> Extend<T> for Vec<T>
    {
        fn extend<I>(&mut self, iter: I)
        where
            I: IntoIterator<Item = T>;
    }
    
  • 按顺序遍历迭代器,并将迭代器所访问的元素依次插入到向量容器的尾部。

  • impl<'a, T> Extend<&'a T> for Vec<T>
    where
        T: Copy + 'a,
    {
        fn extend<I>(&mut self, iter: I)
        where
            I: IntoIterator<Item = &'a T>;
    }
    
  • 按顺序遍历迭代器,并将迭代器所访问的元素依次插入到向量容器的尾部。

  • impl<T> core::ops::Index<u32> for Vec<T>
    {
        type Output = T;
    
        fn index(&self, index: u32) -> &Self::Output;
    }
    
  • 使用下标对序列中的任意元素进行快速直接访问,下标的类型为u32,返回元素的只读引用。若下标越界,则会引发运行时异常并导致交易回滚。

  • impl<T> core::ops::IndexMut<u32> for Vec<T>
    {
        fn index_mut(&mut self, index: u32) -> &mut Self::Output;
    }
    
  • 使用下标对序列中的任意元素进行快速直接访问,下标的类型为u32,返回元素的可变引用。若下标越界,则会引发运行时异常并导致交易回滚。

向量容器支持迭代。在迭代时,需要先调用向量容器的iter方法生成迭代器,并配合for ... in ...等语法完成对向量容器的迭代,如下列代码所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#[liquid(storage)]
struct Sum {
    value: storage::Vec<u32>,
}

...

pub fn sum(&self) -> u32 {
    let mut ret = 0u32;
    for elem in self.value.iter() {
        ret += elem;
    }
    ret
}

注意

向量容器的长度并不能无限增长,其上限为2 32 - 1(4294967295,约为42亿)。

映射容器

映射容器的类型定义为Mapping<K, V>。映射容器是键值对集合,当状态变量类型为映射容器时,能够通过键来获取一个值。使用映射容器时需要通过模板参数传入键和值的实际类型,如Mapping<u8, bool>Mapping<String, u32>等。映射容器提供下列方法:

  • pub fn initialize(&mut self)
    
  • 用于在构造函数中初始化映射容器。若映射容器初始化后再调用initialize接口,则不会产生任何效果。

  • pub fn len(&self) -> u32
    
  • 返回映射容器中元素的个数。

  • pub fn is_empty(&self) -> bool
    
  • 检查映射容器是否为空。

  • pub fn insert<Q>(&mut self, key: &Q, val: V) -> Option<V>
    
  • 向映射容器中插入一个由keyval组成的键值对,注意key的类型为一个引用。当key在之前的映射容器中不存在时,返回None;否则返回之前的key所对应的值。

  • pub fn mutate_with<Q, F>(&mut self, key: &Q, f: F) -> Option<&V>
    where
        K: Borrow<Q>,
        F: FnOnce(&mut V),
    
  • 允许传入一个用于修改映射容器中key所对应的值的函数,在修改完毕后,返回值的只读引用。若key在映射容器中不存在,则返回None

  • pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
    where
        K: Borrow<Q>,
    
  • 从映射容器中移除key及对应的值,并返回被移除的值。若key在映射容器中不存在,则返回None

  • pub fn get<Q>(&self, key: &Q) -> Option<&V>
    
  • 返回映射容器中key所对应的值的只读引用。若key在映射容器中不存在,则返回None

  • pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
    
  • 返回映射容器中key所对应的值的可变引用。若key在映射容器中不存在,则返回None

  • pub fn contains_key<Q>(&self, key: &Q) -> bool
    
  • 检查key在映射容器中是否存在。

同时,映射容器实现了以下 trait:

  • impl<K, V> Extend<(K, V)> for Mapping<K, V>
    {
        fn extend<I>(&mut self, iter: I)
        where
            I: IntoIterator<Item = (K, V)>;
    }
    
  • 按顺序遍历迭代器,并将迭代器所访问的键值对依次插入到映射容器中。

  • impl<'a, K, V> Extend<(&'a K, &'a V)> for Mapping<K, V>
    where
        K: Copy,
        V: Copy,
    {
        fn extend<I>(&mut self, iter: I)
        where
            I: IntoIterator<Item = (&'a K, &'a V)>;
    }
    
  • 按顺序遍历迭代器,并将迭代器所访问的键值对依次插入到映射容器中。

  • impl<'a, K, Q, V> core::ops::Index<&'a Q> for Mapping<K, V>
    where
        K: Borrow<Q>,
    {
        type Output = V;
    
        fn index(&self, index: &'a Q) -> &Self::Output;
    }
    
  • 以键为索引访问映射容器中对应的值,索引类型为&Q,返回值的只读引用。若索引不存在,则会引发运行时异常并导致交易回滚。

  • impl<'a, K, Q, V> core::ops::IndexMut<&'a Q> for Mapping<K, V>
    where
        K: Borrow<Q>,
    {
        fn index_mut(&mut self, index: &'a Q) -> &mut Self::Output;
    }
    
  • 以键为索引访问映射容器中对应的值,索引类型为&Q,返回值的可变引用。若索引不存在,则会引发运行时异常并导致交易回滚。

注意

映射容器的容量大小并不能无限增长,其上限为2 32 - 1(4294967295,约为42亿)。

注意

映射容器不支持迭代,如果需要迭代映射容器,请使用可迭代映射容器。

可迭代映射容器

可迭代映射容器的类型定义为IterableMapping<K, V>,其功能与映射容器基本类似,但是提供了迭代功能。使用可迭代映射容器时需要通过模板参数传入键和值的实际类型,如IterableMapping<u8, bool>IterableMapping<String, u32>等。

可迭代映射容器支持迭代。在迭代时,需要先调用可迭代映射容器的iter方法生成迭代器,迭代器在迭代时会不断返回由键的引用及对应的值的引用组成的元组,可配合for ... in ...等语法完成对可迭代映射容器的迭代,如下列代码所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#[liquid(storage)]
struct Sum {
    values: storage::IterableMapping<String, u32>,
}

...

pub fn sum(&self) -> u32 {
    let mut ret = 0u32;
    for (_, v) in self.values.iter() {
        ret += v;
    }
    ret
}

此外,可迭代映射容器的insert方法与映射容器略有不同,其描述如下:

  • pub fn insert(&mut self, key: K, val: V) -> Option<V>
    
  • 其功能与映射容器的insert方法相同,但是其参数并不K类型的引用。

注意

可迭代映射容器的容量大小并不能无限增长,其上限为2 32 - 1(4294967295,约为42亿)。

注意

为实现迭代功能,可迭代映射容器在内部存储了所有的键,且受限于区块链特性,这些键不会被删除。因此,可迭代容器的性能及存储开销较大,请根据应用需求谨慎选择使用。