Stable变量和升级方法:

互联网计算机的一个关键特性是它能够使用 WebAssembly 内存和全局数据库而不是传统的数据库来持久存储canister智能合约的状态。这意味着,在没有明确用户指令的情况下,每条消息前后都会魔术般地恢复到canister中的整个状态。这种自动的、用户透明的状态保持叫做正交持久性。

虽然方便,正交持久性提出了一个挑战,当涉及到升级的canister的代码。如果没有一个明确的表示canister的状态,如何从旧的canister转移任何应用程序数据?

为了适应升级而不丢失数据,需要一些新的设备来将canister中的关键数据迁移到升级后的canister中。例如,如果您希望部署用户注册容器的新版本以修复问题或添加功能,则需要确保现有注册在升级过程中幸存下来。

互联网计算机的持久化模型允许存储器将这些数据保存并恢复到专用的稳定存储器中,不同于普通的存储器,这种存储器可以在升级过程中保留,允许存储器将大量数据传输到它的替换存储器中。

对于用 Motoko 编写的应用程序,该语言提供了利用 Internet Computer稳定内存保存状态的高级支持。这种更高级别的特性称为稳定存储(stable storage),其设计目的是适应对应用程序数据和用于生成应用程序代码的 Motoko 编译器的更改。

能否利用稳定的存储取决于应用程序编程人员能否预测和指出升级后需要保留的数据。根据应用程序的不同,您决定持久存储的数据可能是某个给定actor的状态的一部分、全部或者没有。

声明stable变量:

在actor中,可以通过使用 stable 关键字作为变量声明中的修饰符,指定一个变量用于稳定存储(在 Internet Computer稳定内存中)。

更准确地说,actor中的每个 let 和 var 变量声明都可以指定变量是稳定的还是灵活的。如果不提供修饰符,则默认情况下将变量声明为灵活的。

下面是一个简单的例子,说明如何声明一个稳定的计数器,可以升级,同时保留计数器的值:

actor Counter {

  stable var value = 0;

  public func inc() : async Nat {
    value += 1;
    return value;
  };
}
Note
你只能在actor的作用域中使用stableflexible修饰符和letvar这些声明。您不能在程序的其他任何地方使用这些修饰符。

Typing:

因为编译器必须确保稳定变量与升级后的替换程序兼容并且有意义,下面的类型限制适用于稳定状态:

  • 每个stable变量都必须有一个稳定类型

若类型是共享的(shared),则类型是稳定的,如果该类型是通过忽略其中的任何 var 修饰符获得的。("where a type is stable if the type obtained by ignoring any var modifiers within it is shared.")

因此,稳定类型和共享类型之间的唯一区别是前者支持突变。与共享类型一样,稳定类型仅限于一阶数据(first-order data),不包括由本地函数(如对象)构建的本地函数和结构。这种对函数的排除是必要的,因为一个由数据和代码组成的函数值的含义不能轻易地在升级过程中保留下来,而普通数据的含义(可变或不可变)则可以保留下来。

Note
通常,对象类型不稳定,因为它们可以包含局部函数。但是,稳定数据的普通记录是稳定对象类型的特殊情况。此外,对actor和共享函数的引用也是稳定的,允许您在升级过程中保留它们的值。例如,可以保留状态记录一组actor或订阅服务的共享函数回调。

stable变量如何被upgrade?

当您第一次编译和部署一个canister时,actor中所有灵活和稳定的变量都按顺序初始化。当您使用upgrade模式部署一个canister时,actor以前版本中存在的所有稳定变量都使用其旧值预先初始化。当稳定变量用它们以前的值初始化后,剩余的灵活的和新增加的稳定变量按顺序初始化。

Preupgrade and postupgrade system methods:

声明一个变量为stable也要求它的类型是稳定的。由于并非所有类型都是稳定的,因此有些变量不能声明为stable

作为一个简单的例子,从正交持久性的讨论来考虑 Registry actor:

import Text "mo:base/Text";
import Map "mo:base/HashMap";

actor Registry {

  let map = Map.HashMap<Text, Nat>(10, Text.equal, Text.hash);

  public func register(name : Text) : async () {
    switch (map.get(name)) {
      case null {
        map.put(name, map.size());
      };
      case (?id) { };
    }
  };

  public func lookup(name : Text) : async ?Nat {
    map.get(name);
  };
};

await Registry.register("hello");
(await Registry.lookup("hello"), await Registry.lookup("world"))

这个actor将顺序标识符分配给 Text 值,使用基础map对象的大小(map.size())来确定下一个标识符。与其他actor一样,它依赖于正交持久性来维护调用之间的 hashmap 状态。

我们想使Registery可升级,而不会失去任何现有的注册记录。

不幸的是,它的状态 map 具有包含成员函数(例如 map.get)的对象类型,因此 map 变量本身不能声明为stable

对于这种仅使用stable变量无法解决的情况,Motoko 支持用户定义的升级钩子(hooks),如果提供了,则可以在升级前后立即运行。这些升级钩子允许您在不受限制的灵活变量之间将状态迁移到更受限制的稳定变量。这些钩子被声明为具有特殊名称的preupgradepostupgrade系统函数。两个函数都必须具有类型: ()→()。

preupgrade函数允许您在运行时将稳定变量的值提交到 Internet Computer稳定内存并在进行升级之前,对稳定变量进行最终更新。postupgrade函数在升级初始化了用来替换的actor(包括其稳定变量)之后,但在对该actor执行任何共享函数调用(或消息)之前运行。

在这里,我们引入了一个新的 stable 变量 entry,用于保存和恢复不稳定哈希表的条目:

import Text "mo:base/Text";
import Map "mo:base/HashMap";
import Array "mo:base/Array";
import Iter "mo:base/Iter";

actor Registry {

  stable var entries : [(Text, Nat)] = [];

  let map = Map.fromIter<Text,Nat>(
    entries.vals(), 10, Text.equal, Text.hash);

  public func register(name : Text) : async () {
    switch (map.get(name)) {
      case null  {
        map.put(name, map.size());
      };
      case (?id) { };
    }
  };

  public func lookup(name : Text) : async ?Nat {
    map.get(name);
  };

  system func preupgrade() {
    entries := Iter.toArray(map.entries());
  };

  system func postupgrade() {
    entries := [];
  };
}

注意,entries的类型(只是 Text 和 Nat 对的数组)确实是一个稳定类型。

在这个示例中,preupgrade系统函数只是在entries保存到稳定内存之前将当前map entries写入到entries中。在 mapentries初始化(let map =Map.fromIter(entries.vals(), 10, Text.equal, Text.hash);)之后,postupgrade系统函数将entries重置为空数组。

升级已编译的程序:

在你部署了一个包含适当的stable变量的 Motoko 程序或者preupgrade和postupgrade系统方法之后,你可以使用带有--mode=upgrade选项的 dfx canister install 命令来升级已部署的代码。

如:(注意,upgrade前需要对源代码对出修改)

dfx canister install --all --mode upgrade

有关升级已部署程序的信息,请参阅Upgrade a canister

Unknown-2
- Motoko -

A Student on the way to full stack of Web3.