掘金 后端 ( ) • 2024-05-07 10:41

讲动人的故事,写懂人的代码

  • 故事梗概:
  • 在她所维护的老旧Java系统即将被淘汰的危机边缘,这位在编程中总想快速完事的女程序员,希望能转岗到公司内部使用Rust语言的新项目组,因此开始自学Rust;
  • 然而,在掌握了Rust编程知识之后,为了通过Rust项目组的技术面试,使得转岗成功而不至被裁员,她必须领会编程如何"快速"才能有真正的意义。

上次聊到,Java程序员赵可菲和C++程序员席双嘉在Rust大神贾克强的带领下,找到了AI编程小助手艾极思把Rust编程书中的游戏需求改成了“猜骰子冷热”,现在得重新写一遍代码了。

他们用 cargo new 命令开了个新的Rust项目,并且把Cargo.lock文件也上传到版本库去了。

下一步,就是要开始为“猜骰子冷热”游戏写代码了。

3.1 列出“猜骰子冷热”游戏的用户故事

赵可菲:“在这个AI写代码的时代,最快的写代码方式就是告诉艾极思你的需求,然后让它帮你写。”

席双嘉:“这主意不错哦!但是我们才刚开始学Rust,艾极思如果直接给我们一大堆最后的代码,我们可能会一头雾水。”

“要不我们按照书上的方法,把游戏的需求分成一些小的用户故事。一点一点来学习。”

赵可菲点点头。

springwortley_Two_young_Chinese_software_developers_Java_female_a6536065-fc85-4b76-845a-7ea7c16762de-xiaohongshu.png

他们两个列出了“猜骰子冷热”游戏的7个用户故事,改编自Guessing Game的故事:

1 获取玩家猜的两个骰子点数之和并显示给玩家 2 生成两个骰子点数之和的随机答案 3 比较答案与玩家猜的点数之和 4 将玩家猜的点数之和字符串转换为数字以便比较 5 允许玩家在没猜对后继续猜 6 玩家在猜对后程序退出 7 玩家的输入若不是数字,则继续猜

赵可菲照着书上的代码,写出了故事1“获取玩家猜的两个骰子点数之和并显示给玩家”的代码:

use std::io;

