Beyond Compare! Rust Vs Js

本文作者系360奇舞团前端开发工程师

文章标题:Beyond Compare! Rust Vs Js

前言

笔者最近计划开发一个SSR渲染的Blog。筹备之初,一度犹豫不决:是继续沿用较为熟悉的JavaScript技术栈,还是尝试当下非常热门的Rust相关技术栈,经过考虑,决定要做一些有挑战性的尝试。 一般而言,WEB开发,动手之前,我们都会在脑海里会有一个大概的雏形,也就是说无论采用哪种语言、哪种方案作为主要的技术栈,项目结构通常都是将前后端分离开来,类似于以下这种结构:

  • xxx-frontend
    • api
    • hooks
    • styles
    • utils
    • constant
    • assets
    • components
    • pages
    • App.ts
    • ...
    • Header.tsx
    • Footer.tsx
    • ...
    • Home.tsx
    • login.tsx
    • public
    • docker
    • public
    • build
    • src
    • package.json
    • tsconfig.json
    • dev.env
    • ...

Rust的学习曲线通常被认为是比较陡峭的,尤其是对于前端开发者来说,Rust的很多概念与前端使用的语言存在很大的差异,可能会使得很多对此感兴趣的rustaceans[1]多次经历“从入门到放弃”的尴尬阶段。本文试着从前端的视角来找出Rust与js/ts的语法相似处,进而“触类旁通”,加深对于Rust这门语言的了解。

Rust的某些重要的新特性、新的改动总是充满争议,譬如Async。下面是截取自Hacker News中关于Rust Async这一语言特性的两种截然相反的却又极具代表性的两类评价:

"我觉得这样一来,大家都会跃跃欲试去尝试Rust[2]。我本人就非常期待这一刻的到来。这一个新的大门已经开启,等着我们去探索。而且,这个更新具有我们想要的一切特性:开源、工程质量高、设计过程公开,非常厉害!"

反对者认为:

"我真的不能理解,为什么有人会看Rust一团乱麻的异步编程部分然后觉得它是个好设计[3]。我多次试图去理解它,但最后我发现它实在是太混乱了!我真的很喜欢Rust,现在我的大部分工作都是用它做的,但是每当我遇见Rust的异步代码时,我就感到非常的懊恼"

是的,你没看错,Rust中也有Async语法,在这一系列博文中[4],作者作为在2017年到2019年间,推动Async/Await语法的设计的合伙人,可谓是“深层次、多方面全角度”的阐明了Rust的为何采用这样的设计。

对于Rust的异步编程的决策背景是Rust决定用"无栈协程[5]”对用户空间并发进行实施。

理论上,语言都有将工作划分为任务并将这些任务安排到线程上的方式。

Rust的这个系统就是async/await。

并且作者文章中提到了为何Rust会决定——移除"绿色线程"(green threads)。

作者解释了绿色线程如何影响内存管理、性能,以及它们与其他语言和平台的交互问题。

并最终阐述了为什么Rust最终选择基于一个可优化的状态机[6]实现Future,以此构建其异步生态。

本文主要脉络

  • 注释

注释这一部分往往被大部分开发者忽略...,清晰的项目目录结构和必要的代码注释(现实中往往注释和文档欠缺?...)总是项目中必不可少的组成部分。

这里将几个不同点简单罗列:

  • 文档注释:

Rust 使用 /// 对单行文档注释进行标记,使用 //! 给包、模块或函数添加注释。Rust 还支持 /** ... */ 形式的文档注释。

这些注释可以被解析生成文档:

        /// 这是一个文档注释,描述了下面的函数
        fn some_function() {
            // ...
        }
        
        //! 这是一个模块级别的文档注释
  • 同时rust还支持嵌套注释:
/* 这是外层注释开始
// 这里有一行被注释的代码
/* 这是内层嵌套注释开始
更多的代码
*/
 内层嵌套注释结束
