Kotlin可以从Rust中学到什么 - Cedric


在开始之前想重申一下,我的观点不是要在两种语言之间发起语言战争,也不是试图将一种语言变成另一种语言。我花了很多时间分析我想要讨论的特性,并自动排除了对一种语言非常有意义而在另一种语言中很荒谬的特性。例如,在 Rust 中要求垃圾收集是愚蠢的(因为它的主要主张是对内存分配的非常严格的控制)并且反过来,Kotlin 采用借用检查器是没有意义的,因为 Kotlin 收集垃圾是其主要诉求之一。
 

我一直对语言中的宏有爱恨交加的关系,尤其是不卫生的。至少,宏应该完全集成在语言中,这需要两个条件:

  • 编译器需要了解宏(例如,与 C 和 C++ 中的预处理器不同)。
  • 宏需要拥有对静态类型 AST 的完全访问权限,并能够安全地修改此 AST。

Rust 宏满足了这两个要求,因此,解锁了一组非常有趣的功能,我很确定我们才刚刚开始探索。
例如,dbg!()宏:
let a = 2;
let b = 3;
dbg!(a + b);

会打印:
[src\main.rs:158] a + b = 5

注意:不仅仅是源文件和行号,还有正在显示的完整表达式(“ a + b”)。
Kotlin 在这方面并非完全没有武装,因为注解和注解处理器的组合提供了一组功能,与您在 Rust 中可以通过宏和属性实现的功能相去甚远。主要区别在于,虽然 Kotlin 的方法只允许合法的 Kotlin 代码出现在 Kotlin 源文件中,但 Rust 允许任何任意语法作为宏参数出现,并且编译器生成正确的 Rust 取决于宏会接受。
一方面,能够在 Rust 源文件中编写任何类型的代码很好(这就是 React 对 JSX 所做的),另一方面,滥用的可能性很高,人们理所当然地担心有一天Rust 源文件看起来与 Rust 代码完全不同。然而,到目前为止,我的恐惧从未成为现实,我遇到的大多数宏都非常节俭地使用自定义语法。
宏的另一个非常重要的方面是 Rust IDE 理解它们(好吧,至少 CLion 理解它们,并且可能所有 IDE 都可以,并且将会)并且它们会在出现问题时立即向您显示错误。
宏用于大量场景,并为 Rust 提供一些非常简洁的 DSL 功能(例如,用于支持 SQL、Web、图形等的库)。
此外,宏与……非常巧妙地集成在一起。
 
预处理的属性
属性是 Rust 版本的注释,它们以或开头#!:
#![crate_type = "lib"]

#[test]
fn test_foo() {}

这里没有什么开创性的东西,但我想讨论的是条件编译方面。
在 Rust 中,通过将属性和宏与 结合来实现条件编译cfg,它既可用作属性又可用作宏。
宏版本允许您有条件地编译语句或表达式:
#[cfg(target_os = "macos")]
fn macos_only() {}

在上面的代码中,函数macos_only()只有在操作系统是 macOS 时才会被编译。
宏版本cfg()允许您向条件添加更多逻辑:

let machine_kind = if cfg!(unix) {
    "unix"
} else { … }

上面的代码是一个宏,这意味着它在编译时,编译器将完全忽略条件的任何部分。
您可能会想知道 Kotlin 中是否需要这样的功能是有道理的,我也问过自己同样的问题。
Rust 在多个操作系统上编译为本地可执行文件,如果您想在多个目标上发布工件,这使得这种条件编译非常必要。Kotlin 没有这个问题,因为它生成了在 JVM 上运行的操作系统中立的可执行文件。
尽管 Java 和 Kotlin 开发人员已经学会了不用预处理器,因为 C 预处理器给几乎每个使用过它的人留下了如此糟糕的印象,但在我的职业生涯中,也有一些情况能够进行条件编译,包括或排除源代码文件,甚至只是语句、表达式或函数,都会派上用场。
不管你在这场辩论中的立场如何,我不得不说我真的很喜欢 Rust 生态系统中两个截然不同的特性,宏和属性,能够协同工作以产生如此有用和多功能的特性。
 
扩展特性
扩展特征允许您“事后”使结构符合特征,即使您不拥有这些特征中的任何一个。最后一点需要重复:结构或特征是否属于您未编写的库并不重要。您仍然可以使该结构符合该特征。
例如,如果我们想在u8类型上实现一个函数last_digit():

trait LastDigit {
    fn last_digit(&self) -> u8;
}

impl LastDigit for u8 {
    fn last_digit(&self) -> u8 {
        self % 10
    }
}

fn main() {
    println!("Last digit for 123: {}", 123.last_digit());
   
// prints “3”
}

首先,我发现 Rust 语法优雅和简约(甚至比 Haskell 更好,并且可以说,比我为 Kotlin 提出的更好)。其次,能够以这种方式扩展特征在建模问题方面释放了很多可扩展性和功能,但我不会深入研究这个主题,因为它会花费太长时间(查找“类型类”以了解您可以实现的目标)。
这种方法还允许 Rust 模仿 Kotlin 的扩展函数:
fun Type.function() {...}

