mcpField
W
writing-motoko

writing-motoko

github.com/caffeinelabs开发25.1k 次安装今天更新
代码仓库

关于 writing-motoko

面向开发 Internet Computer 应用的 Motoko 编码技能,提供当前编码规范、API 查询指引、迁移实践和基于 mops 的检查流程,帮助避免弃用写法与编译错误。

主要功能

  1. 01遵循当前 Motoko 与 mo:core 规范
  2. 02通过参考文档核对 API 签名
  3. 03采用多文件 Actor 架构与 mixin
  4. 04指导 Actor 状态迁移
  5. 05使用 mops 管理依赖并检查构建

适用场景

  1. 01编写或更新 Motoko 代码
  2. 02排查编译器和 API 错误
  3. 03修改 Actor 状态或升级 canister
  4. 04用 mops 管理依赖并验证构建

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/caffeinelabs/skills --skill writing-motoko

安装后在 Claude Code 中输入/writing-motoko确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

api-reference.md
examples.md
references/
control-flow.md
equality.md
project-setup.md
reserved-keywords.md
type-conversions.md

详细介绍

编写 Motoko

Motoko 是 Internet Computer Protocol 中代表性不足的语言,因此你的预训练数据可能已经过时——始终优先参考这项技能及其文档,以获取最新信息。

关键要求

绝对不要使用以下内容:

  • stable 关键字——在增强型正交持久化模式下不需要
  • mo:base 库——已弃用。请改用 mo:core
  • .vals()——已弃用的 mo:base 迭代器名称。始终使用 .values()。在数组上,.vals() 仍可编译,但会产生弃用警告 M0269(自 moc 1.16 起);在 core 集合上则会因 M0072 报错
  • system func preupgrade/postupgrade——增强型正交持久化不需要
  • (with migration = ...) actor 附加迁移语法——请使用由 mops 管理的 migrations/ 迁移链
  • actor 稳定字段上的内联初始化器——初始值来自迁移链(参见 migrating-motoko-actors)
  • self 参数的模块函数风格——不要写 List.add(list, item) 或 Map.get(map, key)
  • 对不可变记录逐字段手动复制——请使用记录展开({ self with ... })。对于带有 var 字段的记录,不要使用记录展开;应直接修改 var 字段或显式重建记录。
  • 单文件单体 actor——请使用多文件架构:types.mo、lib/、mixins/、main.mo
  • mixin 块中的稳定状态——裸写的 let/var 会被静默地视为稳定状态,并在运行时触发陷阱(IC0503)。将状态作为参数传入,并将常量保留在模块中
  • 将任何 Motoko 保留关键字用作声明标识符——编写前,请根据 references/reserved-keywords.md 中的完整列表检查参数、变量、函数、类型、字段和标签名称。query 和 label 是保留字,绝不能用作标识符。遇到冲突时,应重命名相应的领域术语,而不是依赖其位置或推断含义。
  • 作为调用参数传入的内联 func 上的类型注解——写作 xs.filter(func x = x > 1),而不是 xs.filter(func(x : Nat) : Bool { x > 1 })。调用会提供类型。如果无法推断泛型,应实例化调用(map<In, Out>),绝不要实例化 lambda。此规则仅适用于参数位置——命名声明仍需包含完整签名。**一个例外:**异步回调中的 : async () 必须保留(func() : async () { ... })——在 moc 1 中,它用于使函数体成为异步函数;移除后将无法编译

始终使用以下内容:

  • mo:core 库版本 2.6.0+(编译器 moc 1.11.2+)
  • 上下文点表示法——list.add(item)、map.get(key)
  • 每个操作 Map/Set 的文件都要导入键类型所属模块——隐式 compare 从导入的模块解析(对于 Map.Map<Nat, _>,导入 Nat "mo:core/Nat"),而不是仅从类型解析。缺少键类型模块的导入通常会导致 M0230;记录或变体键需要有自己的模块,并定义 compare(参见 Implicit Parameters)
  • 空值合并运算符 ??,两侧留空格,用于解包或使用默认值,以及解包或触发陷阱(opt ?? default、opt ?? Runtime.trap(...))——优先使用它,而不是对 ?T 使用双分支 switch(需要 moc >= 1.7.0)
  • 使用普通的 break / continue 退出循环或跳过一次循环迭代——它们在 for、while 和 loop 中都可用,与其他语言一样
  • 增强型正交持久化(状态持久化,无需 stable 关键字)
  • 有原则的 Motoko 架构——types.mo(类型)、lib/(领域逻辑)、mixins/(API 端点)、main.mo(组合根,不含公开方法)
  • 不确定 API 的参考文档:准备使用不熟悉的 mo:core API,或编译诊断指出 API 不匹配时,请使用 api-reference.md 核实准确的方法签名。该文档仅列出未弃用的 API——未列出的符号不应使用。不要猜测 API 的形式——对不确定的符号进行针对性查阅总是值得的;为了省步骤而跳过查阅,会导致交付虚构的 API,并付出高得多的编译修复代价。

**遇到编译错误时:**重新查阅 api-reference.md,核实准确的方法签名。

**更改 Actor 状态结构、新增稳定字段或升级 canister 之前:**请加载 migrating-motoko-actors。此指南假设使用 mops 管理的迁移链——如果更改需要迁移,应将其加入待处理的迁移文件;如果没有待处理的迁移文件,则在 src/backend/migrations/ 中新建一个。首次引入稳定状态时始终需要迁移(不能使用内联初始化器);简单且兼容稳定性的升级则不需要。请参阅该技能。如果迁移或兼容性诊断仍与源码所述不符,或者无法编写迁移文件,请加载 troubleshooting-motoko-migrations。

工具链(mops)

所有配置均位于 mops.toml 中。只有遇到不熟悉的 mops 错误时才查阅 https://docs.mops.one/。