还有更多的代码
*/
 外层注释结束
  • 数据类型:
    • 相似:JavaScript/TypeScript和Rust都有基本数据类型,如数字(number/i32, f64),布尔(boolean/bool)和字符串(string/String)。
    • 不同:Rust是静态类型语言,有更严格的类型系统,包括元组、数组和结构体(struct)等复合类型。Rust不允许隐式类型转换。
  • 变量声明:
    • 相似:变量可以用let声明,TypeScript中可以选择是否使用类型注解。
    • 不同:Rust中的变量默认是不可变的(immutable);要声明可变变量需要使用mut关键字。此外,Rust的变量需要类型注解。
  • 函数:
    • 相似:函数的基本结构在语言之间类似,都有参数和返回值。
    • 不同:Rust函数参数和返回值需要类型注解。Rust还有所有权和借用的概念。
  • 流控制:
    • 相似:条件语句(if)、循环(for、while)、match(类似TypeScript的switch)等在所有语言中都有所体现。
    • 不同:Rust中的match语句是穷尽性(exhaustive),必须处理所有可能的匹配项。
  • 错误处理:
    • 相似:都有错误处理的概念。
    • 不同:Rust使用Result和Option枚举来处理可能的错误和空值,而不是JavaScript/TypeScript中的try/catch和null/undefined。
  • 模块系统:
    • 相似:都有模块化代码的概念。
    • 不同:Rust有一个更严格的模块系统,需要显示地通过mod和use关键字管理。
  • 异步编程:
    • 相似:都支持异步编程。
    • 不同:Rust的异步编程是通过Future trait和async/.await实现的,与JavaScript/TypeScript的Promise不同。

Async/Await

无论是前/后端,异步编程都是一个重要的话题。近年来,许多编程语言中都引入了async/await用于优雅地处理异步操作:

  • Python 的 asyncio 也有类似的模型。不过,它的 Future 类型是基于回调的,而不是轮询的。Python 异步程序需要一个 "循环",类似于 Rust 中的运行时。

  • JavaScript 的 Promise 与之类似,但同样基于回调。语言运行时实现了事件循环[7],因此 Promise 解析的许多细节都被隐藏起来。

Rust 和 JavaScript 都支持异步编程,并且都采用了 async/await 语法来简化异步操作的处理流程。不过,它们在异步实现和上下文中存在很多差异。以下是对两种语言中 async/await 的简单对比:

  • JS中的 async/await本质上是语法糖

JavaScript 的 async/await 是基于 Promise 的语法糖。当你在函数前加上 async 关键字后,这个函数会隐式返回一个 Promise 对象。

  • 事件循环

JavaScript 运行在单线程中,依赖事件循环来处理异步行为。await[8] 关键字可以暂停当前 async 函数的执行,等待 Promise 解决,然后恢复执行。

  • 错误处理:可以使用传统的 try/catch 语法来捕获异常。

回过头来,我们再看下Rust中的 async/await:

  • Rust 的 async/await 是基于 Future trait 的抽象。async 将一个代码块变成返回 Future 的状态机,而 await 会等待 Future 完成。
  • Executor

Rust 需要一个Executor来轮询 Future 直到其完成。这与 JavaScript 的事件循环不同,Rust 的Executor可以是多线程的,也可以更加灵活地控制。

  • Error handling

Rust 中的 Result[9]Option[10] 类型可以与 async/await 结合,来处理可能的错误或者空值。

由此我们可以得知二者的主要差异大致为以下几点:

  • 异步实现

JavaScript 中的异步操作是内建在语言和执行环境中的,而 Rust 的异步实现则更为底层,需要Executor的支持。

  • 线程和并发

JavaScript 的异步编程依赖单线程的事件循环,而 Rust 可以利用其强大的并发能力[11],通过多线程执行异步任务。

当我们谈论Async时,我们在谈论什么

下面我们重点看一下Rust 中的 async/await:

  • 类型系统

Rust 强类型系统的一部分,async 函数返回的是一个 Future,这是一种在编译时就确定了输出类型的泛型 trait。