同时提供一种更通用的机制来扩展函数和类型,但代价是语法稍微冗长一些。
 
Cargo交付
这可能令人惊讶,因为在 Gradle 中,Kotlin 拥有非常强大的构建和包管理器。这两个工具当然具有相同的功能表面区域,允许构建复杂的项目,同时还管理库下载和依赖项解析。
我认为Gradle 是一个更好的替代品的原因是因为它在声明性语法和命令式方面之间进行了清晰的分离。简而言之,标准的、通用的构建指令在声明性cargo.toml文件中指定,而临时的、更具程序性的构建步骤则直接在 Rust 中的名为 的文件中编写build.rs,使用 Rust 代码调用一个相当轻量级的构建 API。
相比之下,Gradle 一团糟。首先是因为它开始在 Groovy 中被指定,现在它支持 Kotlin 作为构建语言(并且这种转变仍在进行中,在它开始多年后),但也因为这两者的文档仍然非常糟糕
我所说的“糟糕”并不是指“缺乏”:有很多文档,只是……糟糕、不堪重负,其中大部分已经过时或已弃用,等等……需要从 StackOverflow 复制/粘贴数百行作为一旦你需要一些与众不同的东西。插件系统的定义非常松散,基本上允许所有插件访问 Gradle 内部结构中的任何内容。
显然,我对这个主题非常固执,因为我创建了一个受 Gradle 启发的构建工具,但使用了更现代的语法和插件解析方法(称为 Kobalt),但与此无关,我认为cargo设法取得了非常好的平衡在一个灵活的构建+依赖管理器工具之间,该工具充分涵盖了所有默认配置,而不会随着项目的增长而变得非常复杂。
 
u8, u16, ...
在 Rust 中,数字类型非常简单:u8是 8 位无符号整数,i16是 16 位有符号整数,f32是 32 位浮点数,等等……
这对我来说是一种清新的空气。在我开始使用这些类型之前,我从未完全确定我一直对 C、C++、Java 等定义这些类型的方式感到不舒服。每当我需要一个数字时,我都会使用int或Long作为默认值。在 C 中,我有时甚至long long没有真正理解其中的含义。
Rust 迫使我密切关注所有这些类型,然后,每当我尝试执行可能导致错误的强制转换时,编译器都会毫不留情地让我保持诚实。我真的认为所有现代语言都应该遵循这个约定。
 
编译器错误信息
并不是说 Kotlin 的错误消息很糟糕,但 Rust 确实在多个维度上在这里树立了新标准。
简而言之,以下是您可以从 Rust 编译器中得到的期望:

  • 带有箭头、颜色、问题部分的清晰描述的 ASCII 图形。
  • 简单的英语和详细的错误消息。
  • 关于如何解决问题的建议。
  • 相关文档的链接,您可以在其中找到有关该问题的更多信息。

我当然希望未来的语言能够获得灵感。
 
可移植性
大约 25 年前,当 Java 出现时,JVM 做出了一个承诺:“一次编写,随处运行”(“WORA”)。
虽然这一承诺在早些年站不住脚,但不可否认,WORA 在今天已经成为现实,并且已经存在了几十年。JVM 代码不仅可以编写一次,到处运行,这样的代码还可以在任何地方编写,这对开发人员来说是一个重要的生产力提升。您可以在任何 Windows、macOS、Linux 上编写代码,并在任何 Windows、macOS 和 Linux 上部署。
令人惊讶的是,即使 Rust 生成本机可执行文件,它也具有这种多功能性。无论您在何种操作系统上编写代码,生成大量可执行文件都是不大的,而且这些可执行文件是本机的,而且由于 LLVM 令人难以置信的技术成就,其额外的好处是性能也非常好。
在 Rust 之前,我已经接受了这样一个事实,即如果我想在多个操作系统上运行,我必须付出在虚拟机上运行的代价,但 Rust 现在表明你可以拥有你的蛋糕,也可以吃它。
Kotlin(以及整个 JVM)也开始吸取这一教训,通过 GraalVM 等举措,但为 JVM 代码生成可执行文件仍然充满限制和限制。
 
总结
TestNG是我在 2004 年左右开始的一个项目,唯一的目的是混淆事物。我想向 Java 世界展示我们可以比 JUnit 做得更好。我无意让任何人喜欢和采用 TestNG:它是一个项目实验室。一个实验。我想做的就是证明我们可以做得更好。我真的希望 JUnit 团队(或其他任何团队)能够看看 TestNG 并思考“哇,从来没有想过!我们可以将这些想法融入 JUnit 并使其变得更好!”。
如果这两个非常非常不同的世界(Rust 和 Kotlin 社区)从他们极快的发展速度中停下来,快速地看看彼此。我也会欣喜若狂。
我是一个要求很高的开发人员,我已经写了 40 年的代码,并计划在智力允许的情况下继续这样做。我对编程语言感到莫名其妙的热情,我希望我的热情通过这两篇文章闪耀。