关于 writing-motoko
面向开发 Internet Computer 应用的 Motoko 编码技能,提供当前编码规范、API 查询指引、迁移实践和基于 mops 的检查流程,帮助避免弃用写法与编译错误。
主要功能
- 01遵循当前 Motoko 与 mo:core 规范
- 02通过参考文档核对 API 签名
- 03采用多文件 Actor 架构与 mixin
- 04指导 Actor 状态迁移
- 05使用 mops 管理依赖并检查构建
适用场景
- 01编写或更新 Motoko 代码
- 02排查编译器和 API 错误
- 03修改 Actor 状态或升级 canister
- 04用 mops 管理依赖并验证构建
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill writing-motoko安装后在 Claude Code 中输入/writing-motoko确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
api-reference.mdexamples.mdreferences/control-flow.mdequality.mdproject-setup.mdreserved-keywords.mdtype-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+(编译器moc1.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:coreAPI,或编译诊断指出 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;请使用mopsCLI,以确保依赖元数据和锁文件保持原子一致。([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 query | public 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 exist | Map.put 已重命名 | .add() |
field delete is deprecated | Map.delete 已重命名 | .remove() |
field toLowerCase does not exist | JS Text API 拼写 | .toLower() |
field toLowercase does not exist | JS Text API 拼写 | .toLower() |
You can use the dot notation ... contains | Text contains 形式错误 | text.toLower().contains(#text term) |
operator may trap for inferred type Nat | Nat 运算可能不安全 | 避免 Nat 减法;使用边界检查或循环 |
Int cannot produce expected type Nat | Int/Nat 类型不匹配 | .toNat() |
field fromX is deprecated | fromX 转换已弃用 | 对源值使用对应的 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 field | stable actor 字段上有初始化器 | 仅声明其类型;将值移入迁移的 NewActor |
M0254 / M0267 initial actor requires field | 没有迁移提供 stable 字段 | 将其添加到待处理迁移的 NewActor |
M0255 stable signature downgrade | 链配置或迁移配置已移除 | 恢复配置——增强型迁移是单向的;加载 troubleshooting-motoko-migrations |
shared function has non-shared parameter/return type | API 中有可变类型 | 返回 [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 persistent | moc 1 未使用 --default-persistent-actors | 参见 references/project-setup.md |
M0217 redundant persistent keyword | Actors 已经是 persistent(moc 2,或 moc 1 的该标志) | 移除 persistent |
M0218 redundant stable keyword | EOP 下的 stable | 移除 stable——普通 let/var 已经是 stable |
M0064 misplaced '!' | ! 位于 option 块之外 | 用 do ? { ... } 包裹 |
M0145 does not cover value | switch 或 let 模式未穷尽 | 添加缺失的分支或 case _;对于 let,使用 let ... else { ... } |
M0242 implicit oneway function | public 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、remove | O(log n) |
| List | 可增长数组 | add、get、at | O(1) 访问 |
| Queue | FIFO 处理 | pushBack、popFront | O(1) |
| Stack | LIFO 处理 | push、pop | O(1) |
| Array | 固定集合 | index、map、filter | O(1) 访问 |
| Set | 唯一值 | contains、add | O(log n) |
最佳实践
- 始终使用
mo:core,绝不使用mo:base - 不使用
stable关键字——增强的正交持久化会处理状态 - 所有
self参数函数都使用点表示法 - 使用
??解包(opt ?? Runtime.trap(...)或opt ?? default);将switch留给转换/副作用/变体;仅在预期值可能缺失时使用?T - 使用 types.mo / lib/ / mixins/ / main.mo 结构
- Mixins 仅接收所需的状态切片
- 只读操作使用查询,状态变更使用更新
- 使用迭代器链式调用,避免创建中间集合
- 对不可变记录使用记录展开
{ self with ... };包含var字段的记录应进行变更或重建 - 稳定 Actor 字段上不使用内联初始化器——初始值来自迁移链
- 内联
func参数不带类型注解(异步回调上的: async ()除外);仅当编译器报告 M0098 时,才实例化调用 - 仅在可能拒绝的
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 — 完整可运行的代码示例