这与 JavaScript 的 Promise 相比,为错误检查和优化提供了更强的保障(当然也更复杂...)。

  • 内存安全

Rust 的内存安全保证也适用于异步编程。使用 async/await 时,总绕不开所有权和生命周期这类编译难题。

  • Executor

Rust 的异步任务需要一个外部的Executor来驱动 Future 对象的完成。

  • 并发模型

Rust 允许开发者利用其强大的并发能力,例如通过使用多线程,无需担心数据竞争或其他并发问题,这是 JavaScript 单线程模型所无法比拟的。

  • Poll-based

Rust 中 Future 的执行是基于轮询(poll-based)的。

当调用一个 Future 的 poll 方法时,如果任务未完成,它会返回 Pending,并在准备就绪时通知Executor再次轮询。这种方式与 JavaScript 的事件循环有本质上的不同。

  • Future

异步函数返回一个实现了 Future trait 的类型。

此处就需要用 .await 显式等待 Future 完成,并且在等待过程中,编译器保证类型和生命周期的安全:

async fn fetch_data() -> Result<(), Box<dyn Error>> {
    let response = reqwest::get("some_url").await?;
    let data = response.json::<HashMap<StringString>>().await?;
    println!("{:?}", data);
    Ok(())
}
  • 错误处理

Rust 使用 Result 枚举来显式处理错误,这种方式要求开发者在代码中显式地处理每一个可能的错误,这样可以减少运行时错误的发生。

  • 无需回调函数

Rust 的 async/await 使得开发者能够写出无需回调函数的异步代码,这样可以避免 JavaScript 中常见的回调地狱问题,使代码更加清晰和易于维护。

  • 生命周期标注

在 Rust 中,你可能需要为函数参数和返回值添加生命周期标注,以确保在异步操作中引用的数据在使用期间保持有效。这是 JavaScript 中不存在的概念。

总体来说,Rust 的 async/await 在保证性能、安全性以及类型正确性方面提供了更多的控制和保障,但这也意味着它的使用和理解要远比 JavaScript 更复杂。

以下是一个简单的Rust异步编程的Demo:

// 引入标准库中的`Future`和执行异步任务所需的`executor`。
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::thread;
use std::time::Duration;

// 等待一秒钟然后返回一个数字
async fn wait_and_return() -> u32 {
    // 模拟一个耗时的I/O操作。在现实世界中,这可能是一个HTTP请求。
    thread::sleep(Duration::from_secs(1));
    42 // 在等待一秒后返回数字
}

// 主函数也是异步的,这样它可以使用`.await`。
#[tokio::main] // 使用tokio运行时运行主函数
async fn main() {
    // `wait_and_return()` 是一个异步函数,但它本身不会执行任何操作。
    // 需要调用`.await`来等待它完成。
    let number = wait_and_return().await;

    println!("Got number: {}", number);
}

回过头来我们简单分析下这段代码:

我们定义了一个异步函数wait_and_return,使用async fn声明。这个函数内部执行了一个阻塞操作(thread::sleep),在现实的应用中,这将是一个非阻塞的异步操作。

使用#[tokio::main]属性宏来指定tokio Executor作为异步代码的运行时(runtime)。这是因为异步函数需要一个Executor来驱动他们,而Tokio是Rust生态中最流行的异步运行时之一。

在main函数中,我们调用wait_and_return().await来等待异步函数的结果。.await会暂停当前任务,并将控制权交回Executor,只有当wait_and_return()函数准备好继续执行时才会恢复。

异步函数返回的是一个Future,它是一个表示未来某个时候会产生值的操作。Future本身并不执行计算,除非我们将它提交到Executor上。

请注意,在真实的异步操作中,我们不会使用thread::sleep这样的阻塞操作。我们会使用非阻塞的异步I/O,例如Tokio提供的异步文件和网络操作。上面的Demo仅用于说明async/.await的使用。使用tokio或其他异步库时,会有配套的功能来处理异步的延时和其他I/O操作。

简单总结

