当前位置:首页 > 文章列表 > 文章 > 软件教程 > Rust 是如何干掉空指针的

Rust 是如何干掉空指针的

2026-08-20 23:02:30 0浏览 收藏

如果你一直看我的博客,你会发现我不仅仅会讲述 Android 相关的内容,我也会讲一些 Rust 相关的知识。

我不是 Rust 方面的专家,平时只有 20% 的时间编写 Rust 的代码,但我对 Rust 的喜爱溢于言表。

我的 Rust 经验主要是编写 UI 工具链,方便平时开发 AOSP 相关的内容。

对于一个写惯了 Ja va 和 Kotlin 的人来讲,我非常推崇 Rust 对空指针的处理方式。那么,就让我这个不那么精通 Rust 的人来看看,Rust 是如何解决空指针的。

空指针的泥潭

如果你写过 C、C++ 或 Ja va,应该都见过类似的错误:对象明明应该存在,运行时却突然抛出一个空指针异常。

代码看起来没什么问题,编译也通过了,真正的问题要等到程序跑起来才暴露。

更麻烦的是,空指针错误往往不是发生在赋值的位置,而是发生在很远之后的某次访问上。等你回头排查时,只能沿着调用链一点点追踪:这个值到底是从哪里变成 null 的?

虽然 C++ 和 Ja va 对于空指针都有明确的规避方式,但是作为开发者,对于空指针的担忧始终萦绕在心头。你能保证自己写的代码是完美的,但你不能规避第三方代码的空指针问题呀!

Rust 的神来之笔

在我 21 年接触 Rust 的时候,我发现 Rust 对 null 的处理非常有意思。

Rust 没有 null 这个值。

当然,没有 null 并不是简单地说“我没有空指针”,而是把“可能为空”这件事从一种隐式风险,变成了类型系统中明确表达的一部分。

换句话说,Rust 并没有让空值彻底消失,而是让它没法再偷偷混进普通代码里。

你必须直面空指针,而不是假装这里不会有空指针问题,这是 Rust 对于空指针的最终奥义!

在 Rust 里,普通引用默认就是非空的。比如 &T&mut T,它们表达的语义不是“这里可能有一个 T”,而是“这里一定有一个有效的 T”。

所以你不能随便构造一个空引用,也不能把 null 当成一个正常引用传来传去。

这个设计非常关键,因为它让大量代码天然摆脱了空指针检查。

fn print_name(name: &String) {
    println!("{}", name);
}

这段代码里,name 不需要判断是否为空。因为从类型上看,它就不可能是空的。函数签名已经把约束说清楚了:调用者必须传一个有效的 String 引用进来。编译器会帮助你维护这个约束,而不是把风险留到运行时。

那如果一个值确实可能不存在怎么办?Rust 使用 Option 来表达。

enum Option {
    Some(T),
    None,
}

这其实就是 Rust 处理空值的核心思路:不要用一个特殊的 null 值混在所有类型里面,而是用一个明确的类型告诉你,这里可能有值,也可能没有值。

比如查找一个用户时,用户可能存在,也可能不存在:

fn find_user(id: u64) -> OptionString> {
    if id == 1 {
        Some("Alice".to_string())
    } else {
        None
    }
}

调用者拿到 Option 之后,不能直接把它当成 String 用。

你必须处理 SomeNone 两种情况。

match find_user(1) {
    Some(name) => println!("user: {}", name),
    None => println!("user not found"),
}

这就是 Rust 和很多传统语言最大的区别。

传统语言里,一个变量可能是 null,但类型本身往往看不出来。你看到的是 UserStringObject,但运行时它也可能是 null

所以在这些语言里,你不得不编写大量的代码去处理空指针问题 —— if (a != null)

Rust 则把这种不确定性写进类型里:String 就是有值,Option 才是可能没值。

Rust 干掉空指针的方式,不是靠程序员自觉多写几个 if 判断,而是改变了问题的表达方式。

以前我们写代码时,经常在心里记着:“这个地方有可能为空,后面要小心。”Rust 不让你只在心里记着,它要求你在类型上写出来。

凡事都有例外

当然,Rust 也不是完全没有“空指针”这个概念。

比如在和 C 语言交互时,或者在写底层代码时,Rust 仍然有原始指针:*const T*mut T。原始指针是可以为空的,也可以指向无效地址。但是使用原始指针解引用需要进入 unsafe 代码块。

let p: *const i32 = std::ptr::null();

unsafe {
    // 解引用之前必须自己保证它是有效的
    // println!("{}", *p);
}