fn main() {
    println!("Guess the sum of two dice!");

    println!("Please input your guess (between 2 and 12).");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

3.2 自动加载标准库的prelude

贾克强:“我来考考你们。这段代码里,哪些是用了prelude的标准库?哪些又得自己use标准库呢?”

席双嘉:“书上说,io::stdin()不在prelude里,得用use std::io自己来use。”

“但是,哪些用了prelude的标准库,我就不知道了。”

赵可菲一边翻书一边说:“prelude是啥来着?”

贾克强:“Rust的标准库功能多得很。但是,如果每个功能都得手动use,那就太麻烦了。同样,use一堆你根本不用的功能也不妥。所以,要找个平衡。”

prelude就是Rust自动use到每个Rust程序的功能清单。它尽量保持简洁,主要关注那些几乎在每个Rust程序中都会用到的特性,尤其是trait。至于trait,咱们以后找机会再聊。”

“Rust的 use 命令在编译器上运行,只涉及到在模块范围内解析路径和名字,完全不需要把代码复制到源文件里。”

”这对于代码管理超有帮助,因为它让我们可以用更短、更易读的方式,引用代码库中其他地方定义的函数、结构体、枚举和其他内容。”

赵可菲:“哈!咱们可以把所有的use语句删掉,看看代码里哪些语句不报错,就知道哪些语句来自prelude的标准库了吧!“

“这么一删,看起来println!宏,还有String类型,和它的new()方法,就是来自prelude标准库的。”

贾克强对赵可菲竖起了大拇指。

3.2.1 Java的自动import机制

赵可菲:“在Java里面,和Rust的prelude机制很像的,就是自动import机制。”

“任何Java程序,都可以直接使用自动import的java.lang包里的所有功能,不必专门去import。”

java.lang包里面包含了Java编程必需的基础类,像所有类的超类Object、用于I/O操作的System,还有像IntegerDouble这样的包装,和StringMath这样的基础工具。”

"Java的import和Rust的 use 命令不一样。import是在Java虚拟机(JVM)上运行的哦。“

"而且它也只是解决类和包的名称解析问题,不会把代码复制到源文件里的。"

3.2.2 C++的手动include机制

席双嘉:“哈哈,C++可没有像Rust prelude这样的等效物。在C++里,include代码库的功能,是由程序员通过#include这个预处理directive来明确控制的。”

“在C++里,最接近的概念可能是include某些在许多程序中频繁使用的头文件,像是用于输入/输出操作的<iostream>,还有<vector><string>和其他STL(标准模板库)组件。”

“但即使这样,也需要在使用它们的每个文件中,明确include它们。C++不会默认自动include任何头文件或命名空间。一切都要由开发者来指定。”

"跟Rust和Java不同,C++的#include在预处理的时候就干活了,直接把include的文件内容全都复制到源代码文件中,这可能会让编译时间变长啊。"

"具体来说,C++的编译过程中,预处理阶段和编译阶段是这么回事。"

"预处理阶段就是编译过程的开头。在这一阶段,预处理器处理源代码文件中的所有预处理指令,就像#include这种。"

"对于#include这个指令,预处理器会直接把指定的文件内容复制到原始源代码文件中的那个位置。"

"这一步还包括宏替换和条件编译等操作。预处理器不会理会函数或类的定义,只是文本层面的替换和插入。"

"然后就进入编译阶段了。预处理过的代码,也就是已经包含了所有通过#include插入的代码,进入编译阶段。"

"在这里,编译器会把处理过的源代码转换成机器代码。"

"编译器会解析代码的结构,像函数调用、变量定义、类的实例化这些,然后生成目标代码。"

"这一步涉及到语法分析、语义分析、优化和代码生成等复杂过程。"

"由于#include在预处理阶段就把文件内容全部复制到源文件中了,所以可能会导致编译输入的代码量大大增加,这会增加编译阶段的工作量,可能会导致编译时间变长。"

"这跟Rust的use和Java的import完全不一样,后两者在编译时只解析和检查必要的名称和路径,不涉及代码的物理复制。"

3.3 通过模式匹配和代码展开来生成代码的Rust的宏

贾克强:“你们知道吗,Rust的println!其实是一个宏,不是函数,这就意味着它在编译时会变成真正负责输出的代码。”

“就像C语言的printf一样,它用{}来占位。”

“而且,Rust 的格式宏能保证类型安全的参数插入,编译器会在编译时检查格式字符串跟参数类型是否搭配得当。“

赵可菲:“什么叫做宏呢?”

贾克强:“在Rust里,宏可以让我们在编译时对代码做出更复杂的处理和生成。”

Rust的宏在编译时操作代码,通过模式匹配和代码展开来生成代码,这不仅仅是简单的文本替换。”

"Rust的宏有三大亮点哦!"

"首当其冲的就是类型安全。Rust宏在编译时就处理了,保证所有生成的代码都是类型安全的,运行时的错误就少了许多哦!"

"再来就是强大的表达能力。Rust宏支持复杂模式匹配和逻辑,能生成高度定制的代码,让你的代码抽象级别更高,复用性更强!"

"最后是错误检测。既然在编译时就处理宏,编译器就能提供准确的错误信息,让开发者能快速定位问题!"

"但是,Rust的宏也有两个小缺点哦!"

"首先,学习它可能要花一点时间。Rust宏的语法和功能都很强大,但这也意味着它们相对有点复杂,需要时间去学习和掌握。"

"然后就是,它可能会增加编译的时间。特别是复杂的宏,特别是在大型项目中,可能会让编译的时间变长哦!"

"你可能会问,Rust的宏主要用在哪儿呢?像生成重复的代码,实现基于特征(trait)的代码生成,还有条件编译和代码配置等等。"

3.3.1 替换代码文本的C++的宏

席双嘉:“C++的宏与Rust的宏不一样。它是由预处理器用来处理的。C++的宏在编译前就把代码文本进行简单的替换了。“

"C++的宏其实根本不理解代码的含义,只是按照给定的模式替换文本而已。"

"宏,一般都是在头文件中定义的,用 #define 指令就行了。“

"C++的宏,其实还是有点用的,主要有两点。”

"首先,就是灵活。C++宏有文本替换功能,可以在编译前对代码做简单的修改,适合条件编译、代码生成这些场景。”

"其次,就是兼容性很好。宏是C++语言早期的一部分,老代码和库中都有用到,这样就可以保证和历史代码的兼容性了。”

"但是,C++的宏也有不好的地方。”

"首先,它不安全。C++宏只是简单地替换文本,不会检查类型,可能会导致类型错误或者行为出现意外。”

"然后,它还很难调试。宏的错误可以说是很难找的,因为宏在编译前就被替换了,错误信息可能会指向错误的源代码位置。“

"C++的宏,大家一般用来简化重复的代码;做条件编译,比如根据不同的操作系统编译不同的代码块;还有定义常量和简单函数的快捷方式。”

3.3.2 不直接改变代码逻辑的Java的注解

赵可菲:“你们这么一说,在我看来,Java的注解annotation可能跟Rust和C++的宏最像。”

“Java 的注解其实就是一种数据,可以提供一些关于程序的信息,但它不会直接影响程序的运行。”

注解可以在编译时被程序处理,也可以在运行时通过反射来访问。”

“注解可以用在类、方法、变量等地方。”

“Java的注解有三个主要的优点。”

“首先,代码看起来更清楚。注解提供了代码的元数据,不会直接改变代码的逻辑,所以代码结构看起来会更清晰,也更容易维护。”

“第二,框架集成。注解已经成为现代Java框架(比如Spring和Hibernate)的一个核心部分,通过注解,配置和引导框架的行为就变得更简单了。”

“第三,运行时处理。Java注解可以在运行时被读取和处理,这样就可以支持一些动态行为,比如动态代理或反射。”

“但是,注解也有一些缺点。首先,性能开销。运行时处理注解可能会消耗一些性能,尤其是在需要频繁反射操作的时候。”

“其次,复杂性。虽然注解本身很简单,但是处理注解的程序可能会比较复杂,需要额外的学习和理解。”

“注解可以用来提供一些框架级别的信息,比如在Spring框架中定义bean或请求映射;可以用来校验数据;还可以用来自动生成代码,比如getter和setter。”

3.4 小结

Java程序员赵可菲和C++程序员席双嘉,在Rust程序员贾克强的指导下,开始学习Rust。

他们讨论了“猜骰子冷热”游戏的7个用户故事,并写了故事1“获取玩家猜的两个骰子点数之和并显示给玩家”的代码。

Rust的prelude是Rust自动use到每个Rust程序的功能清单,主要关注那些几乎在每个Rust程序中都会用到的特性。

Java的自动import机制和Rust的prelude有些相似,任何Java程序,都可以直接使用自动import的java.lang包里的所有功能。

C++没有像Rust prelude这样的等效物,要明确include使用的每个文件。

机制 Rust的Prelude Java的自动导入 C++的手动包含 概述 Rust在每个Rust程序中自动use一组功能,主要聚焦在几乎每个Rust程序中都会use的功能。 每个Java程序都可以直接使用java.lang包的所有功能,无需显式导入。 在C++中,使用#include指令由程序员显式控制代码库的功能的包含。 编译 use命令在编译器上运行,只涉及到模块范围内的路径和名称的解析,无需将代码复制到源文件中。 import在Java虚拟机(JVM)上运行,它只解决类和包名解析的问题,不会将代码复制到源文件中。 #include在预处理期间工作,它直接将所包含文件的内容复制到源代码文件中,可能会增加编译时间。 类型安全 是 是 否 灵活性 是 是 是 复杂性 中等 低 高 编译时间 较短,只有在编译期间需要解析和检查的路径和名称。 较短,只有在编译期间需要解析和检查的路径和名称。 较长,因为在预处理期间,#include将所有文件内容复制到源文件中。

Rust的println!其实是一个宏,不是函数,这就意味着它在编译时会变成真正负责输出的代码。

C++的宏与Rust的宏不一样,C++的宏在编译前就把代码文本进行简单的替换了。

Java的注解annotation可能跟Rust和C++的宏最像,它不会直接影响程序的运行,可以在编译时被程序处理,也可以在运行时通过反射来访问。

这三种机制,都是为了让代码更加简洁,减少重复,提高我们写代码的速度。

但是,它们还是有些不同。

在功能上,Rust的宏就像超级英雄一样强大,可以创造出复杂的代码;C++的宏主要是做文本替换,比较简单;Java的注解主要是提供元数据,对代码的影响不大。

在安全性上,Rust宏在编译时执行,保持类型安全;C++宏可能会带来类型错误;Java注解自己不会引入执行逻辑,但是注解处理器可能会变得复杂。

在处理时间上,Rust宏和C++宏在编译前后处理,Java注解可能在编译时或运行时处理。

Rust宏 Java注解 C++宏 运行机制 在编译时操作代码,通过模式匹配和代码展开来生成代码 注解是一种数据,可以在编译时被处理,也可以在运行时通过反射来访问 在编译前对代码文本进行简单的替换 优势 保证类型安全,强大的表达能力,错误检测准确 代码看起来清晰,框架集成,支持运行时处理 灵活,兼容性好 劣势 学习曲线可能较陡,可能会增加编译时间 可能会有性能开销,处理注解的程序可能复杂 不安全,难以调试 使用场景 生成重复的代码,实现基于特征的代码生成,条件编译和代码配置 提供框架级别的信息,数据校验,自动生成代码 简化重复的代码,条件编译,定义常量和简单函数的快捷方式

如果你想要了解Rust是如何通过超越传统赋值语句的binding,实现不变性、模式匹配和所有权设计理念的,那就关注我,继续看下去吧!

【未完待续】


如果喜欢我的文章,期待你的点赞、在看和转发。

如果不喜欢,在评论区留个言告诉我哪里不喜欢呗~