读到这里,我们由此可得出一个简单的结论:JavaScript和Rust的async/await语法针对的都是异步编程:

  • 二者使用相同的关键词async和await,async用于声明一个函数是异步的,await用于暂停并等待一个Promise(JS)或Future(Rust)的完成。
  • 都让异步代码看起来"更像"同步代码了

但是很重要的一点区别就是JS中的异步调用默认是非阻塞的,而Rust中的异步调用需要在异步运行时(例如tokio)中执行才能实现非阻塞

比如下面的这个"Go to Sleep"的简单Demo:

use tokio::time::{delay_for, Duration};
async fn sleep() {
    println!("sleep start");
    // Future await
    delay_for(Duration::from_secs(2)).await;
    println!("sleep end");
}

#[tokio::main]  // 使用tokio作为异步运行时
async fn main() {
    println!("main start");
    sleep().await;
    println!("main end");
}

Borrow checker

Rust的Borrow Checker是一种编译时检查机制,它确保程序中的引用是有效的并且遵守Rust的所有权、借用和生命周期规则。这个机制有助于避免数据竞态[12]垂悬指针[13]和其他内存安全问题。

  • 首先我们简单了解下所有权(Ownership):
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 的所有权被移动(move)到了 s2

    println!("{}, world!", s1); // 错误!s1 已经不再有效
}

在这个Demo中,s1的所有权被移动到了s2。当我们尝试使用s1时,编译器会报错,因为s1不再持有那个字符串的所有权。

  • 然后我们看下借用(Borrowing)是如何起作用的:
fn main() {
    // 结构体关联函数
    let s1 = String::from("hello");
    let s2 = &s1; // 借用s1,s2是一个对s1的不可变引用

    println!("{} world", s2); // 正确,s2是不可变引用,可以读取s1的数据
    println!("{} world", s1); // 仍然正确,s1的所有权没有被移动
}

在这里s2是对s1的不可变借用,所以我们可以安全地读取s1的值。

  • 这里不得不提到可变借用(Mutable Borrowing):
fn main() {
    let mut s = String::from("hello");

    {
        let s_mut = &mut s; // 可变借用s
        s_mut.push_str(", world");
    } // s_mut的借用在这里结束,可以再次借用或使用s

    println!("{}", s); // 正确,s现在包含"hello, world"
}

这个Demo展示了可变借用和借用作用域。内部块中,我们对s进行了可变借用,并修改了它的内容。借用结束后,我们又可以安全地访问s。

生命周期(Lifetimes)

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("Rust");
    {
        let s2 = "C";
        let result = longest(s1.as_str(), s2);
        println!("The longest string is {}", result);
    } // s2的生命周期结束
}

在这个Demo中,函数longest有一个生命周期参数'a,它指定了输入参数x和y必须至少和返回的引用一样长。s1和s2在main中有不同的作用域,但它们在调用longest时都是有效的。

Borrow Checker在编译时确保这些规则被遵循,从而在运行时防止出现错误。

详细比对rust与ts中的enum

在Rust和TypeScript(TS)中,enum都是用来定义一组命名的常量,但它们在使用上有一些显著的差异。

  • 我们看下Rust中的Enum:

Rust中的enum是非常强大的,它不仅可以像传统的枚举那样用来表示一组相关的常量,而且每个枚举成员可以有不同的类型和数量的关联数据。

以下是Rust中枚举的定义和使用的简单示例:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32i32i32),
}

fn main() {
    let msg = Message::Write(String::from("hello"));
    // ... 使用match来处理msg
}

在上面的Message枚举中,我们定义了四种不同类型的消息,每一种都可以携带不同类型和数量的数据。例如,Write变种携带一个String,而ChangeColor带有三个i32类型的值。

Rust中的枚举非常适合模式匹配,这使得分解枚举值和访问其数据变得非常方便。

TypeScript中的Enum

TypeScript的枚举对于前端小伙伴而言就很熟悉了,它更接近于传统的枚举,通常用作定义一组命名的数字常量,虽然TypeScript的枚举也可以定义为字符串。