这说明 Rust 的目标不是假装世界上没有危险,而是把危险圈在边界里。

普通业务代码使用引用和 Option,编译器帮你保证基本安全;真正需要和底层世界打交道时,可以使用原始指针,但这部分代码会被 unsafe 明确标出来。代码审查时,大家自然也知道这些地方需要额外小心。

性能

Rust 的 Option 还有一个很容易被忽略的细节:它并不一定会带来额外的运行时开销。

很多人一开始看到 SomeNone,会以为这肯定比 null 指针更重。实际上,Rust 编译器会做所谓的空指针优化。

对于引用、Box、函数指针这类本身不能为 null 的类型,OptionOption> 往往可以和普通指针占用一样大的空间。因为编译器可以用原本非法的 null 位模式来表示 None

这就是 Rust 的高明之处了:在语义上让“可能为空”变得明确,在性能上又尽量不让你为这种明确性付出额外代价。

强迫症

当然,这套设计也带来一个很现实的体验:你不能偷懒。

拿到一个 Option,就必须处理没有值的情况。你可以用 match,也可以用 if let,还可以用 unwrap_ormapand_then 之类的方法。

对于简单场景,这些工具非常顺手。

let name = find_user(2).unwrap_or("Guest".to_string());
println!("{}", name);

但如果你直接使用 unwrap(),其实就有点回到老路上了。

let name = find_user(2).unwrap();

unwrap() 的意思是:我确信这里一定有值,如果没有,就让程序 panic。

它不是不能用,比如测试代码、原型代码、明确不可能失败的地方都可以用。但在正式业务代码里频繁使用 unwrap(),就等于把 Rust 类型系统帮你拦下来的风险又手动放回去了。

Rust 不能阻止你写出不负责任的代码,但它会让这种不负责任变得非常显眼,你的领导一旦在业务代码中审查到这种代码,一定会批评你几句。不说领导了,你让 AI 给你查一下,也都会提醒你这个地方的处理过于粗糙。

Kotlin 呢

再来看 Kotlin。

Kotlin 处理空值的思路和 Rust 有相似之处:它同样把“是否可空”放进了类型系统。

只不过 Kotlin 是运行在 JVM 上的语言,它需要和 Ja va 互操作,所以它的设计不像 Rust 那样彻底。

我现在想想,如果当时 Kotlin 对于 Ja va 中的可空互操作,自定义一个 Option 去处理,会不会比现在更好呢?

在 Kotlin 里,StringString? 是两个不同的类型。

val name: String = "Alice"
val nickname: String? = null

String 表示非空字符串,String? 表示这个字符串可能为空。

如果你直接访问一个可空类型的方法,编译器会报错。

val nickname: String? = null

// 编译不通过
// println(nickname.length)

你必须使用安全调用:

println(nickname?.length)

这里的 ?. 表示:如果 nickname 不为空,就访问它的 length;如果为空,整个表达式结果就是 null

Kotlin 还提供了 Elvis 操作符 ?:,用来给空值提供默认结果。

val length = nickname?.length ?: 0

也可以在判断之后,让编译器自动做智能类型转换。

if (nickname != null) { // 你必须确保 nickname 是 val
    println(nickname.length)
}

在这个 if 分支里,Kotlin 知道 nickname 已经不是 null 了,所以允许你像普通 String 一样访问它。

从日常开发体验看,Kotlin 的空安全非常舒服。它不像 Rust 那样把所有权、生命周期、借用检查一起压过来,学习成本低很多;但它也比 Ja va 更严格,能提前发现大量空指针问题。

尤其在 Android 开发里,Kotlin 的空安全确实减少了很多低级崩溃。

不过 Kotlin 也保留了一个“后门”:!!

val length = nickname!!.length

!! 的意思是:我认为它一定不为空,如果它为空,就直接抛出 NullPointerException

这个操作符很直白,也很危险。它基本相当于开发者告诉编译器:“这里你别管,我自己负责。”如果判断错了,程序就会在运行时崩溃。

Kotlin 还有一个绕不开的问题:Ja va 互操作。

Ja va 代码里的类型默认并不携带严格的可空信息,所以 Kotlin 引入了平台类型,比如 String! 这种概念。

你在代码里不会直接写出 String!,但从 Ja va 返回的对象可能在 Kotlin 看起来既可以当非空用,也可以当可空用。这是 Kotlin 空安全体系里最容易漏风的地方。

比如 Ja va 里有这样一个方法:

public String getName() {
    return null;
}

