灿飞屋头像
Mojo编程实战五:错误处理与 Optional封面图

Mojo编程实战五:错误处理与 Optional

第 5 章 错误处理与 Optional

本章目标 - 理解 Mojo 的错误为什么"只是一个返回值",以及它和 Python/Java 那种栈展开异常到底差在哪。 - 会用 raises、raise、try/except/else/finally 写出能编译、能正确清理资源的错误处理代码。 - 能为自己的模块设计带语义的自定义错误类型(struct + Writable),而不是到处抛字符串。 - 掌握 Optional[T] 的全套用法:None、if opt、value()、or_else()、map()、and_then()。 - 在"错误码 / raises 异常 / Optional"三种风格之间,能根据场景做出有意识的取舍。

5.1 先换个脑子:Mojo 的错误不是你想的那种异常

写过 Python、Java、C++ 的人,脑子里的"异常"大概是这样的:运行时在调用栈上一路 unwind(栈展开),每经过一层就跑一遍该层的析构函数/finally,直到某个 catch 接住为止。这套机制写起来舒服,但代价不小——编译器得在每个可能抛异常的位置塞异常表,CPU 在热路径上也要为"万一要展开"留好退路。

Mojo 的选择不一样。它把错误当成一个普通的返回值来传递,而不是靠运行时在调用栈上翻跟头。具体说,一个 raises 的函数,本质上多返回了一个"状态位":要么是正常结果,要么是错误对象。调用链上游不 try 就必须继续往上传,编译器在静态层面把这条链路卡死。

这带来两个直接后果:

  1. 热路径几乎零开销。官方手册的口径是,raises 在成功路径上的运行时成本,跟多返回一个 Bool 差不多。这对 Mojo 瞄准的数值/AI 场景很关键——你不会想在一段被调用几百万次的内核里,为"可能出错"付出一整套异常表的代价。
  2. 错误流是显式的、静态可见的。一个函数会不会抛错、抛哪种错,签名上写得清清楚楚;不像 Java 那样受检异常被人人用 RuntimeException 绕过,也不像 Go 那样靠人自觉 if err != nil。

为什么这件事对 Mojo 特别重要?想一下 Mojo 的目标用户:写矩阵乘法、写概率采样、写 GPU 算子的人。这些函数一秒钟可能被调用上百万次。如果每次调用都要为"万一抛错"预留栈展开的准备工作,性能就全毁了。Mojo 选"错误即返回值",本质上是为它的核心场景(热内核)让出来的优化空间。你写业务代码时可能感觉不到这个差异,但写第 17 章那种基准测试时会体会到。

flowchart TD
    A["调用方 f()"] -->|"try"| B["被调用方 g() raises"]
    B -->|"成功:返回正常值"| A
    B -->|"失败:返回错误对象<br/>(不走栈展开)"| C{"调用方是否 try?"}
    C -->|"try 接住"| D["执行 except 分支"]
    C -->|"没 try"| E["编译器报错:<br/>未处理的 raises"]
    D --> F["finally 清理后继续"]
    E --> G["编译失败,要求你显式处理"]

设计权衡:Mojo 不是在"异常 vs 错误码"之间和稀泥,它是把异常的"调用方必须意识到"和错误码的"零成本返回值"捏在了一起。代价是你不能再像 Python 那样随手 raise 一个东西然后指望别人兜底——凡是可能出错的函数,签名必须写 raises,这是写 Mojo 时第一个要养成的肌肉记忆。

5.2 def 默认不抛错:raises 是必须显式声明的

Mojo 1.0 之后,def 统一接管了原来 fn 的语义:一个函数默认是 non-raising 的。也就是说,除非你在签名上明确写 raises,否则这个函数里出现 raise 就是编译错误。

这和 Python 完全相反。Python 里任何函数都可能抛 Exception,谁也不知道;Mojo 把这件事提前到了签名上:

# 这个函数不写 raises,里面也不能 raise
def add(a: Int, b: Int) -> Int:
    return a + b

# 这个函数可能抛错,必须显式写 raises
def divide(a: Float64, b: Float64) raises -> Float64:
    if b == 0.0:
        raise Error("division by zero")
    return a / b

如果你在一个没写 raises 的函数里偷偷 raise,编译器会直接拒绝。反过来,如果你写了 raises 但实际从不抛错,也能编译——这在后面讲 raises Never 时会用到。

踩坑提示:main() 本身如果要调用 raises 函数,也得写成 def main() raises:,否则你连一个 try 都不写就想调用 divide,编译不过。很多第一次写 Mojo 的人卡在这一步:明明逻辑对,编译器却抱怨"你调用了一个 raising 函数却没处理"。

5.3 抛出错误:raise Error("...") 与简写 raise "..."

最常用的错误是标准库内置的 Error。两种写法等价:

raise Error("division by zero")
raise "division by zero"     # 简写,等价于上面

第二种是语法糖,编译器会把字符串包成 Error。日常快速抛错用简写就够了;但如果你要传递结构化信息(错误码、字段名、行号),就得自定义错误类型,下一节讲。

📄 示例文件:examples/ch05/raising-divide.mojo

# def 默认不抛错;可能抛错必须在签名上写 raises
def divide(a: Float64, b: Float64) raises -> Float64:
    if b == 0.0:
        raise Error("division by zero")   # 抛出内置 Error
    return a / b

def main() raises:
    try:
        var r = divide(10.0, 2.0)
        print("10 / 2 =", r)
    except e:
        print("捕获到错误:", e)
    finally:
        print("无论是否出错,finally 都会执行")

运行结果:

10 / 2 = 5.0
捕获到错误: division by zero
无论是否出错,finally 都会执行
---
第二次捕获到错误: division by zero
第二次 finally

注意一个细节:except e 里的 e 就是被抛出的错误对象本身。print(e) 能工作,是因为 Error 实现了 Writable(旧称 Stringable,第 6、7 章会细讲这个 trait)。

5.4 捕获:try / except / else / finally

Mojo 的 try 块比 Python 简单——一个 try 只能配一个 except。你不能像 Python 那样写好几个 except ValueError / except KeyError 分支分别接不同类型。如果你需要按错误类型分支,要么在外层包多个 try,要么拿到错误后自己用 match(v1.2 新增)或字段判断。

try:
    # 可能抛错的代码
    ...
except e:
    # 唯一的 except:接住

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/canfeit/article/details/170146933

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--