以下是TypeScript中枚举的定义和使用的Demo:

enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}

// 字符串枚举
enum FileAccess {
    // constant members
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // computed member
    G = '123'.length
}

let dir: Direction = Direction.Up;

在上面的Direction和FileAccess枚举中,我们定义了一些命名的常量。这些值默认从0开始自增,但也可以显式地指定值。

通过以上的对比我们可以看出二者存在的几点差异:

  • 关联数据:Rust的枚举成员可以携带不同类型和数量的关联数据,而TypeScript的枚举通常不带数据。
  • 模式匹配:Rust通过match表达式使用模式匹配来解构和处理枚举成员和它们的数据,TypeScript中没有这样的机制。
  • 类型安全性:Rust的枚举是安全类型的,每个成员是枚举的唯一类型。TypeScript中枚举成员是其枚举类型的子类型,可以赋值给数字类型变量,这降低了类型严格性。
  • 计算成员和常量成员:在TypeScript中,计算成员和常量成员可以共存于一个枚举中,而Rust的枚举成员只能是其中之一。
  • 运行时:TypeScript的枚举在编译JavaScript后在运行时存在,而Rust的枚举在编译后不直接存在于运行时代码中,它们被编译成了其携带的数据的结构。

由此我们可以得出以下结论:

尽管两者均用enum关键字定义,但Rust和TypeScript中的enum在使用和功能上有着很大差异。Rust的枚举提供了更强的类型检查和模式匹配能力,而TypeScript的枚举更接近传统的枚举[14]用法,侧重于为一组数值命名。

TypeScript中的接口和Rust中的trait有何区别?

TypeScript 中的接口(Interfaces)和 Rust 中的 trait 在目的相似,只不过是在定义使用方式上存在一系列的区别。

首先我们看下TypeScript 中的接口

在 TypeScript 中,接口是一个纯粹的类型层面的抽象,都是在编译时进行检查的,并不会编译成任何 JavaScript 代码。接口可以描述对象的一系列特征,也可以用于注解参数和返回类型:

interface Animal {
    name: string;
    numberOfLegs: number;
    makeSound(sound: string): void;
}

function greet(animal: Animal{
    console.log(`Hello, ${animal.name}`);
}

在 TypeScript 中,接口可以被类实现(rust中,impl <特性名 > for <所实现的类型名 >),它们也可以继承其他接口:

interface Mammal extends Animal {
    liveBirth(): void;
}

class Dog implements Mammal {
    name: string;
    numberOfLegs: number;

    makeSound(sound: string) {
        console.log(sound);
    }

    liveBirth() {
        console.log("Dogs give birth to live young.");
    }
}

由于本文主要面向前端开发伙伴,所以这里有关于TS的接口相关不多赘述。

Rust 中的 trait

在 Rust 中,trait 不仅可以定义一组要由某些类型实现的方法,还可以包含默认方法实现:

trait Animal {
    fn make_sound(&self, sound: &str);
}

struct Dog;

impl Animal for Dog {
    fn make_sound(&self, sound: &str) {
        println!("{}", sound);
    }
}

Rust 的 trait 可以指定关联类型,并且trait本身可以是泛型的:

trait Animal<T> {
    fn baby_name() -> String;
}

struct Dog;

impl Animal<Dog> for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

除此之外,Rust 的 trait 还可以有条件实现,也就是说基于一些条件为类型自动实现 trait。

主要区别

  • 默认方法: Rust 的 trait 可以有默认方法实现,而 TypeScript 的接口不可以。
  • 关联类型: Rust 的 trait 可以包含关联类型。这对于约束实现 trait 的类型返回某种特定内部类型非常有用。
  • 泛型: Rust 的 trait 可以是泛型的,且可以基于泛型参数有条件地实现。TypeScript 的接口也可以进行泛型参数化,但没有条件实现。
  • 运行时影响

TypeScript 的接口在编译后的 JavaScript 代码中没有直接体现,而是完全在编译时用于类型检查;Rust 的 trait 可以影响运行时行为,特别是它们可以用于创建动态的 trait 对象。

  • 约束:

在 Rust 中,trait 可以用于约束泛型类型,这对于编写类型安全的泛型函数和结构体至关重要。TypeScript 的接口也可以这样用,但是它们的约束在编译成 JavaScript 后不会保留。

...尽管 TypeScript 的接口和 Rust 的 trait 在概念上相似,它们在实现、功能和约束方面具有显著的差异。

TypeScript 的接口更多地用于静态类型检查和定义对象的形状,而 Rust 的 trait 则提供了深层次的类型系统集成、运行时多态性和类型安全的泛型编程。

思考:rust的trait某种程度上"表现"的很像ts中原型链继承

简单对比rust中的structs与js中的Object或者Class

首先我们看下Rust 的 Structs

在 Rust 语言里,struct(结构体)是一种自定义数据类型,它能够用来创建有组织的数据项的集合。

  • 静态类型:每个结构体的字段都明确地声明了其数据类型。
  • 所有权和生命周期:Rust 结构体与 Rust 的所有权、借用和生命周期规则密切相关,确保了内存安全。
  • 方法定义:在 Rust 中,你可以为结构体实现方法。这通过 impl 来完成。
  • 模式匹配:可以与 Rust 的 match 语句结合,使用模式匹配来解构结构体。 例如:
struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn greet(&self) {
        println!("Hello, my name is {}."self.name);
    }
}