Kotlin 调用它时,如果没有注解告诉编译器这个返回值可能为空,开发者就很容易把它当成非空值使用,最后还是可能遇到运行时空指针。

所以 Kotlin 的策略可以概括为:在 Kotlin 自己的世界里尽量做到空安全,但在 JVM 生态和 Ja va 历史包袱面前,仍然需要开发者保持警惕。它比 Ja va 向前走了一大步,但没有像 Rust 那样把边界切得那么硬。

走马观花

除了 Rust 和 Kotlin,其他现代语言也在用不同方式处理空值问题。比如 Swift,它的设计和 Kotlin 很像,也使用 Optional 表达可能不存在的值。

var name: String? = nil

if let realName = name {
    print(realName)
} else {
    print("no name")
}

Swift 里的 StringString? 也是不同类型。

访问 Optional 时,需要解包。你可以用 if let,也可以用 guard let,还可以用可选链调用。

Swift 也有强制解包 !,一旦值为空就会崩溃。这个设计和 Kotlin 的 !! 非常接近:语言鼓励你安全处理,但也允许你在明确知道风险的时候强行解包。

再比如 TypeScript。

Ja vaScript 里长期存在 nullundefined 两种“空”的表达,这也是很多 bug 的来源。

TypeScript 没法改变 Ja vaScript 的运行时模型,但它可以在类型系统里增加约束。开启 strictNullChecks 之后,nullundefined 不会再随便赋值给其他类型。

let name: string = "Alice"

// 开启 strictNullChecks 后,这样会报错
// name = null

如果一个值可能为空,就要写成联合类型:

let nickname: string | null = null

if (nickname !== null) {
  console.log(nickname.length)
}

TypeScript 的做法和 Kotlin、Rust 都有点像:让“可能为空”出现在类型签名里,而不是偷偷藏在运行时。

其不同之处在于,TypeScript最终是要编译成Ja vaScript的,类型检查主要在编译阶段进行,运行时并不会额外多出一套类型系统。所以说,它能帮你减少不少错误,但这有个前提,那就是项目得开启严格模式,而且团队不能大量使用 any 来绕过类型检查。

亿点想法

如果把这些语言放在一起看,会发现现代语言对于空指针的处理有一个共同趋势:不再把 null 当成所有引用类型的默认可能值,而是把“值可能不存在”单独表达出来。

Rust 用 Option,Kotlin 用 T?,Swift 用 Optional,TypeScript 用 T | nullT | undefined

语法不同,理念很接近。它们都在推动开发者做一件事:不要假装一个值永远存在,而是在它可能不存在的时候,明确写出处理逻辑。

这背后其实是编程语言设计思想的变化。

过去很多语言更信任程序员,语言本身提供强大的自由度,至于你会不会踩坑,那是你自己的事。

现代语言则更倾向于把常见错误变成编译期问题。能在编译期发现的,就不要留到运行时;能在类型系统里表达清楚的,就不要只靠注释和约定。

那么也就是说,随着技术的发展,语言越来越不信任程序员了? —— 很有意思的一个想法。

空指针就是一个典型例子。它不是复杂的算法问题,也不是高深的架构问题,但它足够常见,足够隐蔽,也足够致命。

一个本该存在的对象突然为空,轻则页面崩溃,重则服务异常。如果语言能在编译阶段把这类问题提前暴露出来,整体代码质量自然会提升。

当然,类型系统不是银弹,空问题本身并不能完全规避!

Rust 里可以滥用 unwrap(),Kotlin 里可以到处写 !!,Swift 里也可以强制解包,TypeScript 里可以用 any 把类型检查绕开。

语言只能给你铺一条更安全的道路,把危险的操作标得更醒目,最终你怎么走,还是取决于你的习惯和工程纪律。

这就是现代语言对稳定性和健壮性的共同追求:让代码的风险更可见,让错误更早暴露,让边界条件更难被忽略。

空指针曾被称为“十亿美元的错误”,而Rust、Kotlin、Swift、TypeScript这些语言所做的努力,实际上是在向我们传达这样一个信息:别再把这种错误丢给运行时去处理了,既然能在编写代码的时候就解决,那就不要等到线上崩溃了才去解决。

Kotlin apply 详解:对象初始化、链式配置与实战示例Kotlin apply 详解:对象初始化、链式配置与实战示例
上一篇
Kotlin apply 详解:对象初始化、链式配置与实战示例
Android-aar与jar的生成与使用,从0到1记录
下一篇
Android-aar与jar的生成与使用,从0到1记录
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    5037次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4573次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4522次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4779次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4731次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码