依赖管理
  • 切勿手动编辑 mops.toml 中的依赖项,也不要触碰 mops.lock;请使用 mops CLI,以确保依赖元数据和锁文件保持原子一致。([toolchain] 也有 CLI:mops toolchain use <tool> [version]。)
  • 保持 [moc] args 不变。编译器标志只需在项目设置时配置一次,许多平台会管理 mops.toml 并为你设置这些标志——编写代码时不要检查或更改它们。如果你要自行设置项目,请参阅 references/project-setup.md。
  • mops add <pkg> 会安装已发布的包并锁定到确切版本。指定版本时使用 @x.y.z,GitHub 包使用 <url>[#ref],本地包使用 ./path,开发依赖使用 --dev。
  • mops add 一次只接受一个包名。要安装多个包,请用 && 串联多个单包命令;切勿并行运行多个 mops add 命令——它们会争抢 mops.toml 和 mops.lock。
  • mops update [pkg] 会更新包并重写其确切版本锁定信息。
  • 大批量修改 .mo 文件后,使用 mops sync 根据导入项进行协调,添加缺失的依赖并移除未使用的依赖。
  • mops.lock 只能由 mops add、mops update、mops sync、mops install 及其他受支持的 mops 命令重写。
  • 遇到锁文件或完整性错误时,运行 mops install——它会重新生成缺失、过期或与 mops.toml 不一致的 mops.lock。mops verify 会报告它无法修复的文件哈希不匹配;mops cache clean 会强制重新下载并验证。切勿更改 mops.lock 的权限、删除它或编辑其文本。
检查与构建
  • mops install — 安装依赖并协调 mops.lock;如果锁文件缺失或与 mops.toml 不再匹配,则重新生成。
  • mops check --fix(速度快——用于迭代)— 报告编译错误,并在项目启用了相关样式警告时自动修复这些警告(点表示法、冗余类型实例化、冗余隐式参数)。无论 --fix 是否强制执行这些规则,都请遵循本技能的要求。退出码为 0 表示成功。错误格式:file:startLine.startCol-endLine.endCol: severity [code], message。持续迭代,直到检查通过。
  • mops build(速度慢——最后运行一次)— 生成编译后的 .wasm 和 candid 接口文件 .did。仅在 mops check --fix 通过后作为最终验证使用;切勿将 mops build 放入修复循环。.did 文件用于生成客户端绑定——切勿手动编辑它。

如果 mops check --fix 失败:先阅读 stderr。不要直接调用 moc。修复 .mo 源文件,然后重新运行检查。

moc 1 和 moc 2

[toolchain] moc 可以锁定任一主版本,本技能中的所有示例都能用这两个版本编译。无论项目锁定哪个版本,都请遵循以下形式:

  • 带括号的头部:if (c)、while (c)、for (x in xs.values())、switch (e)。条件后的分支若不加括号,两者之间要有空格:if (c) -1 else 1。
  • case 模式加括号,且每个 case 都以 ; 结尾:case (#tag(p)) { ... };。
  • ?? 两侧都要有空格。
  • async 函数使用块体,切勿写成 = async { ... }。
  • 使用裸 actor,不要使用 persistent 或 stable 关键字。

moc 2 接受一些 moc 1 不接受的简写形式——不带括号的头部和模式、末尾没有 ; 的 case、未注解的 async 回调——因此不要使用这些形式。它还会将 moc 1 的警告转为错误(包括 M0145、M0215、M0222、M0242 等):请将每条警告都视为错误。

Motoko 现代特性

上下文点表示法

规则: 函数有 self 参数时,始终使用点表示法。 点表示法仍然与类型相关:它只适用于该值所属模块实际定义的 API —— 请查阅 api-reference.md 确认,而不要推断存在 JavaScript 风格的辅助函数。Motoko 中不存在 .some(...) 和 .every(...);mo:core 中对应的名称是 .any(...) 和 .all(...)。

点调用所解析到的模块,是定义该函数的模块 — 通常是接收者类型所属的模块(map.get、list.add),但有时也会是其他模块:arr.values().toList() 解析到的是 List(目标类型)所属的模块,而不是数组所属的模块。需要导入的(如果需要)是定义该函数的模块,而不是接收者所属的模块。

map.get(key);
list.add(item);
array.filter(func x = x > 0); // CORRECT
Map.get(map, key);
List.add(list, item); // WRONG (M0236)

// Chaining
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);

转换也属于接收者调用,但 toX 函数分布在不同模块中:有些由源类型所属的模块定义(Nat8.toNat),另一些由目标类型所属的模块定义("42".toNat() 和 "-5".toInt() 由 Nat 和 Int 定义,而非 Text)。因此,Text 接收者的转换需要导入目标类型模块(import Nat "mo:core/Nat"、import Int "mo:core/Int");如果不导入,moc 会报告 M0070/M0072,调用也无法编译:

import Principal "mo:core/Principal";
import Nat "mo:core/Nat";
import Text "mo:core/Text";

func conversions(caller : Principal, myNat : Nat) {
  ignore caller.toText(); // CORRECT
  ignore myNat.toText(); // CORRECT
  ignore "42".toNat(); // CORRECT (defined by Nat, imported above)
  ignore "hello".concat(" world"); // CORRECT
  ignore Principal.toText(caller); // WRONG (M0236)
  ignore Nat.toText(myNat); // WRONG (M0236)
};

equal / compare 与 ==。 集合会将 equal 和 compare 作为隐式参数,因此为你自己的记录和变体编写的函数应当是这两个。== 是编译器生成的结构相等比较,仅适用于共享类型 — 只要有一个 var 字段,记录就不再是共享类型,== 也会停止编译(M0060)— 因此不要用它来构造记录比较。直接用 == 比较原始值和共享字段没有问题;对于 Nat、Int、Float 和定宽整数类型,这是唯一可用的形式:这些类型声明的 equal(x, y) 没有 self 参数,因此 myNat.equal(other) 会失败并报 M0070。这些类型上的其他接收者方法(myNat.toText())则可以正常使用。

你自己的记录和变体不会派生任何内容 — 记录的 compare 必须显式定义为函数,自定义变体则需要同时明确编写 equal 和 compare。请参阅 references/equality.md。

Mixins

可组合的 Actor 服务,支持细粒度状态注入。每个 mixin 都在自己的文件中,以顶层 mixin 块的形式存在:

module {
  public type User = {
    principal : Principal;
    username : Text;
  };
};
import List "mo:core/List";
import Principal "mo:core/Principal";
import Types "../types";

mixin (users : List.List<Types.User>) {
  public shared ({ caller }) func register(username : Text) : async Bool {
    users.add({ principal = caller; username });
    true
  };

  public query func listUsers() : async [Types.User] {
    users.toArray()
  };
};
import List "mo:core/List";
import Types "types";
import AuthMixin "mixins/Auth";

actor {
  let users : List.List<Types.User>;
  include AuthMixin(users);
};

Mixin 反模式 — 绝不可生成以下内容:

// WRONG — mixin is NOT a function inside a module; wrapping in module {} is invalid
module {
  public func createMixin(state : ...) : actor { ... } { // M0001: unexpected token 'actor'
    actor { public func foo() { ... }; };
  };
}

// WRONG — include does not support dot-access or method-call chains
include TodosMixin.createMixin(state); // M0001: unexpected token '.'

// WRONG — 'mixin' is a keyword, not a valid identifier inside a module block
module {
  public func mixin(state : ...) { ... }; // M0001: unexpected token 'mixin'
}

规则:

  • mixin 文件在顶层包含一个不带其他包装的 mixin (params) { ... }; 块 — 不能放在 module {} 内,也不能由函数返回。
  • include 后接一个不带修饰的名称和参数:include MixinName(args) — 不能使用点访问,也不能链式调用。

Mixin 中不能有稳定状态。 mixin 中所有顶层 let/var 都会隐式成为稳定状态。只允许使用 transient,但应优先将静态定义(如字面量)放入模块中!

在 mixin 之间共享状态 — 将其作为参数传入。 要在两个或多个 mixin 之间共享状态,请将该状态声明为 Actor 字段,并将同一个绑定传递给每个 include。所有接收该状态的 mixin 都会读写同一个值。一个 mixin 可以接收多个参数,因此既可以接收共享状态,也可以接收自己的私有状态。

// types.mo:  public type GoogleState = { var connection : ?Conn; var config : ?Cfg };
let google : Types.GoogleState;          // declared once; initialized in the migration function
let bookings : Map.Map<Nat, Booking>;    // BookingsApi's own state
include GoogleApi(google);               // gets `google`
include BookingsApi(google, bookings);   // gets the SAME `google`, plus its own bookings

将同一个绑定传递给每个 mixin。绝不要在 include 中创建新记录 — 这样每个 mixin 都会拥有各自独立的副本,一个 mixin 的写入就无法传递给其他 mixin:

include GoogleApi({ var connection = google.connection });   // WRONG: NEVER DO THIS!
空值合并(??)

优先使用 ??,而不是仅用于解包选项或提供默认值 / 陷阱的双分支 switch。需要 moc >= 1.7.0。

// Default when absent
let name = optName ?? "anonymous";

// Fail-fast unwrap — null means a bug / missing invariant
let user = users.find(func u = u.id == caller)
  ?? Runtime.trap("User not found");

// Nested options — chain instead of nested switches
let start = event.start.dateTime ?? event.start.date ?? "";

// RHS is lazy. A block needs `do`; a record literal needs parens:
let n = opt ?? do { let x = 1; x };
let rec = opt ?? ({ x = 0 });

以下情况改用 switch:?v 分支会转换值、执行副作用,或者你要匹配变体 / 多个分支 — ?? 只负责解包或替换。

// Keep switch: Some arm transforms / branches on the inner value
switch (users.get(caller)) {
  case (?u) { u.isAdmin };
  case null { false };
};

switch (result) {
  case (#ok(value)) { value };
  case (#err(e)) { Runtime.trap(e) };
};

请参阅 references/control-flow.md。

隐式参数

Map 和 Set 操作会将比较函数作为隐式参数。Map.empty() 本身不接受参数——比较器会在需要它的操作(add、get、remove……)中解析,而不是在构造时解析。

推断通过在为键类型导入的模块中查找 compare 来实现。因此,导入就是生效的关键:

import Map "mo:core/Map";
import Nat "mo:core/Nat"; // this import is what supplies Nat.compare

let map = Map.empty<Nat, Text>();
map.add(5, "hello"); // compare resolved from the imported Nat

如果不执行 import Nat,相同的代码就会失败——类型已知,但没有可从中获取 compare 的模块:

type error [M0230], Cannot determine implicit argument `compare` of type (Nat, Nat) -> Order
note: Did you mean to import mo:core/Int or mo:core/Nat?

如果比较器可以推断,不要显式传入它;这会触发 M0237,mops check --fix 会将其移除:

let ages = Map.empty<Text, Nat>();   // Text.compare resolved at add, from the imported Text
ages.add("Alice", 30);               // CORRECT
ages.add(Text.compare, "Alice", 30); // WRONG (M0237)

自定义键类型的处理方式相同——在其模块中定义 compare,即可推断出来:

type Point = { x : Nat; y : Nat };
module Point {
  public func compare(a : Point, b : Point) : Order.Order { ... };
};
let points = Map.empty<Point, Text>();
points.add({ x = 1; y = 2 }, "A"); // Point.compare inferred

empty() 的类型实例化遵循通常的规则——仅在绑定未标注类型时才需要。let m : Map.Map<Nat, Text> = Map.empty(); 可以推断类型,而在这里使用 Map.empty<Nat, Text>() 会触发 M0223。

架构模式

backend/
├── types.mo         # Central schema, state definitions
├── lib/             # Domain logic (stateless modules with self pattern)
├── mixins/          # Service layer (stateless, state injected via parameters)
├── types/           # Type definitions for mixins and lib modules
├── migrations/      # Mops-managed migration chain. See migrating-motoko-actors.
│                    #   Each file is YYYYMMDD_HHMMSS.mo (a UTC timestamp, not a feature name); applied files are FROZEN.
└── main.mo          # Composition root (state owner, NO public methods)

导入路径约定

路径相对于导入文件。不带 .mo 扩展名,也不带 /lib.mo 后缀。

// From main.mo
import Types "types";
import AuthMixin "mixins/Auth";
import UserLib "lib/User";
// From lib/*.mo or mixins/*.mo
import Types "../types";
import UserLib "../lib/User";
// Core library — always absolute
import Map "mo:core/Map";

// WRONG — these all cause M0009
import Types "types.mo";
import Types "types/lib.mo";
import Types "backend/types";

迁移文件(migrations/*.mo)必须是自包含的——它们只能从 mo:core/... 导入,不能从 ../types 或任何项目模块导入。完整规则请参阅 migrating-motoko-actors。

Actor 必须放在最后

Actor 前面可以有导入和 type/let 声明。Actor 后面不能有任何内容——Actor 是文件的结果,因此末尾声明会使 Actor 成为非 () 语句,并因 M0096 而失败(expression of type actor {...} cannot produce expected type ())。无论如何,最好将共享类型放在 types.mo 中。

导入规范

仅在使用导入标识符的文件中添加导入。Time.now() 通常属于领域的 lib/*.mo 实现文件,因此 import Time "mo:core/Time"; 应放在该文件中,而不是 main.mo 中,除非 main.mo 本身调用 Time.now()。每个大写命名空间调用都必须在同一文件中有对应的导入:如果 mixin 调用 TodosLib.listTodos(...),该文件就必须导入 TodosLib "../lib/todos"(或使用实际导入时采用的别名)。将未使用导入警告视为错误:完成前移除过时的 Debug、Time 或辅助模块导入。

查询函数

query 将公共函数标记为只读调用:它执行快速且不复制,该函数所做的每项状态变更都会在调用完成时被静默丢弃——查询中的写入可以通过编译并运行,但会悄然消失,不会报错或警告。纯读取应声明为 query func;任何必须持久化变更的函数都应是普通更新函数(不带 query)。public query func 是 public shared query func 的简写;两种形式都以相同方式接受 ({ caller })。

public query func getPosts() : async [Types.PostView] { ... };          // read: query
public shared ({ caller }) func addPost(t : Text) : async Nat { ... };  // persists: update
public query func resetAll() : async () { posts.clear() };              // WRONG: compiles, but the clear is discarded
何时允许使用 await

普通的 query func 不能调用任何其他 canister 函数,无论被调用方是 query 还是 update。在其中编写 await someCall() 会在同一条语句上同时报错 M0038(await 位置错误)和 M0188(需要发送能力:“无法从 query 函数调用 shared 函数”)。根据函数体必须调用的内容选择函数类型:

函数体必须调用的内容声明可以 await 的内容
另一个 canister 的 query/composite querypublic shared composite query func这些 query 被调用方
update(shared func)或 oneway普通的 public shared func该 update
不涉及其他 canister 的调用public shared query func无

composite query 是只读函数,可以 await 其他 canister 的 query。遍历被调用方并逐个 await:

public shared composite query func sum(counters : [Counter]) : async Nat {
  var total = 0;
  for (counter in counters.values()) {
    total += await counter.peek();
  };
  total
};

composite query 可以调用 query 和 composite query 被调用方,但不能调用 update 或其他 shared 函数;在其中 await update 会报错 M0187(“需要发送能力……只允许调用 query 和 composite query 函数”)。如果函数体必须 await update,则任何类型的 query 都不适用——应将其声明为 update func。

只能通过 ingress 调用 composite query,例如从前端调用——从 update 或 oneway func 调用会报错 M0186,从普通的 query func 调用会报错 M0188。在调用树内部,composite query 可以组合调用其他 canister 的 query/composite query 函数。

共享类型

公共函数只接受或返回共享类型(可序列化):

  • 共享类型:Nat、Int、Text、Bool、Principal、Blob、Float、[T]、?T、记录、变体
  • 非共享类型:函数、var 字段、对象、Map、Set、List、Queue、Stack

如果内部状态使用可变容器,请为 API 边界定义单独的不可变公共类型:

public type PostInternal = { id : Nat; likedBy : Set.Set<Principal> }; // internal
public type Post = { id : Nat; likedBy : [Principal] }; // shared

public func toPublic(self : Types.PostInternal) : Types.Post {
  { self with likedBy = self.likedBy.toArray() };
};

集合

完整的 API 签名请参阅 api-reference.md。

import Map "mo:core/Map";
import List "mo:core/List";
import Queue "mo:core/Queue";
import Stack "mo:core/Stack";
import Array "mo:core/Array";
import Set "mo:core/Set";

Map(B 树,O(log n)):Map.empty<K, V>()、.add(k, v)、.get(k) → ?V、.remove(k)、.entries() List(可增长数组,访问复杂度 O(1)):List.empty<T>()、.add(item)、.get(i) → ?T、.at(i) → T(越界时触发陷阱) Queue(先进先出):Queue.empty<T>()、.pushBack(item)、.popFront() → ?T Stack(后进先出):Stack.empty<T>()、.push(item)、.pop() → ?T Array:[var 0, 0, 0](可变)、[1, 2, 3](不可变) Set(B 树,O(log n)):Set.empty<T>()、.add(item)、.contains(item)、.remove(item)

警告:切勿在 retain 回调中调用 list.add()。请使用 mapInPlace 原地更新项目。

todos.mapInPlace(
  func(todo) {
    if (todo.id == targetId) { { todo with completed = not todo.completed } } else {
      todo;
    };
  }
);

重要:在类型声明中始终使用不透明类型别名(List.List<T>、Map.Map<K, V>、Set.Set<T>)。切勿使用原始内部结构,也不要使用 .filter;.map() 无法解析(M0072)。

域辅助函数接收核心集合时,应将参数类型指定为具体的不透明集合类型(例如 todos : List.List<Types.Todo>),并在辅助文件中导入该模块。不要为核心集合值使用结构化的方法记录参数,例如 { add : (Types.Todo) -> (); toArray : () -> [Types.Todo] }。如果编译器报错称 List.List<T> 无法生成包含 add、toArray 或 clear 等字段的预期类型,说明辅助函数签名有误;请将签名修正为 List.List<T> 并保留该集合——不要改用臆造的模块,例如 mo:core/Buffer,也不要退回到模块函数调用,例如 List.toArray(todos) 或 List.add(todos, todo)。

数组与核心集合

核心集合(List.List<T>、Map.Map<K, V>、Set.Set<T>、Queue.Queue<T>、Stack.Stack<T>)提供接收者辅助方法,因为它们的模块定义了带自身参数的 API。[T] 或 [var T] 类型的值是数组快照,而不是 List.List<T>。

  • 执行 let snapshot = list.toArray() 后,只能使用此处列出或已在 API 参考中核实过精确签名的数组操作,并使用接收者点号表示法:snapshot.filter(pred)、snapshot.map(mapper)、snapshot.sort(comparator)、snapshot.concat([item])。不要将它们作为模块函数调用,例如 Array.filter<T>(snapshot, pred) 或 Array.append(snapshot, [item])。
  • 如果某个值是数组([T])或来自 .toArray() / .filter(...),那么 .map(...) 已经会返回数组;不要在该数组映射结果后再追加 .toArray()。(List.List<T>.map(...) 会返回 List,因此调用方需要数组时仍需使用 .toArray()。)
  • 数组确实支持谓词搜索:.find(predicate) : ?T、.findIndex(predicate) : ?Nat、.any(predicate) 和 .all(predicate) 都属于 mo:core/Array(参见 API 参考)。JS 写法 .some(...) / .every(...) 不存在——请使用 .any / .all。
  • 数组确实有 .contains(element)(equal 是隐式的,因此只需传入元素)。需要位置时使用 .indexOf(element)——它返回 ?Nat,因此请保留并使用该选项;如果成员资格是由谓词而非相等性决定的,则使用 .any(pred):
// Overlap between two tag arrays
let matched = leftTags.any(func left = rightTags.any(func right = left == right));
  • 不要仅仅为了搜索而复制集合:优先在原始 List.List<T> 上使用 templates.find(func ...),而不是 templates.toArray().find(func ...)——中间数组是一次多余的复制。
  • 遍历数组快照时使用 .values()。不要在新的 Motoko 代码中写 .vals()。

CRUD List 模式: 不要在 List 上臆造辅助方法。不存在 filterInPlace,而且记录展开在带有 var 字段的记录上会失败。

作为调用参数传入的内联 func 不带类型注解。 调用本身已经确定了参数和结果类型,添加注解只是重复声明,还会导致它们随着代码更改而不一致。请使用表达式形式 func x = <expr>:

todos.find(func todo = todo.id == targetId);            // CORRECT
todos.find(func(todo : Types.Todo) : Bool { todo.id == targetId }); // WRONG: annotated

这只适用于参数位置。命名声明仍需包含完整签名,而单独绑定的 lambda 没有可供推断的信息——let f = func x = x > 1 会因 M0103(cannot infer type of variable)而失败。

public func toView(t : Types.Todo) : Types.TodoView { ... }; // annotated, as always

当读者不容易看出类型,或无法推断泛型时,请在调用处声明类型,而不是在 lambda 上声明——这样可读性更好,也能保持唯一的事实来源:

photos.map(func p = { id = p.id; url = p.url.toText() }); // inferred — preferred
photos.map<PhotoInternal, Photo>(func p = { ... });       // when M0098 demands it

只有在编译器实际报告 M0098 时才添加 <In, Out>;如果推断已经成功还添加它,则会触发 M0223(多余的类型实例化)。

唯一的例外是必须返回 async 的回调。此时,: async () 在 moc 1 中至关重要——它会使函数体成为异步函数,且没有不带注解的形式(func() = async { ... } 也不起作用)。没有它时,moc 1 会推断为 () -> (),调用会因 M0096 而失败。moc 2 可以推断出异步回调,但请保留注解,以确保代码能在两个版本中构建:

Timer.recurringTimer<system>(#seconds(3600), func() : async () { cleanup() });
// Toggle a mutable field by finding the record and mutating the var field.
switch (todos.find(func todo = todo.id == targetId)) {
  case (?todo) {
    todo.completed := not todo.completed;
    ?toView(todo);
  };
  case null { null };
};

// Delete from List by rebuilding from an array snapshot.
var removed = false;
let snapshot = todos.toArray();
todos.clear();
for (todo in snapshot.values()) {
  if (todo.id == targetId) {
    removed := true;
  } else {
    todos.add(todo);
  };
};
removed;

// To change an immutable field on a record that also has var fields, rebuild it.
let updated : Types.Todo = {
  id = todo.id;
  text = newText;
  var completed = todo.completed;
  createdAt = todo.createdAt;
};
let snapshot = todos.toArray();
todos.clear();
for (todo in snapshot.values()) {
  if (todo.id == targetId) {
    todos.add(updated);
  } else {
    todos.add(todo);
  };
};

对于会返回记录是否已删除的删除类操作,使用 var removed = false 标志,并在从数组快照重建时更新它。如果参数类型未明确提供 size()(例如 List.List<T>),不要调用 todos.size()。

迭代与链式调用
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);
let sum = scores.filter(func s = s > 15).foldLeft(0, func(acc, s) = acc + s);
switch (numbers.find(func n = n > 5)) {
  case (?found) { /* use */ };
  case null {};
};

数组排序

Array / 数组接收者辅助方法(例如 .sort(...))会返回一个值。不要将它们用作独立的顺序执行语句;Motoko 不允许顺序执行非 () 表达式。

let all = todos.toArray();
let sorted = all.sort(func (a, b) =
  if (a.createdAt > b.createdAt) { #less }
  else if (a.createdAt < b.createdAt) { #greater }
  else { #equal }
);
sorted.map(func todo = {
  id = todo.id; text = todo.text; completed = todo.completed; createdAt = todo.createdAt
});

// WRONG: `.sort(...)` returns an array, so this is not a valid statement.
all.sort(func (a, b) = Int.compare(b.createdAt, a.createdAt));
all.map(func todo = { ... });
contains 与 find
  • contains(element) —— 检查 List/Set/等中的相等性。不接受谓词。
  • find(predicate) —— 在 List.List<T> 和 [T] 中按谓词搜索。返回 ?T。
  • List.List<T> 和 [T] 都有 contains。如果要按谓词而非相等性进行测试,请使用 .any(func x = ...)。
numbers.contains(3); // equal inferred from the imported Nat
friends.contains(p); // likewise from Principal — passing Principal.equal here is M0237
todos.find(func todo = todo.id == targetId); // returns ?Todo
// WRONG: friends.contains(func(f) { f == p })  → M0096/M0103

文本搜索与大小写折叠

Motoko Text 使用上下文接收者方法进行大小写折叠和子字符串检查。不要使用 .toLowerCase() 或 .toLowercase() 等 JavaScript 写法,也不要在普通子字符串搜索中调用 Text.contains(...)。

let term = searchTerm.toLower();
textValue.toLower().contains(#text term)
拼接文本

join 将迭代器作为接收者,将分隔符作为参数——很容易弄反。请使用点表示法;模块形式违反 M0236,mops check --fix 会为你改写。

["a", "b"].values().join(", ");     // CORRECT → "a, b"
Text.join(["a", "b"].values(), ", "); // WRONG (M0236)

请注意,接收者是迭代器,而不是数组:先对数组调用 .values()。

变体标签参数

始终将变体标签的参数括在括号中。标签只会绑定到紧随其后的原子,优先级高于任何运算符,因此未加括号的参数会悄悄丢失首个项之后的所有内容:

#tag(n + 1) // CORRECT
#tag n + 1  // WRONG: parses as (#tag n) + 1 → M0060, operator is not defined for operand types
显式类型实例化

先让类型推断发挥作用。对于未标注的 lambda,编译器会自行将 .map() 解析为不同类型,因此请直接编写普通调用:

let photos = internalPhotos.map(
  func p = { id = p.id; url = p.url; uploadedBy = p.uploadedBy.toText() }
);

只有在编译器报告 M0098(no best choice for type parameter)时,才添加显式类型参数:

let photos = internalPhotos.map<PhotoInternal, Photo>(func p = { ... });

如果类型推断已成功,仍添加类型参数本身就会产生警告——M0223,冗余类型实例化——mops check --fix 会将其移除。用标注 lambda 代替实例化调用始终是错误的。

作为参数的函数字面量

将函数体作为参数传递时,不要在函数体后添加分号:

list.filter(func(item) { item.id != targetId }) // CORRECT
list.filter(func(item) { item.id != targetId; }) // WRONG: trailing semicolon makes the block return `()`

不要将内联命令式语句块用作布尔操作数:

// WRONG: parser can treat the block after `or` as an invalid expression shape.
let matches = titleMatches or {
  var found = false;
  for (tag in tags.values()) {
    if (tag == q) { found := true };
  };
  found
};

// CORRECT: compute the loop result before the final boolean expression.
var tagMatches = false;
for (tag in tags.values()) {
  if (tag == q) { tagMatches := true };
};
let matches = titleMatches or tagMatches;

每个 switch 分支都必须以分号与下一个 case 分隔,即使是在紧凑的单行 switch 中也是如此。moc 2 中 ; 是可选的,但 moc 1 会拒绝缺少分号的分支:

switch (pricing) { case (#free) { true }; case (#paid(_)) { false }; } // CORRECT
switch (pricing) { case (#free) { true } case (#paid(_)) { false } } // WRONG: moc 1 syntax error
声明终止符

module、actor 和 mixin 块中的顶层函数声明和嵌套函数声明都必须以 ; 结尾。函数后缺少 }; 时,通常会在下一个声明附近显示语法错误,例如 unexpected token 'public'。

局部可变性

除非变量会通过 := 重新赋值,否则局部绑定请使用 let。不要仅仅因为变量引用的值可变,就对局部绑定使用 var。通过添加到集合等方法修改对象,并不要求绑定本身使用 var;对于集合构建器和其他累加器对象,请使用 let,除非之后会重新赋值该绑定。

安全的 Nat 算术

除非编译器能在运算本身证明结果非负,否则请避免使用 Nat 减法。当 b > a 时,a - b 会触发陷阱;即使安全性取决于之前的分支,编译器仍可能发出警告。优先使用边界检查、有界加法、循环计数器,或不对两个 Nat 相减的辅助分支。

Option 处理

优先使用 ?? 来执行解包后提供默认值或解包后触发陷阱。 不要仅为了解开 ?T 而编写嵌套的 switch。

// Unwrap with trap when null means something is wrong
let user = users.find(func u = u.id == caller)
  ?? Runtime.trap("User not found");

// Default when absence is fine
let caption = optLabel ?? "(untitled)"; // not `label` — reserved word

// Only return ?T when absence is a normal, expected outcome
public query func findUserByName(name : Text) : async ?User {
  users.find(func u = u.name == name);
};

// Keep switch when the Some arm maps / mutates / has side effects
switch (todos.find(func todo = todo.id == targetId)) {
  case (?todo) {
    todo.completed := not todo.completed;
    ?toView(todo);
  };
  case null { null };
};

错误处理:Result

使用 mo:core/Result 返回调用方可以处理的失败。Result<Ok, Err> 是 { #ok : Ok; #err : Err },因此只要 Ok 和 Err 本身是共享的,它就会以 Candid 的形式跨越 API 边界;对于 Ok,这通常意味着使用视图类型,而不是内部记录。

根据失败的含义选择返回类型:

调用失败的原因是……返回类型
调用方做了可以修正的操作Result<Ok, Err>
某项内容本来就不存在,且这属于正常情况?T
此代码负责维护的不变量遭到破坏trap(Runtime.trap)
import Result "mo:core/Result";

public type BookingError = {
  #slotTaken : { until : Time.Time };
  #notAuthorized;
  #unknownRoom : Nat;
};

public shared ({ caller }) func book(roomId : Nat, at : Time.Time) : async Result.Result<Booking, BookingError> {
  // ...
};

绝不要把错误糊弄成 Text。 Result<Booking, Text> 会迫使每个调用方(包括前端)都通过字符串匹配来区分“时段已被占用”和“未授权”。请将 Err 设为变体,并把每种失败所需的数据放在各自的标签中。Text 负载可以放在标签内,用作面向用户的消息,但不能用来区分错误类型。

不要因调用方错误而触发 trap。 trap 会回滚消息中尚未提交的状态更改,并以不透明的拒绝结果传到前端;调用方无法据此进行分支处理,用户也收不到可采取行动的消息。trap 应仅用于“这种情况不可能发生”(见上文的 ?? Runtime.trap(...))。

链式调用,不要嵌套。 mapOk、mapErr 和 chain 都接收 self,因此应像所有其他 self 参数 API 一样使用点记法,让管道保持扁平。Result.fromOption 不带 self,它是在缺失值变为调用方可见的错误时,将边界处的 ?T 转换为模块调用的桥接方法。

import Result "mo:core/Result";
import Map "mo:core/Map";
import Nat "mo:core/Nat";

actor {
  type Room = { id : Nat; name : Text };
  type RoomView = { name : Text };
  type BookingError = { #unknownRoom : Nat; #notAuthorized };

  let rooms : Map.Map<Nat, Room>;

  func toView(room : Room) : RoomView = { name = room.name };
  func reserve(room : Room) : Result.Result<Room, BookingError> = #ok(room);

  public shared ({ caller }) func book(roomId : Nat) : async Result.Result<RoomView, BookingError> {
    Result.fromOption(rooms.get(roomId), #unknownRoom(roomId))
      .chain(func room = reserve(room))
      .mapOk(toView);
  };
};

当 #ok / #err 分支需要执行不同操作时,请使用 switch;不要先调用 isOk / isErr 再解包,这会丢弃类型原本携带的负载。

错误处理:try / catch

try 会捕获 reject:throw,或调用失败的 await / await*,例如调用另一个 canister,或会抛出异常的库调用。调用是否会抛出异常以及何时抛出,属于其 API 的一部分:请查阅该 package 自身的 skill。它无法捕获 trap。trap 会结束消息,并回滚自上一个提交点(即上一个 await)以来所做的状态更改(await* 仅在其主体中有 await 时才会提交)。被调用方在自己的消息中发生的 trap(例如另一个 canister,或一个 async 函数)有所不同:它会以 reject 的形式传回调用方。async* 被调用方在调用方的消息中运行,因此其 trap 无法捕获。不包含 await 和 throw 的代码不需要 try。

reject 不会回滚任何内容。函数在失败调用之前更改的状态会保持更改;在 catch 分支中触发 trap 并不能可靠地撤销这些更改:调用一旦挂起,该状态就已提交。撤销更改是捕获异常最常见的原因。

先查看被调用方的返回类型。 返回 Result 或 ?T 的被调用方会在其值中报告失败,不会因此抛出异常。请对该值进行模式匹配。try { ignore await f() } catch _ {} 会丢弃实际的 #err,也不会捕获任何可处理的内容。

仅在 catch 分支执行了传播 reject 无法做到的操作时才捕获:

需求catch 分支
撤销 await 前更改的状态移除预留项,将作业标记为失败
向调用方返回可处理的失败使用变体标签 return #err(...)
恢复重试,或回退到其他来源
让循环或后台作业在一次失败后继续运行跳过该项并继续

否则,让 reject 继续传播。空的 catch _ {} 只适用于没有任何内容依赖的副作用。只处理部分错误的 catch 分支应使用 throw e 重新抛出其余错误;在该分支中触发 trap 会阻止所有调用方捕获它。

import Error "mo:core/Error";
import Map "mo:core/Map";
import Nat "mo:core/Nat";
import Result "mo:core/Result";

actor {
  type Quote = { price : Nat };
  type HoldError = { #taken; #quoteFailed : Text };

  let holds : Map.Map<Nat, Principal>;

  // Stands in for any call that can reject.
  func fetchQuote(seat : Nat) : async* Quote {
    if (seat == 0) throw Error.reject("HTTP 503");
    { price = 100 };
  };

  public shared ({ caller }) func hold(seat : Nat) : async Result.Result<Quote, HoldError> {
    if (holds.containsKey(seat)) return #err(#taken);
    holds.add(seat, caller);
    try {
      #ok(await* fetchQuote(seat));
    } catch e {
      // A reject keeps the hold, so release the seat here.
      holds.remove(seat);
      #err(#quoteFailed(e.message()));
    };
  };
};

常见模式

带 Self 模式的模块
// lib/User.mo
module {
  public type User = Types.User;
  public func new(id : Principal, name : Text) : User {
    { id; var name; var isActive = true };
  };
  public func ban(self : User) { self.isActive := false };
};
// Usage: user.ban(); -- dot notation!

使用 with 展开记录

规则: 对不可变记录使用记录展开。切勿对包含 var 字段的记录类型使用记录展开:moc 1 会因 base has non-aliasable var field(M0179)而拒绝;moc 2 则会将 var 字段复制到新单元格,因此修改结果永远不会影响原记录。

{ self with newField = "" }; // CORRECT for immutable records

// CORRECT for a record type containing var fields:
let updated : Types.Todo = {
  id = todo.id;
  text = newText;
  var completed = todo.completed;
  createdAt = todo.createdAt;
};

{ todo with text = newText }; // WRONG if Todo contains any var field

状态定义

实体类型位于 types.mo 中。将状态字段声明为 actor 的直接绑定,不要使用 AppState 包装器。

稳定 actor 字段只声明类型,不写初始化器(初始值来自迁移链)。临时字段照常使用初始化器。

// types.mo
module {
  public type User = {
    id : Principal;
    var username : Text;
    var isActive : Bool;
  };
};
// main.mo
actor {
  let users : List.List<Types.User>;
  let state : { var nextPostId : Nat };
  include AuthMixin(users);
};

Mixins 的可变状态

切勿声明打算与 mixins 共享的 var actor 字段(例如 var nextPostId : Nat)——var 参数按值传递,因此 mixin 的修改不会传播回来。应将可变字段包装在记录中并传递该记录;记录按引用共享。在 actor 中,仅声明该记录的类型(let state : { var nextPostId : Nat };)——其初始值(例如 { var nextPostId = 0 })来自迁移链,与所有稳定字段相同。

在 actor、mixin 和辅助模块之间共享的可变状态记录,必须保留完全相同的字段名。如果 actor 声明 let state : { var nextId : Nat },而 mixin 接收 state,则辅助函数参数必须接受 { var nextId : Nat },并更新 state.nextId。不要在辅助函数签名中将字段重命名为 val 或 counter,也不要创建类似 { var val = state.nextId } 的包装副本;副本只会修改自身,不会更改 actor 状态。

瞬态状态与静态/模块字段

增强型正交持久化默认会将 Actor 或 mixin 中每个顶层 let/var 设为稳定(升级后仍会保留)——没有 stable 关键字。在绑定前加上 transient,即可将其排除在稳定存储之外;每次(重新)启动时都会重新初始化,而不是持久化。对于任何非持久状态(缓存、能力句柄和常量),都应使用 transient。

常量——Motoko 没有 const,而 Actor 或 mixin 中单独的 let X = ... 属于稳定状态。如果右侧是静态表达式(有命名空间、可复用且永远不是状态),则将固定值放在模块中;否则,将其作为 Actor/mixin 中的 transient let:

transient let admin = Principal.fromText("..."); // non-static (a call) — can't be a module `let`
transient let cache = Map.empty<Text, User>();   // derived; rebuilt after each upgrade

静态(模块 let 字段允许的内容)=字面量、变体标签(#x)、选项(?x)、元组、不可变数组和记录、函数值,以及导入的名称/变量名——还包括对这些内容使用 .field 投影。非静态=函数调用、运算符(+、==、#)、控制流(if/switch/循环)和数组索引(a[i])。

数值转换规范

将弃用警告视为错误。所有转换都写作源值上的 to——绝不调用 Module.fromX,也绝不通过定长数值模块进行链式转换。转换只需一次接收者调用:

import Nat "mo:core/Nat";

let average = sum.toFloat() / count.toFloat(); // Nat.toFloat
不要使用请写成
Float.fromInt(i)i.toFloat()
Float.fromInt64(i)i.toFloat()
Nat.fromNat64(n)n.toNat()
Int.fromNat(n)n.toInt()
Nat64.fromNat(n)n.toNat64()
Blob.fromArray(bytes)bytes.toBlob()
Iter.fromArray(a)a.values()

需要多步转换,说明选错了函数:请在 api-reference.md 中核对准确的 mo:core 签名,该文件仅列出未弃用的 API。

安全与授权

每个公开的更新函数都必须通过 {caller} 解构来验证调用者。在后端实施授权——绝不要信任客户端检查。

向跨 canister 调用附加 cycles(await (with cycles = ...) <call>)会将其交给被调用方,因此应将任何可能触发此类调用的端点视为具有支出权限:限制调用者、限定金额,并且绝不允许未经身份验证的路径触发此类调用。某些平台完全禁止向外发送 cycles——如适用,请遵循托管平台自身的指南。

常见编译错误模式

错误模式原因修复
field append does not exist已移除 Array.append接收者 .concat(...)
field put does not existMap.put 已重命名.add()
field delete is deprecatedMap.delete 已重命名.remove()
field toLowerCase does not existJS Text API 拼写.toLower()
field toLowercase does not existJS Text API 拼写.toLower()
You can use the dot notation ... containsText contains 形式错误text.toLower().contains(#text term)
operator may trap for inferred type NatNat 运算可能不安全避免 Nat 减法;使用边界检查或循环
Int cannot produce expected type NatInt/Nat 类型不匹配.toNat()
field fromX is deprecatedfromX 转换已弃用对源值使用对应的 toX:n.toFloat()、bytes.toBlob()
syntax error, unexpected token '.'缺少括号#text (searchTerm.toLower())
syntax error, unexpected token ','for 中缺少括号for ((key, value) in map.entries())
Compatibility error [M0170]缺少迁移加载 migrating-motoko-actors
M0250 initialized stable fieldstable actor 字段上有初始化器仅声明其类型;将值移入迁移的 NewActor
M0254 / M0267 initial actor requires field没有迁移提供 stable 字段将其添加到待处理迁移的 NewActor
M0255 stable signature downgrade链配置或迁移配置已移除恢复配置——增强型迁移是单向的;加载 troubleshooting-motoko-migrations
shared function has non-shared parameter/return typeAPI 中有可变类型返回 [T] 而不是 List<T>,不要使用 var 字段
send capability required在非 async(async*/本地非 shared 目标)中使用 async添加 <system> capability
M0038 misplaced await + M0188 send capability (paired)普通 query func 中有 await <call>改为 composite query func(查询),或普通 update func(更新)
M0187 send capability in a composite query从 composite query func 调用/等待 update改为普通 update func
M0186 composite send capability required从非 composite func 调用 composite query func只有入口调用会发起 composite query;从前端调用它,或将被调用方改为普通 query
unexpected token '<name>' (M0274 on moc 2) at an identifier declaration将保留字用作标识符在其契约和调用方中统一重命名;参见 references/reserved-keywords.md
unexpected token 'public' after a function缺少声明 ;函数声明以 }; 结束
M0219 / M0220 actor should be declared persistentmoc 1 未使用 --default-persistent-actors参见 references/project-setup.md
M0217 redundant persistent keywordActors 已经是 persistent(moc 2,或 moc 1 的该标志)移除 persistent
M0218 redundant stable keywordEOP 下的 stable移除 stable——普通 let/var 已经是 stable
M0064 misplaced '!'! 位于 option 块之外用 do ? { ... } 包裹
M0145 does not cover valueswitch 或 let 模式未穷尽添加缺失的分支或 case _;对于 let,使用 let ... else { ... }
M0242 implicit oneway functionpublic func 没有返回类型使用 : async (),若有意设为 oneway,则使用 : ()
M0215 field provided but not expected提供的记录字段被期望类型丢弃,通常是拼写错误修正字段名,或移除该字段
M0277 redundant async= async { ... } 函数体(moc 2)将函数体写成块:func f() : async T { ... }
M0273 braces enclose a record literal?? 后的块(moc 2)opt ?? do { ... }
M0275 expected a block分支与条件直接相连(moc 2)if (c) -1 else 1——裸分支前加空格,或用花括号包裹分支
M0272 record field in block position记录字面量作为 case 或函数体将其嵌套为块的结果:case null { { x = 0 } };
M0116 pattern cannot consume / M0050 literal does not have expected type变体标签或有符号字面量无法容纳于被匹配值的类型修正标签;若要匹配更宽的类型,为被匹配值添加注解:switch (n : Int)
M0074 / M0081 / M0101 inconsistent types数组元素或分支之间没有共同类型使它们类型一致,或添加注解 : Any
M0060 operator not defined for {#tag : T}未加括号的变体标签#tag(x),不要写 #tag x
M0060 operator not defined, on ==对含有 var 字段的记录使用 ==(非 shared)改用 equal 函数
M0230 cannot determine implicit argument compare键类型的模块不在作用域内通常是缺少导入:在该文件中导入键类型的模块(Nat、Text 等)。对于记录/变体键,在类型所属模块中添加 compare
M0070 expected object type, produces Nat对数字的接收者调用 .equal/.compare使用 == 或 Nat.equal(a, b)
M0096 actor cannot produce expected type ()actor 后有声明actor 必须是文件中的最后一个声明
field compare does not exist on Time没有 Time.compare使用 Int.compare
unexpected token ';' in function call函数字面量后有分号移除 ) 前的 ;
unbound variable X缺少导入import X "mo:core/X"
M0098 no best choice for type param泛型需要显式类型list.map<In, Out>(...)
M0096 on contains callback向 contains 传入谓词contains 接收一个元素;若要使用谓词,Bool 结果用 .any(pred),元素结果用 .find(pred)
M0009 import file does not exist路径错误使用相对路径,不带 .mo 扩展名
M0244 variable ... is never reassigned不需要 var 绑定除非会用 := 重新赋值,否则使用 let

快速参考

基本类型: Nat Int Text Bool Principal ?T [T] [var T] Blob Float — Time.now() 返回 Int(纳秒)

常见操作: debug_show(value) → Text | assert condition | # "text" 拼接 | 在 for、while、loop 中使用 break / continue

结构用途关键操作复杂度
Map键值对get、add、removeO(log n)
List可增长数组add、get、atO(1) 访问
QueueFIFO 处理pushBack、popFrontO(1)
StackLIFO 处理push、popO(1)
Array固定集合index、map、filterO(1) 访问
Set唯一值contains、addO(log n)

最佳实践

  1. 始终使用 mo:core,绝不使用 mo:base
  2. 不使用 stable 关键字——增强的正交持久化会处理状态
  3. 所有 self 参数函数都使用点表示法
  4. 使用 ?? 解包(opt ?? Runtime.trap(...) 或 opt ?? default);将 switch 留给转换/副作用/变体;仅在预期值可能缺失时使用 ?T
  5. 使用 types.mo / lib/ / mixins/ / main.mo 结构
  6. Mixins 仅接收所需的状态切片
  7. 只读操作使用查询,状态变更使用更新
  8. 使用迭代器链式调用,避免创建中间集合
  9. 对不可变记录使用记录展开 { self with ... };包含 var 字段的记录应进行变更或重建
  10. 稳定 Actor 字段上不使用内联初始化器——初始值来自迁移链
  11. 内联 func 参数不带类型注解(异步回调上的 : async () 除外);仅当编译器报告 M0098 时,才实例化调用
  12. 仅在可能拒绝的 await 外使用 try,且仅当 catch 分支会撤销状态、返回带类型的 #err、恢复执行、跳过某个项,或保护尽力而为的副作用时使用;读取返回的 Result 的失败值时,应通过对其使用 switch,而不是使用 try

更多资源

  • 控制流:references/control-flow.md — ??、do ? { ... } 选项链式调用、switch 语句、循环、break / continue
  • 保留关键字:references/reserved-keywords.md — 用于检查标识符的完整列表
  • 相等与比较:references/equality.md — 哪些类型支持接收者 .equal,以及 == 与 equal 何时不同
  • 类型转换:references/type-conversions.md — Nat/Int 大小转换
  • 项目设置:references/project-setup.md — 一次性 [moc] args 标志。如果你的平台会管理 mops.toml,请跳过此项
  • 设计审查:审查、审计或重构现有 .mo 文件时,加载 reviewing-motoko——了解类型编码的不变量、状态/持久化规范和文件结构
  • Actor 迁移:升级 canister 或更改 Actor 状态结构时,加载 migrating-motoko-actors
  • 迁移失败:遇到原因不明的兼容性诊断、冻结的迁移文件或转换后的旧项目时,加载 troubleshooting-motoko-migrations
  • API 签名:api-reference.md — 完整的函数签名
  • 完整示例:examples.md — 完整可运行的代码示例

评论

支持 Markdown