let person = Person { name: String::from("Alice"), age: 30 };
person.greet();

回过头来我们看下JavaScript 的 Object 和 Class

  • 动态类型:JS 是一个动态类型语言,对象的属性可以在运行时添加、修改或删除。
  • 原型继承:JS 使用基于原型的继承。每个 JS 对象在创建时都会与另一个对象相关联,并可以继承其属性和方法。
  • 灵活性:JS 对象可以存储各种类型的值,不论其是原始值、对象还是函数。
  • 函数是一等公民:在 JS 中,函数可以作为对象的属性(方法)存在。
  • Class 语法糖:JS 的 class 是ES6引入的语法糖,目的是让基于原型的对象创建更接近传统的基于类的编程。
  • this 关键字:JS 的方法中经常使用 this 来引用当前对象,它的值取决于函数的调用方式。 例如:
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() { 
        console.log(`Hello, my name is ${this.name}.`);
    }
}

const person = new Person("Alice"30);
person.greet();

结论

  • 类型系统

Rust 有着静态的、强类型系统。每种数据类型都在编译时定义清楚。而 JS 是动态的、弱类型系统,对象属性可以灵活变化。

  • 内存管理

Rust 通过所有权模型管理内存,而 JS 依靠垃圾回收机制。

  • 默认可变性

Rust 中不可变是默认的,而在 JS 中,对象是默认可变的。

  • 继承机制

Rust 没有基于原型的继承机制,而是使用 Trait。JS 使用基于原型的继承。

  • 数据封装

Rust 允许更细粒度的控制数据封装,通过 pub 关键字明确字段的公开程度。在 JS 中,所有对象属性默认都是公开的。

  • 方法和函数

Rust 使用 impl 为类型定义方法,JS 对象或类的方法则直接定义在对象或者类的字面量里。

Vector vs Array

在 Rust 中,Vec是一种动态数组,被广泛用于存储同类型值的集合。Vec 拥有以下特性:

  • 动态大小:Vec 可以根据需要增长或缩小,而无需在编译时确定大小。
  • 堆分配:Vec 中的数据存储在堆上,它的大小在运行时可以改变。
  • 所有权系统:Rust 使用所有权系统来管理内存,Vec 确保内存安全且没有垃圾产生。
  • 迭代器:Rust 提供强大的迭代器 API 来操作 Vec 中的元素。
  • 泛型:Vec使用泛型来存储任意类型 T 的对象,且 T 必须满足 Sized trait。

与 Vec 对应的还有固定大小的数组,其大小在编译时必须已知且无法更改:

let fixed_array: [i325] = [12345]; // 定义了固定大小的数组
let mut vec: Vec<i32> = Vec::new(); // 可动态变化的 Vec
vec.push(1); // 向 Vec 添加元素

TypeScript

在 TypeScript 中,数组是 JavaScript 的原生对象的一个扩展,可以容易地在 TypeScript 中使用,并且拥有以下特性:

  • 动态大小:TypeScript 数组的大小不是固定的;可以在任何时候向数组添加或删除元素。
  • 类型注解:TypeScript 支持类型注解,使得数组可以明确其存储的元素类型。
  • 灵活性:数组可以通过 .push(), .pop(), .slice() 等方法灵活操作。
  • 泛型:TypeScript 数组可以使用泛型来存储任意类型的对象,例如 Array。 在 TypeScript 中,通常使用两种方式声明数组:
let fixed_array: number[] = [12345]; // 使用类型注解声明数组
let generic_array: Array<number> = [12345]; // 使用泛型声明数组

简单结论

  • 类型系统

TypeScript 作为 JavaScript 的超集,具有动态类型语言的灵活性,并通过类型注解提供静态类型检查。Rust 有着静态、强类型系统,并且更加注重类型的显式性和安全性。

  • 性能

Rust 的 Vec 类型通常表现出更优的性能和内存使用效率,因为它更灵活地控制内存分配,并且避免了垃圾回收的损耗。

迭代器(Iterator)

JavaScript和Rust都提供了迭代器(Iterator)的概念,允许开发者在容器(例如数组或向量)上进行遍历操作。

在JavaScript和Rust中使用迭代器,主要有以下几个不同之处:


    1. 语法和构造方式

JavaScript:

迭代器协议在JavaScript中是通过实现Symbol.iterator[15]函数来定义的,该函数返回一个带有next()方法的迭代器对象。

next()方法返回一个带有value和done属性的对象。

通常不需要手动创建迭代器,因为许多内置类型(如Array, Map, Set等)已经默认实现了迭代器协议。

可以使用for...of循环来遍历迭代器。

let array = [123];
for (let value of array) {
  console.log(value); // 输出:1 2 3
}

Rust:

Rust中迭代器是通过实现Iterator trait(类似于接口)来定义的,该trait要求定义next()方法。

next()方法直接返回Some(value)或None来表明是否还有值。

Rust中也有许多内置集合类型实现了Iterator trait,如Vec, HashMap, HashSet等。

可以使用for循环来遍历迭代器。

let vector = vec![123];
for value in vector.iter() {
    println!("{}", value); // 输出:1 2 3
}

尽管两种语言在迭代器的设计和使用上有共通之处,但也存在显著的差异:

相似点:

  • 提供遍历操作:两者都支持迭代器协议,允许对集合进行元素接连访问的操作。
  • 链式调用:均支持链式调用,可将多个迭代器操作组合起来(如map、filter等)。
  • 延迟计算:两者的迭代器都是延迟计算的,只在迭代器被消费时才真正地执行计算。

不同点:

  • 类型系统:
    • JavaScript的迭代器是动态类型的,不需要在使用时指定元素的类型。
    • Rust的迭代器是静态类型的,且类型检查发生在编译时,更严格但也更安全。
  • 错误处理:
    • JavaScript中,错误通常通过抛出和捕获异常来处理。
    • Rust中,迭代器可以传递Result类型,能够在遍历时处理错误,并且显式地区分成功和错误的值。
  • 并行迭代:
    • JavaScript的并行操作依赖于异步编程模型,如Promises和async/await。
    • Rust可以通过rayon这样的库来实现数据的并行迭代,这在Rust中是类型安全并且容易实现的。
  • 所有权和借用:
    • JavaScript/TypeScript没有所有权和借用的概念,迭代过程中无需关心变量的生命周期。
    • Rust严格区分值的所有权,迭代时需要注意所有权和借用规则,例如使用可变迭代器时可能需要借用可变引用。
  • 性能:
    • JavaScript/TypeScript的性能受到其解释执行的引擎影响。
    • Rust的性能更高,编译器可以进行更多的静态优化。

对于Rust 迭代器相关的概念感兴趣的读者朋友推荐阅读Rust迭代器:高阶教程[16]

结语

在学习和编写Rust的道路上必须克服的第一个主要障碍,就是Borrow Checker本身[17]

对于前端伙伴来说,学习Rust的一个有效策略是多写、多练、多多的compile Error...

"自从用了Rust,我就习惯于看不到我的代码,而是一大串编译错误。"

或者也可以像本文作者的思路一样,试着在Rust中实现已经我们熟悉的JavaScript/TypeScript功能或者概念,这样可以直观地看到两种语言的相似之处和不同之处,进而加深认知。

参考资料[1]

rustaceans: https://medium.com/kbtg-life/that-is-why-you-and-i-should-become-rustaceans-31ddd212e52f

[2]

Rust async: https://news.ycombinator.com/item?id=21473418

[3]

Rust一团乱麻的异步编程: https://news.ycombinator.com/item?id=37436413

[4]

Why async rust: https://without.boats/blog/asynchronous-clean-up/

[5]

stackless coroutine: https://ruststack.org/stackless-coroutine/

[6]

State Machine: https://en.wikipedia.org/wiki/Finite-state_machine

[7]

Event Loop: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop

[8]

JS Await: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

[9]

Result is similar to Option: https://google.github.io/comprehensive-rust/std-types/result.html?highlight=result#result

[10]

Option: https://google.github.io/comprehensive-rust/std-types/option.html?highlight=Option#option

[11]

Rust too-many-web-servers: https://ibraheem.ca/posts/too-many-web-servers/

[12]

Date races in Rust: https://users.rust-lang.org/t/data-races-in-rust/54627

[13]

Dangling Reference: https://www.reddit.com/r/rust/comments/tw2mdd/what_are_dangling_references_in_rust/

[14]

what are enums and why are they useful: https://stackoverflow.com/questions/4709175/what-are-enums-and-why-are-they-useful

[15]

Symbol.iterator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator

[16]

Rust的迭代器在整个代码库中被广泛使用: https://blog.jetbrains.com/rust/2024/03/12/rust-iterators-beyond-the-basics-part-i-building-blocks/

[17]

Fast Development In Rust, Part One: https://blog.sdf.com/p/fast-development-in-rust-part-one


相关推荐

  • 招募200名互联网运营师,不限经验,男女可报!居家办公!
  • [开源]MIT开源协议,智慧协同办公OA,企业级协同办公整体解决方案
  • 每日 prompt:黑猫手机壁纸
  • 黄仁勋的GTC大会集齐 Transformer 论文七大作者
  • 工厂数字化系统是自研?还是对外采购?
  • 3.8K Star小众Dart实现的键鼠输入可视化软件
  • 大模型时代,5个最顶级的向量数据库
  • Android玩家折腾不动了
  • Oracle正式发布Java 22
  • 开源日报 | 马斯克为何要作开源 “秀”;当初质疑倪光南的人,今天果然被打脸;Java 22正式GA;压缩的二三事
  • Redis不再 “开源”
  • Maven 中的 classifier 属性用过没?
  • 最新955不加班的公司名单
  • ​库克谈苹果生成式 AI ;OpenAI有望在年中发布GPT-5;微软开源远程缓存存储系统 Garnet | 极客头条
  • C++ 之父反驳白宫警告:自诞生第一天起,C++ 的目标就一直是提高安全性
  • 瘫痪8年的29岁小哥成为马斯克脑机接口试验第一人,手术2个月后,现身开启直播意念玩象棋!
  • 腾讯发布自研游戏AI引擎:3D城市布局效率提升百倍,UGC工具已上线《元梦之星》
  • 黄仁勋组局,Transformer七子首次重聚对谈 | 中文实录
  • 脑后插管玩《文明6》!马斯克Neuralink首个人类志愿者,直播意念下象棋
  • 突发!Stable Diffusion核心团队被曝集体离职