Rust 难在哪里?

Rust 很难学。不是因为不努力–很多很多人花了很多年的时间来改进诊断、文档和 API,而是因为它很复杂。当人们第一次学习 Rust 语言时,他们正在学习许多不同的交错概念:

 💬 170 条评论 |  rust | 

注意:本文并非Rust教程。

每年将Rust教学融入课堂都面临巨大挑战,因为理解多数程序需要从一开始就掌握所有概念。我始终不知该如何安排教学顺序。但另一方面,当你真正理解所有基础组件的运作机制时,它们往往会自然衔接起来。换言之,存在某个临界点——当这些相互交织的元素从障碍转化为极具价值的助力时,一切便豁然开朗。——Jana Dönszelmann

愿景

我欣赏语言的强劲愿景。例如Uiua拥有鲜明的目标:如何彻底消除语言中的局部命名变量?Zig同样具备明确愿景:提供简洁直观的语言特性,支持便捷的交叉编译,成为C语言的无缝替代方案。

请注意,你不必认同某种语言的愿景,但必须承认它确实存在愿景。我预计多数人会觉得用Uiua编程体验糟糕——这很正常,因为它本就不是面向你的目标群体。

Bjarne Strousup有句名言:“在C++内部,存在一种更精简、更纯粹的语言在挣扎着破茧而出。” Rust内部同样存在着这样一种更精简、更纯粹的语言:它拥有清晰的愿景、目标与聚焦点,其特性之间具有内在一致性。本文正是关于这种语言的探讨。


学习Rust需要同时掌握多重技能

元素周期表

Rust 很难学。不是因为不努力–很多很多人花了很多年的时间来改进诊断、文档和 API,而是因为它很复杂。当人们第一次学习 Rust 语言时,他们正在学习许多不同的交错概念:

  • 第一类函数
  • 枚举
  • 模式匹配
  • 泛型
  • Trait
  • 引用
  • borrow 检查器
  • Send/Sync
  • 迭代器

这些概念相互交织。由于它们彼此交互影响,且各自的设计都会影响其他概念,因此很难逐个学习。此外,标准库对所有这些概念都进行了深度应用。

让我们看一个实现非平凡功能的Rust程序:1

#!/usr/bin/env -S cargo -Zscript
---
package.edition = "2024"
[dependencies]
notify = "=8.2.0"
---
use std::path::Path;
use notify::{RecursiveMode, Watcher};
fn main() -> Result<(), notify::Error> {
    let paths = ["pages", "templates", "static"];
    let mut watcher = notify::recommended_watcher(|result: Result<notify::Event, _>| {
        if let Ok(event) = result {
            let paths: Vec<String> = event.paths
                .into_iter()
                .map(|path| path.display().to_string())
                .collect();
            println!(":{:?} {}", event.kind, paths.join(" "));
        }
    })?;
    for path in paths {
        watcher.watch(Path::new(path), RecursiveMode::Recursive)?;
    }
    loop { std::thread::park(); } // sleep forever
}

我尽力简化了这个程序:仅使用最基础的迭代器组合器,完全不涉及std::mpsc,未使用异步操作,也未进行复杂的错误处理。

即便如此,该程序仍包含大量交错概念。我将忽略模块系统和宏,它们与语言其他部分基本无关。理解此程序需知:

  • recommended_watchermap 以函数作为参数。在本程序中,该函数通过内联构造为匿名函数(闭包)。
  • 错误处理采用名为Result的机制,而非异常或错误码。虽然我使用了fn main() -> Result?语法,但即使不使用这些,你仍需理解Result机制——因为Rust要求必须先检查错误条件才能访问内部值。
  • Result接受泛型错误类型,本例中为notify::Error
  • Result 是数据封装枚举类型,可取值为 Ok 或 Err,通过模式匹配可判断具体变体。
  • 迭代器可通过 for 循环或 into_iter() 遍历。2 for 循环是立即求值的,而 into_iter 是延迟求值的。iterinto_iter 的所有权语义不同。
  • path.display() 返回的结构体会从路径中借用引用。将其传递给其他线程(例如通过通道)会失败,因为传递给 recommended_watcher 的闭包执行完毕后,event.paths 将超出作用域。你需要将其转换为拥有值,或直接传递整个 event.paths
    • 值得一提的是,此类设计会促使开发者将工作拆分为“大”块而非“小”块,我认为这通常有利于CPU密集型程序的性能优化,当然具体效果仍需视情况而定。
  • recommended_watcher 仅接受 Send + 'static 类型的函数。对程序的微小改动(如将当前路径传递给闭包)会引发所有权相关的编译错误。解决此问题需要掌握 move 关键字、理解闭包默认借用参数的特性,以及明确 'static 的含义。
    • 若您使用的是常被推荐给初学者的 Rc<RefCell<String>>3,则需彻底重写程序(改用 Arc/Mutex 或外部可变性)。例如,若想避免输出交错而改用主线程而非工作线程打印变更,你不能简单地将内容追加到all_changes集合末尾,必须使用Arc<Mutex<Vec<Path>>>实现线程间通信。

对于仅20行的程序而言,这涉及大量概念。对比之下,以下是等效的JavaScript程序:

const fs = require('fs');
const paths = ['pages', 'templates', 'static'];
for (let path of paths) {
  fs.watch(path, (eventType, filename) => {
    if (filename !== null) {
      console.log(`${eventType} ${filename}`)
    }
  });
}
await (new Promise(() => {})); // sleep forever

实现这个 JS 程序只需理解:

  • 一等函数
  • 可空性
  • 嗯,大概就这些了。

这里我稍微作弊了——notify 返回路径列表而 node:fs/watch 不会。但只是小作弊。

我的观点并非 JS 是更简单的语言;这点尚有争议。我想说的是:在 JS 中无需理解整门语言就能完成任务。而在 Rust 中,若不掌握核心机制就难以实现非平凡功能。

Rust 核心设计刻意交织

前文似乎暗示我认为这些概念很糟糕。并非如此,恰恰相反。正因这些语言特性协同设计,它们能完美互动:

  • 没有模式匹配的枚举类型极其难以使用,而没有枚举的模式匹配则具有非常奇怪的语义
  • 没有泛型(或鸭子类型——我认为这本质是类型擦除的泛型)就无法实现ResultIterator
  • Send/Sync 以及 println 的预设条件,若无 Traits 便无法实现——这在其他语言中也常见,例如在 Clojure 中打印函数会显示类似 #object[clojure.core$map 0x2e7de98a “clojure.core$map@2e7de98a”] 的内容。在 Rust 中,除非显式启用 Debug 模式,否则会报编译错误。
  • Send/Sync 仅能通过闭包的捕获分析来强制执行。Java虽在多数语言标准中堪称极致追求线程安全,却无法在编译时验证此特性,因此不得不显式记录同步问题

这些交互关系远超本文能详述的范围,而正是它们共同铸就了 Rust 的独特本质。

Rust 还拥有其他卓越的语言特性——例如内联汇编语法堪称艺术杰作,向Amanieu致敬。但这些特性并未像标准库那样深度交织,也无法同样改变人们编写代码的思维方式。

精简版Rust

without.boats 在2019年发表过一篇题为“精简版Rust笔记”的文章(并后续重新审视了该观点)。某种意义上,那个精简版Rust正是我2018年初学时倾心的语言形态。如今Rust在诸多方面已大幅扩展,精简版不过是怀旧滤镜下的美好回忆。但我认为它值得研究,作为正交特性如何在统一设计下完美组合的典范。

若喜欢本文,不妨阅读matklad的《两个优美的Rust程序》(https://matklad.github.io/2020/07/15/two-beautiful-programs.html)。


  1. 本程序刻意采用文件监视器,因为在 Linux 系统上无法高效实现异步文件 I/O(同时近期为 flower 编写过文件监视器,相关知识仍记忆犹新)。Tokio 自身仅使用线程池配合通道来通知未来操作。本文不深入探讨异步机制,仅演示Send/Sync边界与回调机制。
  2. 严格来说,forinto_iter() 的语法糖,但大多数 Rust 程序无需了解这个细节。
  3. 顺带一提,这绝非良策——即便是资深Rust程序员也常难以透彻理解内部可变性;参见dtolnay的这篇博文,其中阐述了引用类型中可变性与唯一性的差异。更合理的建议是采用外部可变的拥有类型并频繁克隆。

本文文字及图片出自 the core of rust

共有 170 条讨论

  1. 具有讽刺意味的是,这个 “简单 ”的 JS 程序有一个错误。fs.watch 的文档非常明确地指出,回调中的文件名可能为空,因此需要进行检查。在 Rust 中,这个事实会被编码到类型系统中,程序员会被迫处理它,但在 JS 中,编写糟糕的代码会更容易。

    https://nodejs.org/api/fs.html#filename-argument

    1. Typescript 会要求你在使用前检查是否为空,我认为这是一个很好的例子,说明了 TS 与 JS 相比,往往是一个相当温和的升级。或者,至少更接近 Rust 的正确性,而不需要所有繁琐的东西。

      1. 但这样你就不能手动管理内存了。没有 GC、安全、简单:任选其二

        1. 你在 Rust 中手动管理内存吗?因为我只是写 Vec::new 或 vec![],就像我在 Java 中写 new ArrayList 一样。

          1. 你可以通过所有权规则和生命周期手动管理内存。

            你可以用 Vec::new 为一个 Vec 分配内存。丢弃它则会去分配。何时何地丢弃很重要。传递所有权也很重要。这些都是内存管理的一部分。

            如果需要将某些东西放在堆上,可以使用 Box。如果需要共享内存,可以使用 Arc 或 Rc。

            Rust 很好地隐式处理了这些常见情况,这样你就不会觉得是在手动管理内存,而是在每一步都对内存管理做出决定。

            1. 此外,Java 和其他 GC 语言在某种意义上也有手动内存管理,不管我们多么喜欢假装没有。

              我们很容易掉进这样一个陷阱:你的 Banana 类变成了 GorillaHoldingTheBananaAndTheEntireJungle 类(借用 Joe Armstrong 的一句话),没有任何东西被释放,因为所有东西总是被其他东西引用。

              更不用说避免长时间 GC 停顿等黑科技了。

              我想,在 rust 中也可以做到这一点。最明显的区别是,在 rust 中这些东西都是显式的,而不是隐式的。要在 rust 中做到这一点,就必须使用 “static ”等。当然,另一个区别是编译时与运行时。

              1. > 最明显的区别是,在rust中,这些东西是显式的而不是隐式的。要在 rust 中实现这一点,你必须使用 “static ”等。

                你可以使用 “static”,也可以通过 Rc/Arc 和锁定将对象的(部分)所有权转移到对象本身,导致底层计数器永远不会返回 0。

              2. > 借用乔-阿姆斯特朗(Joe Armstrong)的一句话:"你的香蕉类(Banana class)很容易掉进一个陷阱,变成了一个'持有香蕉的大猩猩'类(GorillaHoldingTheBananaAndTheEntireJungle class)。

                你能详细解释一下吗?我很难想象这样一种情况:我有一只正在使用的大猩猩,但让它拿着的香蕉和它所在的丛林继续活着是件坏事。

                1. 这个笑话的意思是,你正在使用香蕉,但实际上你并不想要这只大猩猩,更不想要整片丛林。例如,你可能有一个对象,它代表了你正在使用的数据库中的单行,但它却保持着一个大的结果集、一个连接句柄和一个事务的活力。同样的情况如果只发生在内存数据结构中(例如,你计算了一些大的树结构来计算你需要的结果),情况就没那么糟糕,但它仍然会对内存使用造成很大影响。

              3. 关于显式,为什么是 pub/pub(crate) 而不是 pub(extern)/pub?

                1. pub(crate)是后来才出现的。我想这是一个版本可以改变的地方,但我不记得有人提出过这样的建议。

            2. 再延伸一下,内存一般只有一个所有者。当它离开作用域时,就会被释放。drop()函数看似类似于C/C++中的free(),实际上只是一个空函数,它的唯一目的就是获取所有权并使其退出作用域,从而立即释放内存([2])。

              > 这个函数并不神奇;它的字面定义是: pub fn drop<T>(_x: T) {}

              这通常比 GC 语言更有确定性(没有随机停顿),但对于高度嵌套的数据结构来说可能效率较低。由于不遵守正常的所有权规则,如果不使用 “不安全的 rust”,它还会使链表变得不可能。

              [1]: https://doc.rust-lang.org/rust-by-example/scope/raii.html [2]: https://doc.rust-lang.org/std/mem/fn.drop.html

              1. 指向任意内存的链接列表,是的。从由碰撞分配器管理的连续内存块链接列表:与任何语言一样简单,不需要不安全。

                诚然,用这种语言制作链表并不容易。

          2. 我有时会这样描述:在 Rust 中,内存基本上是在编译时管理的,而(跟踪)GC 是在运行时管理的,在 C 语言中则是由程序员手动完成的。这是一种简化,但 Rust 与 C 语言的相似之处在于,内存管理不需要运行时成本,而 Rust 与 Java 的相似之处在于,在特定的 API 之外,你可以很好地保证某些内存问题不会发生,因为这些问题都是由语言自动处理的。

            1. 我曾半信半疑地试图实现 “静态内存管理”(Rust/C++ RAII)与 “动态内存管理”(Rust/C++ 中的 GC 或 Rc/Arc)的对比,但人们普遍不喜欢这样。不过,我认为这是一个很好的框架。

          3. 是的,通过决定何时、何地以及如何传递。你知道它何时被创建和销毁,因为你编写的代码要么直接这样做,要么遵循约定(在超出范围时销毁,或使用 arc – 这是程序员的决定)。

        2. 错误的选项集,人们只是没有使用具有自动资源管理功能的正确系统语言,而这些语言也为手动内存管理提供了逃生门。

          Cedar 就是其中最早的一种。

          其思想逐渐发展为 Modula-2+、Modula-3、Oberon、Oberon-2、Component Pascal、Active Oberon、D、C#(在 Midori 的经验最终融入该语言之后)、Swift、Chapel,以及研究人员现在试图将生产力与现代类型系统相结合的下一代语言。

        3. 对于包括我在内的很多人来说,手动内存管理是一个巨大的缺点,而不是优点。

          1. 是的,但在某些用例中,这是一个硬性要求。

            1. 是的!

              但文件监视器程序或 http 服务,或真正在现代 Linux 服务器上运行的任何程序,都不属于这些用例!

        4. 没错,两种不同的工具各有利弊。

    2. 还有其他错误:

      paths` 中的路径

      应该是

      for (const path of paths)`

      JS 会立即因为缺少括号而出错,但是 `in` 与 `of` 迭代的是索引,而不是值,而且这些索引不幸被转换成了字符串(因为 `for-in` 是对象字段名迭代)。因此,当(字符串化的)索引被用作 `fs.watch()` 的第一个参数时,即使是 TypeScript 也无法捕捉到它。

    3. 没错,但是循环的语法也是不正确的,运行它就会发现;所以更好的解释可能是,作者并没有花太多时间考虑 JavaScript 代码,因为这对他们的观点并不重要。

      1. > 这对他们的观点并不重要。

        这主要是在撒谎,因为他们在 rust 中抱怨的概念至少有一半都在 js 代码段中。

        1. 是的,很公平。我不知道他们为什么不在 js 部分多花些精力,只是说他们没有。

          1. 但这不正是问题的一部分吗?JavaScript 代码看起来完全没问题,直到 HN 评论区发现了各种微妙的 bug,而这些 bug 有时甚至连 TS 都无法防范?

            1. JS程序有明显的语法错误,TS无法编译。上面的评论者说得没错,我只是偷懒了(谢谢你的提醒,我已经修复了JS程序)。

    4. 我是瞎了眼还是从哪里看出来的?

      > console.log(`${kind} ${filename}`)

      应该是 `eventType` (字符串)。

      1. 是的,它来自 Rust 代码(相关属性名为 “kind”),因为作者没有实际运行他们的 JS 代码段。他们已经(默默地)修复了许多人们指出的问题(包括我指出的问题),但他们仍然没有注意到这个问题。

  2. 小瑕疵

     println 只能打印实现 Trait Display 或 Debug 的内容。因此,不能直接打印路径。
    

    并非所有操作系统都存储与 UTF-8 兼容的路径。在 Rust 中,所有字符串都是 UTF-8 编码。因此,打印路径是一种有损操作(即无法安全地进行往返)。Path 暴露了一个返回实现 Display 的类型的 `display` 方法。这是 Rust 在其类型系统中编码的一个事实,而在 JavaScript(和 TypeScript)中,并不能真正地说明 "字符串内部是 UTF-16 的,你可能需要调用 TextEncoder / TextDecoder 来安全地处理非 Unicode 的路径"。如果您从发送 Shift_JIS 文本的服务器获取数据并调用 `response.text()` ,您在运行时会得到一个空字符串(根据我的经验)。如果你没有处理文本编码问题的经验,我可以看到这将成为一个漫长的调试过程。

    正如其他人指出的,JavaScript 程序中有一个 Rust 程序中没有的 bug,还有一个语法错误(也许应该使用 for..of,而不是 for..in)。这个示例肯定使用了比 "一级函数 "更多的概念。你仍然必须理解迭代器,就像在 Rust 中一样,而且它使用的是 CommonJS 而不是 ES 模块,但我想说的是。async/await 和 Promises 的使用是另一个需要教授的概念,而顶级 await 的使用则是一些运行时(如 Node)直到最近才支持的特殊功能。一些主流的 JS 引擎(例如 React Native 中使用的 Hermes)的最新版本仍然不支持该功能。

    1. 对于你上面提到的路径,正是这样的东西让我不断回到 Rust。这只是其中一个例子,其他语言中也有很多陷阱和问题,但单个问题发生的可能性并不大。

      但所有的?在特定程序的生命周期内?总之,这些问题最终会导致大量随机 bug 出现在异常日志中,而这些异常日志需要被追踪。而且这种情况永远不会结束。有无数你从未想到过的边缘情况没有被完全处理,其中一些会不断被触发。

      这……在 Rust 中不会发生。类型系统捕获(和/或完全排除)了大量这样的情况。我曾多次在 Rust 中编写_完成_软件,一旦发布,只是偶尔需要添加一些功能,但典型的错误消除跑步机并不存在。

      这并不是说从来没有 bug。你可以用任何语言表达错误的逻辑。但是,被断然排除的愚蠢阻抗匹配的数量之多,让我在编写 Rust 程序后的操作和维护体验与使用其他语言时大相径庭。

      1. 我花了几个月的时间来解决 C++ 代码库中的所有问题,以弥补几十年来对路径、字符串和本地的假设,但这些假设在现实中都是站不住脚的。

        事后看来,Rust 的 “难度 ”更胜一筹。

    2. 我发现,很多开发人员并不真正理解 JS/TS 中的 thenables/Promises 和 async-await。当你看到以下内容时,就会发现这一点…

       var fn = async (param) => new Promise((res, rej) => {
      ...
       fooLibraryCall(param).then(res).catch(rej);
       });
      

      我真的看到过类似的情况……开发人员看到回调语法的封装,然后将同样的封装应用于 thenables,并在标记为异步的函数中使用手动 Promises。看到这种情况,我的心都在滴血,而且我在很多地方都看到过。

      当然,还要处理模块导入和 async import() 方法,以及它们是如何运行或被转置或拆分的,等等。

      1. 有时,即使在异步上下文中,您仍然需要使用承诺。想象一下,一个异步函数需要等待 Promise.all([..])。在这种情况下,then()可以作为一个有用的工具,从异步调用中生成新的承诺。

        1. 当然。我指的是有一个异步函数,但让它创建一个 "新的承诺",而这个 "新的承诺 "本身只是在调用一个已经返回承诺的方法……它创建了额外的闭包和开销,而这些是完全没有必要的。

          有时,如果我想在流程外操作一些东西,我会添加一个(`.catch(() => null)`或类似的东西来用(`.then`)操作特定的调用。比如一个方便的方法来规范 api 获取调用等。这只是有用的功能,而不是毫无益处或理由的字面包装。

  3. Bjarne 的这段话基本上是在推销一种使 C++ 越来越糟的反复出现的理由。我想,假设 Bjarne 第一次是真诚的也不无道理,但那是很久以前的事了。事情是这样的

    1. “在 C++ 内部,有一种更小巧、更简洁的语言正在努力脱颖而出”

    2. 然而,仅仅通过对语言进行子集化来获得更小的语言并不能使语言更简洁。相反,我们必须_先_制造一种超集语言,增加一些特性,然后我们可以_子集_这种新语言,以达到我们的更小但更干净的 C++

    3. 第一步,超集将在 C++ N+1 中出现。规划 “超集的子集 ”需要等到我们完成这项工作之后。

    4. C++ N+1 是一个更加笨重的庞然大物。反反复复。

    我不明白为什么见多了这种情况的人还会坚持下去。你不会在第二步之后得到 “更小更简洁 ”的语言,根本就没有第二步,它只会是第一步,然后又是第一步,然后又是第一步,直到永远。

    1. 让我想起了经典的 https://xkcd.com/927/,虽然与 Bjarne 的引用不完全相同,但很相似。

      我对 C++ 也相当熟悉,但这实在是太不协调了,每一个标准都比上一个标准复杂得多,虽然有一些好的变化,但它们并不一定能很好地与之前的版本相匹配,简直就是一团糟。

      Rust 是来自 c++11/14/17/20 的一股新鲜空气,但如果你不了解它的全部内容,它仍然是一个庞然大物。

  4. 有没有人一看到这个东西(自执行的 rust 脚本)就完全走神了?当我发现 Go 也能做到这一点时,我的思维就像爆炸了一样。这绝对是个不错的功能,可以预见它将得到大量的基本使用。我见过几个项目用 rust 来控制构建和测试流水线,在这种情况下,这可能是一个不错的选择。

    不过,除了简单的 bash 之外,我主要还是使用 Deno + TS 来满足我的 shell 脚本需求。主要是因为 JS 是我最熟悉的语言(28 年),对我来说,C#(24 年)与之相差无几。我也是 Node 的早期使用者。我还认为,与 Node、Python 或其他语言/环境相比,处理共享/集中式软件包对 Deno 来说是一个更容易的选择。这里的货物前置似乎也是类似的工作方式。

    1. 作为将货物脚本集成到货物中的设计和实施者(过去曾有过许多第三方实施),我很高兴看到它被广泛使用,也很惊讶和高兴看到它被如此提及!

      文档见 https://doc.rust-lang.org/nightly/cargo/reference/unstable.h

      是的,在定义它应该是什么样子、它应该如何与语言交互、初始版本的正确范围等方面,我们已经走过了漫长的道路。

      目前,我正在进行我希望的收尾工作,包括更新样式指南和 Rust 参考资料。剩下的主要工作是 rustfmt 和 rust-analyzer 的细节。除此之外,我还需要修复 rustc 中的错误,并改进 Cargo 中的错误报告。

      就我自己而言,我几乎每天都在使用货物脚本,因为每当我为一个正在交互的问题创建重现案例时,我都会写一个脚本。

    2. 补充一下,我的意思是 “走神 ”了……我开始搜索货物中的“-Zscript ”功能,很明显,该功能自 2023 年以来一直在开发中,有一个开放的问题已接近完成。此外,我还查看了 ZomboDB 的 repo,在那里我看到 Rust 被用于构建流水线,但我并不完全了解它的上下文。

      更不用说货物前端处理对于此类脚本的可移植性有多大帮助了……只需共享一个文件,无需额外的 “安装/启动 ”步骤来引入依赖关系(例如 Python 或 Node.js)。

    3. > 说到这里,除了简单的 bash 之外,我主要使用 Deno + TS 来满足我的 shell 脚本需求。最主要的原因是,JS 是我最熟悉的语言(28 年),对我来说接近的是 C#(24 年)。

      我深表同情,但在 2025 年,这是选择一个系统的可怕理由。

      在过去的二十年里,很多东西都发生了变化,而且大多是好的变化,好得多。

      1. 我知道它,了解它,它对我来说易于使用,现代化,无需复杂的初始化/设置即可运行,支持大量模块,包括可插入太阳底下几乎所有类型的后端,并可移植为单个文件/脚本。

        这些都不是选择某款产品的理由吗?

        我的意思是,当然,我可以使用 Python 或 Go,或者用其他语言编写小程序,而不是完全使用脚本…… 但是,Deno+TS 实际上是一个相当不错的、有能力编写 shell 脚本的选择。我不是在抨击 Python 或 Go……但考虑到我知道其中一个选项比其他选项更好,这绝对足以让我在选择时作出让步。

        除非你认为我很久以前就开始使用 JS 意味着我不了解现代/当前的惯例?

        1. 你的选择是现代的。我不知道这个人到底在争论什么,但新的 C# 已经非常现代了,.ts 也非常现代。2025 年的 deno 也是如此。

          事实上,在 2025 年,我测试过同样的设置以获得更好的脚本:deno+ts。

          我对 .ts 非常熟悉,但我对 deno 的模式不太了解,尤其是 Python 收到 uv 时。

          但这两个选项都很好。如果说在 2025 年,您应该采用不同的、更现代的方法,而不详细说明,那就没有任何意义了。

          PS C#/.NET 刚刚引入了 “运行”,一旦它能像本地集成一样快速,使用 deno+ts 就没什么意义了。

          1. 是的,我在 v10 注释中看到了 dotnet 运行选项……目前正在使用预览版 5 进行一个项目,需要更新最新版本,但锁定环境意味着要让帮助台参与进来,让每个人在整个团队中进行更新,这很具有讽刺意味,因为我在我们的应用程序服务器上确实拥有管理员权限,只是没有分配给我的笔记本电脑。

            1. 是啊,大多数时候,我们在服务器上的权限比 IT 分配给我们笔记本电脑的权限还要多。我现在不在这种环境中了,但在银行工作时,情况就是这样。

              我希望 dotnet 运行成功,这意味着我可以更多地使用 C#。我们现在有 Go(最大的一部分)、C#、Python、JS/TS 和大量 .sh 等混合语言。还有一些 powershell(为了寻找更好的 shell 脚本)。很少使用 Rust。

              你用 C# 开发图形用户界面应用程序吗?如果有,你使用什么?MAUI 是否已经起飞,还是还没有被广泛采用?承诺是好的。我仍然怀念 Delphi 和拖拽组件。现在有 Qt 了,但那是另一回事。

              1. 如前所述,如果我需要的东西多于(git/msys)bash 可以轻松完成的,我大多会使用 Deno+TS,我不喜欢 Powershell,尽管我在必要/可用的情况下使用过它。

                我不常做桌面 gui 应用程序,但我考虑过 Tauri 封装的基于 Web 的应用程序,主要是因为它的工具看起来很不错,还有 Leptos 和其他一些可与之配合/类似的选项。如果使用 C#,我更倾向于使用 Uno、Avalonia、Etp Fprms 或嵌入 Blazor。对我来说,MAUI 是一种 DoA,只是因为缺乏 Linux 目标。我知道它只占桌面市场的 5-6%,但在我看来,它仍然值得开发者使用。

                我知道基于网络的桌面应用程序比本地应用程序的开销要高一些,但它们可以通过良好的皮肤(CSS)和可访问性功能运行得相对较好,这些功能往往比很多跨平台用户界面工具运行得更好,或者至少更稳定。我发现 MUI + React 在用户界面方面是一个相当不错的组合,我还在托管(后台)代码中使用过类似 Redux 的异步状态管理,在我的实验中运行得相当流畅。在状态与事件之间,你需要考虑得更多一些,但这是一种坚如磐石的体验。

      2. 你对 2025 中的 shell 脚本有什么建议?

      1. 我大约一年前看到过一个例子,但找不到具体的文章……不过大致是这样的…

            #!/usr/bin/env -S go run
        

        我不确定 go 是否/何时会忽略第一行的 shebang……除此之外,我还看到了一些文章,它们的格式略有不同,也是这样做的……但 google 的结果感觉不对/过时了。

        搜索:go shebang 脚本

        Shebang 是以 "#!"开头的文本/脚本文件的术语,其中包含要运行的可执行文件和参数。

    4. 这只是 Unix 的基本功能。

      任何以 #!/some/path 开头的文件都意味着 shell 会调用该命令,并将文件内容传入 stdin。

      1. 我知道……我想说的是,5-8 年前我刚开始阅读/学习 Rust 时,它还没有这个功能。很多语言直到最近才有了脚本功能。C# 甚至在 .Net 10 中就有了,不过我不确定在这种情况下的依赖关系处理。

        我喜欢并正在评论 rust 拥有货物前置事项(cargo frontmatter)而非单独的货物文件,Deno 也是如此,它可以直接引用 repo/url,而不需要单独的模块文件/文件夹(如 node package.json/node_modules),甚至不需要 python。你可以直接引用你需要的模块。

        1. 我还记得大约 10 年前,当我第一次使用 F# 交互式语言时(当时 C# 还没有 REPL),我在 F# 交互式语言中看到了这个功能。我认为它作为一种易于编写、但没有 Python 所有令人头疼的依赖关系的静态类型 Python 替代语言,对于快速编写脚本非常有用。因为与 C#/Java 等需要大量类文件、模板等的语言相比,F# 更像是一种脚本语言,这使它更适合于编写脚本。例如,调入类型提供器、解析 JSON、调用 API、创建快速 CSV 等。这对快速脚本、数据处理或快速原型都很有用–你只需剪切并粘贴单个脚本,然后将其发送给团队中的某个人,它就会运行依赖关系,而不会像简单粘贴到文本文件中那样产生类似的 VENV 问题。安装 .NET SDK 就可以了–它可以运行。在我当时所在的团队中,这能快速提高工作效率–在快速脚本中看到功能的实现,在 REPL 会话中看到数据的可操作性,然后将其推广到实际项目中,这是一个很棒的工作流程。

          我认为这些功能后来已经扩展到 C# 交互式语言(可能是受到 F# 孵化器的启发),因为该语言已经简化了语法,使单个文件应用程序变得更容易。从你的评论来看,Rust 似乎也在采用类似的方法。

        2. 很多第三方 Python 包管理器客户端(最著名的是 uv)现在都支持这种方式。它们都使用相同的前端格式(PEP 723)。不幸的是,pip(通常与 Python 一起发布的第一方软件包管理器客户端)还不支持:https://github.com/pypa/pip/issues/12891

          1. 有意思… 我并没有参与太多的 Python 开发,但知道有这样的选择还是很酷的。我主要是根据需要运行现有项目。

        3. 准确地说,早在 1.0 之前,Rust 文件就已经支持 Shebangs 了,只是当时还没有真正的内置工具来指向 Shebang。相关 RFC 中的新内容实际上是关于 Cargo 的,而不是 Rust;“Cargo frontmatter ”语法并不需要添加到 Rust 中,只需在将文件交给 rustc 之前由 Cargo 将其剥离即可。

        4. 这仍然是一个夜间功能,我刚刚在尝试运行这个示例时发现了这一点。

      2. 目前大多数语言都不支持通过软件包管理器指定依赖关系。例如,大多数 C 和 C++ 工具链都无法在一条命令中编译和运行单个源文件。

        1. 没错,就这一点而言,Python 在实践中的实用性和可移植性就远远超过了许多其他语言。一个 python 文件可以有一个 Shebang,但它周围还需要一堆其他文件,用于模块引用等。更不用说初始化/设置等。

          依赖关系在文件中被引用,并在运行时自动处理(首次或重复),这是一个非常不错的功能……这也是我开始使用 Deno 而不是其他选项的原因。

          1. 我认为第三方工具并不能真正解决问题,因为其价值主要在于能够在电子邮件或聊天信息等中内嵌脚本,而如果收件人必须下载并安装一个单独的工具,这一点就会被削弱。(uv 有一半的功劳,因为它的采用率正在迅速上升,并有机会成为事实上的标准,但只有当 pip 支持这一点时,我才会把全部功劳归于 Python)。不过,它们对探索设计空间很有帮助。

  5. 那么,Rust “正在努力摆脱的更小更简洁的语言 ”到底是什么呢?如果我没理解错的话,该语言仍然有引用、生命周期、Traits、枚举等,因为所有这些特性都是连在一起的;你不能只去掉一个特性,却指望语言的其他部分仍然能正常工作。一旦你赋予了所有这些特性,你的语言并不比 Rust 更小或更简洁;你的语言几乎_就是_Rust。

    最后一节对这种 “更小更简洁 ”的语言可能是什么给出了两种不同的提示,但对我来说,这两种提示都不完全合理。

    首先是 withoutboats 的 “关于更小的 Rust 的笔记”。这篇文章,尤其是它的续篇,非常棒,我非常喜欢,但标题却相当容易误导读者。boats 在这些文章中描绘的语言与 Rust 的设计目标有很大不同;特别是,它放弃了低级程序员对运行时行为的控制要求,因此不适合 Rust 的许多使用情况。相反,我们的想法是探索 Rust 能为设计一种更符合 “主流 ”要求的语言提供哪些借鉴(即一种能负担得起跟踪垃圾收集器等功能的语言,并希望避免 Rust 与其他流行语言相比的缺点,如编译时间慢和语法盐分重)。这种语言并不是在 “努力摆脱” Rust,而是 Rust 并不想成为这种语言。

    其次,"可以这么说,那个更小的 Rust 是我在 2018 年第一次学习它时爱上的语言。今天的 Rust _在很多方面都大了很多,而那个更小的 Rust 只是一种怀旧的玫瑰色记忆。" 我已经在上文解释了为什么我不认为 boats 所提出的 “更小的 Rust ”和真正的 Rust 在其历史上的任何时候都不一样(至少在最早期之后,一旦设计者发现他们的目标是 C++ 的利基市场)。在大多数基本方面,Rust 自 2018 年以来并没有太大变化,很多变化(比如新版本)都是为了让它在语法上更加灵活,并增加可编译的感性程序的比例。也就是说,有两个很大的例外:async 和 const,它们在 2018 年时更加简约,后来扩展成了复杂的元功能,其中有许多互锁的部分,而这些部分并不是语言最初核心的一部分。如果你是想说 Rust 在 async 和 const 出现之前更小更简洁,那你可以这么说!但帖子并没有这么说,这就让我们不得不去揣摩其中的含义了。

    1. > 那么,Rust “正在努力摆脱的更小更干净的语言 ”到底是什么?如果我对帖子的理解没错的话,这种语言仍然有引用、生命周期、Trait、枚举等,因为所有这些特性都是连在一起的;你不能只去掉一个特性就指望语言的其他部分仍然可以工作。一旦赋予了所有这些特性,你的语言并不比 Rust 更小或更简洁;你的语言几乎就是 Rust。

      我认为,在保持核心概念的前提下,你实际上可以做出比 Rust 更简单的语言。这种语言的变体将删除

      – 复制 Trait

      – 借用

      – deref 强制

      – 在循环中自动插入 “into/iter/”。

      – 在作用域结束时自动调用 drop(相反,你必须自己调用它,否则编译器会出错)

      – Trait 边界默认有一个自动的(:Sized)边界。

      – 终生省略

      – 没有 “匹配人体工程学”

      – 可能还有其他一些 “魔法”,初学者可能很难掌握,但这就是我脑子里想到的。

      这种语言的概念更少,“魔法 ”也少得多,因此会简单得多,但日常使用起来也会繁琐得多,因为上面提到的都是巧妙的变通方法,目的是让普通任务尽可能简单明了,我不认为有人会喜欢使用这种语言,而不喜欢使用 Rust 本身。不过,在向学生介绍 Rust 时,它或许可以作为一种教学工具。

      1. 实际上,我并不相信你可以不使用 Copy。它既是语言的核心,也是像共享引用这样的东西正常工作所必需的。复制可以用一个关键字来代替,而不是隐式的,但这与从 Rust 中完全删除这个概念是不同的。

        其余的,你当然可以不用(借用不能被模拟,但我不认为这是编写真实代码的严格必要条件)。我还想加上可捕获异常和使用 Trait 代替显式模块,因为我认为这些东西极大地复杂了语言语义,而且几乎可以肯定,它们对于实现 Rust 的语言目标并非绝对必要。

        1. 你已经可以编写一个从不依赖 Copy 的 Rust 程序了,只要在需要复制某些东西时明确调用 .clone()。只是这样做太疯狂了。

          1. clone()通常是通过Copy来实现的,但是真正的问题是引用的使用,我不知道如何在没有Copy的情况下解决这个问题。每当你调用一个共享引用的函数(包括 `clone` 使用的引用)时,你都在隐式地复制引用。正是因为有了`Copy`,它才会起作用。如果没有它,我想你就需要不安全的代码或其他东西来调用共享方法并保留对原始引用的访问,这几乎不再是 Rust 了,因为绝大多数方法都无法在安全子集中正确表达。也许你可以使用借用风格的技巧来解决这个问题,但正如你所说,Rust 的 “核心 ”不应该有借用功能。或者,你也可以在不安全代码中对引用实现克隆,然后在每次需要复制引用时显式地获取引用…… 还有一种线性类型技巧,你可以通过模式匹配来复制一个值,方法是对其进行破坏性匹配,显式枚举所有可能性,然后生成一个新的产品,每个可能性都列出两次,但这无法实现对引用的复制。

            无论如何,我认为 “复制 ”对于 Rust 来说是非常重要的。在我看来,即使你能以某种方式把它的所有实例都变成语法糖(我认为这是不正确的,就像`Cell`例子所显示的那样),你所使用的表面语言也会与 Rust 有很大的不同,以至于它不再是真正的 Rust 了。

          2. 不是的,有些对象(尤其是 Cell<>)需要 Copy,因为 clone(&self) 获取了一个引用,可以对 Cell 进行任意操作(包括通过 Cell::set() 覆盖其引用指向的数据)。

            1. 我想你需要一个类似 Copy 的自动 Trait 来约束 Cell<T> 的 impl Clone。不过它不必像 Copy 那样神奇。

      2. 与没有这些特性相比,除了编译器工程师之外,这些特性真的会给其他人带来困难吗?我没有亲眼看到新手在使用这些功能时磕磕绊绊;实际上,这些功能设计得非常好,可以淡出人们的视线,只需正常工作即可(也就是说,你不会注意到它们的存在,但如果它们消失了,你肯定会注意到)。是的,语言设计中的极简主义是有道理的,但没有这些功能的 Rust 仍然算不上极简主义,因此放弃这些功能似乎会带来极简主义的大部分代价,而没有好处。

        1. > 与没有这些概念相比,除了编译器工程师之外,这些概念真的会给其他人带来困难吗?我没有亲眼见过,例如,新手在使用它们时磕磕绊绊;

          列表中的几乎所有特性都是我在学习 Rust 时遇到过的困难(Copy Trait 和 lifetime elision),或者我看到过初学者在理解这些特性时遇到的困难(包括新员工,或者我给 Rust 讲课时的学生)。

      3. 注意,Rust 中的 RAII 并不像在每个词法作用域结束时调用 drop() 那么简单,因为有 drop 标志。

      1. 在我看来,它并不比 Rust 简单多少;它拥有上面列出的大多数特性。我知道很多人不喜欢 RAII(Rust 有而 Austral 没有),因为他们希望每个函数调用在调用位置都是可见的,但用线性类型来取代 RAII,无论它们有什么其他优点,都不会让语言变得更容易学习;隐式析构体调用作为一个概念并不难理解,只要你理解了一个有明确生命周期限制的值的概念,而对抗线性检查器似乎比对抗借用检查器(Austral 仍然有)对新用户来说可能是更大的障碍。

    2. 是的,这就是我的观点,在 async 和 const 之前,Rust 更小更简洁。我之所以说得这么间接,是因为我的很多好朋友都在研究这些特性,我不知道该如何措辞,幸好 Matklad 在另一个网站上说得很好: 2015 年的 rust 是一种更完整、更连贯的语言,但 Rust 的愿景并不是完美连贯,而是成为一种即使不漂亮也很有用的工业语言。

      https://lobste.rs/c/b8kevh

      1. 是的,在这种情况下,我认为与 boats 作品的链接有点模糊了重点。

        我对 matklad 的观点有一点不同的理解:我不认为 Rust 在支持哪些广泛的功能方面妥协了很多,但它有几次选择了发布一些并不完美的东西,因为要做到有用,只需要花费一定的时间来迭代设计。

        所以 Rust 1.0 在发布时并没有包含 async,尽管众所周知 Rust 的一些核心用例需要 async,因为它离准备就绪还太远,而且永远等待也不是办法。一旦做出了这个决定,就会对 async 的工作方式产生影响;尤其是,要真正做好 async 就需要线性类型,但 Rust 1.0 发布时并没有意识到这一点,而且这也不是一个向后兼容的变更,所以到 2018 年,async 就被取消了。当时的选择是:以牺牲一些优雅性为代价,以与现有设计决策兼容的方式实现异步,或者根本不做异步。前一种选择不只是更 “工业化”,我认为它的一致性更好,因为同时等待多个事件是基础软件语言必须具备的核心功能,而 2018 年人们使用的基于组合器的方法与语言的其他部分一致性很差(例如,需要不必要的堆分配)。因此,这并不是对一致性的真正妥协。

        (在 async/await 首次发布时,这种情况也曾发生过,但规模较小(例如,特定的 “async ”语法取代了更通用的 coroutine 功能),原因是当年急于发布一些东西。Boat 声称,这是语言生存的问题;我不太同意这种说法。不过,虽然 async/await 在概念上不如完全通用的 coroutines 那么纯粹,但我不认为今天对 async 的任何常见抱怨都是当时为了快速发布而做出的下游决定;从那时起,似乎并没有出现过很多明显的错误)。

        (我的理解是,const 也有类似的故事,但我对那里的设计空间不太熟悉,因为人们没有像对待 async 那样详尽地记录它的历史,也许是因为它没有那么激烈的争议)。

        1. > 尤其是线性类型,但这在 Rust 1.0 发布时并没有得到重视,而且它也不是一个向后兼容的变更,所以到 2018 年它就被取消了。

          早在此之前,它就已经被取消了,因为要实现线性类型的可用性,就必须确保没有恐慌(panic)。(恐慌必须解除堆栈,这相当于自动运行 drop 实现)。这两个问题密切相关,很难单独解决。

          1. 我认为一个有趣的问题是,你可能还需要 “半线性类型”:这些类型据称是线性的,但_可以_丢弃作为解卷的后盾。

            例如,如果你在处理数据库事务,你可能想明确地说明是提交还是回滚,但在慌乱时,你可能会允许自动清理事务。

            1. 大多数 Rust ORM 和查询构建器都公开了一个事务 API,该 API 会获取一个闭包并在事务内部运行,在解卷或(大多数情况下)未明确提交时回滚。这是 Rust 中最常见的成语,用于处理需要向清理例程传递额外数据的情况。不幸的是,对于异步用例来说,这恰好是不健全的: https://tmandry.gitlab.io/blog/posts/2023-03-01-scoped-tasks

          2. 我认为,在 Rust 的版本中,捕获恐慌是不安全的,这完全是合理的,也许应该得到更多的考虑。

            1. 如果仍有可能对语言进行向后兼容的修改的话,这是解决通过线性类型解绕(unwinding-through-linear-types)问题的众多方法之一。

              1. 是的,但与大多数针对这个问题提出的解决方案不同,这个解决方案(1)在 Rust 1.0 发布之前就被认真考虑过,(2)不会对大多数人编写 Rust 程序的方式造成重大改变。

        2. 我认为我们对 “cohere ”一词使用了不同的含义,我不知道如何调和它们。我同意带有 async 的 Rust 是一种更有用的语言。我不认为 “有用 ”意味着一门语言有多连贯(我认为 bash 和 perl 就是连贯度很低的有用语言的例子)。在我看来,“连贯性 ”意味着所有功能都能紧密地结合在一起,并且在设计时就考虑到了彼此,而我认为 Rust 中的 async 和 const 并不是这种情况,这仅仅是因为它们还没有完成设计。

          1. 你关于一致性的观点与一位前 C++ 维护者的观点相似。这段视频是十年前 https://www.youtube.com/watch?v=KAWA1DuvCnQ&t=2530s 发布的,我觉得他的教训没有得到重视。它与弗雷德-布鲁克斯(Fred Brooks)提出的概念完整性(Conceptual Integrity)这一更大、更危险的概念相关。

          2. 从一种语言中移除某种特征是否会使其不那么连贯?

            1. 我认为有可能。如果去掉我在文章中提到的 “核心 ”中的任何一项,语言的连贯性就会大打折扣,即使它的规模更小;没有模式匹配的枚举就是一个简单的例子。

              我并不是说我想回到 “美好的过去”,我确实认为 Rust 的这些部分是作为一个连贯的整体设计的,就像 Uiua 是作为一个连贯的整体设计的一样。

  6. 我知道我有偏见,但 Rust 是最接近完美的编程语言了。借用检查器是不是很麻烦?是的。但有必要吗?绝对有必要。试想一下,用 C 语言编写了同样一个漏洞百出的程序,部署后却在运行时崩溃了–你还是得修复它,不是吗?错误就是错误,需要修复。不同的是,Rust 会迫使你在得到二进制程序之前就处理好它,而用 C 语言时,你可能会在凌晨三点被叫醒,试图找出出错的原因。因此,Rust 并不是更难,它只是不同而已。它需要我们转变编写安全可靠代码的思维模式。对于人类来说,改变一般都是不舒服的,而这种范式的转变正是大多数人(我希望不是)对 Rust 有这种感觉的原因。

    1. Rust 远非完美。

      * 我认为 Rust 给了编译器太多的自由来选择是否应用 Deref(以及应用的顺序)。整个 .into() 和 From Trait 允许编译器执行任意的类型转换,以使代码工作(标准库中充满了类似的 “方便 ”Traits 和函数)。所有这些都倾向于隐藏对象的类型,从而难以将函数调用映射到实现(不过一个好的集成开发环境可以在这方面提供帮助)。* 我认为隐式返回值是一个错误的特性,因为它使流程控制变得隐式,使程序员在审查代码时无法发现。我也不喜欢问号操作符,尽管语法高亮对它有很大帮助。* Rust 模块一般都太小了,所以你需要数百个依赖项才能做任何有用的事情。如果你需要确定性的构建,每个依赖都需要单独的供应商和定期更新。* 异步 Rust 现在是一团糟

      1. 我没说完美,我说的是最接近完美。关于隐式返回类型,这完全是品味问题。例如,如果函数的返回类型是 String,那么是写隐式的 “Hello”.into()还是显式的 ‘Hello’.to/_string()或 String::from(“Hello”) 完全取决于你,Rust 不会抱怨。

    2. > 借用检查器是不是很麻烦?是的。但有必要吗?

      你完全忽略了这篇文章的重点。借阅检查器本身并不是问题所在,问题在于所有东西都太多了。完美有多种定义。我们中那些非常喜欢 2018 年表面上并不完美的《尘埃落定》的人觉得这种特殊的、当前的味道并不吸引人。它可能是灵巧手中的得力工具,然而,正如生活中的一切一样,你不禁会问自己一个问题:这值得我付出努力吗?就我个人而言,如果我想在 2025 年找到更好的 C/C++,我无论如何都会选择 Zig 而不是 Rust(Postgres 的东西是个例外,pgrx 生态系统真的很特别!)。

      不过话又说回来,任何东西都比以编写 C 语言为生强。

      1. > 不过话说回来,任何事情都比以编写 C 语言为生强。

        我喜欢这样。

        我最快乐的职业编程就是 C 语言

        我想品味的多样性是件美妙的事情吧?

      2. > 我们中那些非常喜欢2018年表面上不完美的《Rust》的人,发现这种特殊的、当前的味道并不吸引人。

        我相信这篇文章中的所有内容(除了cargo -Zscript)都是2018年的Rust。

      3. Trait、泛型、所有权、RAII 在很多语言中都出现过,那么 Rust 在生命周期之外还引入了什么概念呢?

        1. 仿射类型、方差、高阶 Trait 边界、幽灵数据、MaybeUninit 以及整个宏和 proc-macro 系统都是我在学习 Rust 时发现具有挑战性的概念。

          动态安全(Dyn-safety)是另一个例子,但我之前在 Swift 中也遇到过。

  7. 我经常遇到想学习编程语言的人,问他们是否应该选择 Rust 作为自己的第一门语言。我的回答是:不: 不。

    学习第一门编程语言很难。而 Rust 只会让它变得更难,因为你要做的就是整天调试编译器的错误,甚至在程序 “完美 ”之前都无法看到它真正运行。这会让你无比沮丧,最终放弃。

    我总是告诉人们从 Python、JavaScript 或 Lua 开始。你可以为自己制作一些有趣的东西,比如游戏,并立即获得反馈,而且可以快速迭代。

    1. 这不是我的经验。我们公司有一位只使用过 python 的 ML 工程师。他想为我们的 rust 代码库做出贡献,于是我花了大约一个小时向他讲解 rust 的基础知识,并回答他提出的问题。他很快就在我们的 rust 代码库中发挥了很大的作用。

      从理论上讲,我不确定运行你的游戏会不会很有趣,因为你把一个字符串传给了一个预期是整数的函数,结果游戏崩溃了,你得盯着回溯,看看那个变量是从哪里来的,再往上查几层调用栈,然后意识到 “哦,我忘了在用户输入字符串后对其进行解析”。我宁愿得到一个编译错误,在有问题的代码下划线,然后说 “这是一个字符串,但函数期望的是一个整数”。也许整天都在调试编译器错误,但另一种选择是整周都在调试运行时错误。

    2. 我不相信。我希望看到优秀的人尝试将 Rust 作为第一语言。教我新泽西州标准 ML 的工作人员已经退休,现任项目负责人是我的一位朋友,他在如何处理整个学科的问题上与我意见相左。

      我认为,最坏的情况是,这是一门 “不成功便成仁 ”的课程,学生中的大部分人都会退学,这对学生人数来说是不利的(这会惹恼学校的财务人员,但对于一所名牌大学来说,从中期来看可能还不错);最好的情况是,学生的退学率并没有什么不同,但好学生所获得的价值要比今天的第一语言(在我所考虑的大学里)Python 要高得多。

      我认为,移动任务语义和使用 “const ”来表示常量,都是 Rust 因老语言将其疯狂的选择当作 “就是这样 ”而受到严重影响的例子。

      1. 已经尝试过了!我已经教过 400 多名初学者了,我无法夸大这不仅是可能的,而且实际效果非常好。学生们立刻就有了学习其他语言的基础。

    3. 我就是博文开头那句话的作者。目前,我已经把铁锈语言作为第一语言教给了400多人,我对这个主题及其评论中的说法感到非常好笑。经过几年的实践,我不仅相信它是可行的,而且还看到了它运行良好的大量证据。

    4. 我同意。

      如果人类能一下子接受 Rust 语言,它也不会是一门糟糕的语言。问题是,没有人会像那样学习,如果你不知道很多关键概念,Rust 就很难迭代。

      这样做的问题是,当你在其他语言上达到一定熟练程度时,你必然要重新学习一些东西,或者感到沮丧,因为它仍然需要所有这些概念,而这些概念在其他地方并不存在。

      1. Rust 有一个简单的子集,即 “纯函数式 ”子集,它完全不使用引用类型,只是按值传递一切。使用 &(“共享”)引用只是稍微难一些。所有这些特性都可以与任何编程语言都不独有的基本概念一起逐步引入。我知道 Rust 通常不是这样教学的,但这种方法值得尝试。

    5. 如果你想正式学习,比如跟着一本书/系列/课程学习,我建议你先学习 Python 或 Lua,然后再学习 JS。JS(和 TS)具有极大的灵活性,而且多年来许多功能都得到了增强,这在某些情况下取决于运行时上下文和构建工具。

      别误会我的意思,在 “Good Parts ”一书问世之前,我就喜欢 JS/TS。作为第一语言,它唯一的优势就是你可以直接在浏览器中开始修补… 浏览器中的调试控制台,让我可以在 UI 功能发布之前就使用生成的 API 客户端。

      如果你想抱着 “我想构建任何东西 ”的目的来学习,那么 JS/TS 可能是一门很好的入门语言……你可能会想先读一些类似于傻瓜书的东西,然后用编码 AI 进行引导……然后修修补补,直到成功为止。注意:刚开始学习时,不要对任何安全方面的关键内容使用这种语言。

    6. 一般来说,我建议大家在学习第一门语言时不要使用静态类型。我是静态类型的支持者,但对于初学者来说,静态类型会造成不必要的混乱。

      编译器错误在本质上往往是反事实的:“编译器无法证明这不是无 ”或 “一旦你通过这个泛型方法,就不再知道你的 Animal 列表只包含 Dog 的成员 ”通常比一个具体的测试用例让你的程序崩溃更令人困惑,也更不容易处理。然后,你就可以开始打印堆栈跟踪。

      如果你仔细查看执行过程中的每一行并检查所有数据,你几乎可以肯定自己不会出错。如果你从编译器中得到了一些胡言乱语,我就不能这么说了。

    7. 平心而论,Rust 在第 10 种语言能力上确实也是如此。

      1. 我真的不这么认为,但这可能取决于你在生活中用过哪些语言。

        我职业生涯的大部分时间都在使用 C 和 Ruby,也有相当多的时间在涉猎函数式语言。这篇文章中关于 Rust 的所有核心内容或多或少都让我眼前一亮。其他功能是存在的,但通常不是你理解整个 Rust 所需要的。

        对大多数人来说,最大的障碍是了解借用检查器真正想教给你的程序结构,并将这些经验内化。不过,有了一些 RAII 的经验,以及在 C 语言中手动指针所有权方面的良好实践,有助于为理解这些概念铺平道路。

      2. 其实不然。如果你在 C/C++ 领域,你就很清楚 Rust 能解决什么问题。如果你甚至只知道 GC 语言,那么是的,这将是一场艰苦的战斗。

    1. > 它显然在很大程度上受到了 Rust 的启发

      创造者说了不是。虽然它的编译器是 Rust 的。但就范式和目标运行时而言,Gleam 是完全不同的语言。它无法真正取代 rust

      1. > 创造者说它不能。

        考虑到大量的相似之处,这似乎不太可能。

        1. 哪些相似之处是其他语言没有的?

          1. * 相同的名称:fn、let、mut、pub、println 等。

            * 相同的 “一切皆表达式”(everything-is-an-expression)风格。块对其中的最后一个表达式进行求值。

            * 文件注释

            * ML 风格模式匹配

            * 结果类型

            * todo, panic

            "Whhaaa 但是你可以在其他语言中找到这些功能!你说的是 ML 风格,你这个大白痴!!"

            的确如此,但 Rust 大量普及了这些特性(有多少人真正使用 ML?

            有趣的是,他们修正了元组语法问题(即 #(a,b,c)),但也复制了匹配模式匹配的错误,即你可以写一个看起来像变量名的东西,然后它就变成了一个模式。这个错误从上世纪 70 年代就已经存在了。

            1. 我认为基本上所有设计编程语言的人都熟悉 ML,即使它在生产中并不常用。

                1. Go 是一个罕见的标本:编程语言的设计者从根本上怀疑编程语言的设计领域。

    2. 如果你对另一种 “简单的铁锈语言 ”感兴趣,我鼓励你看看 fsharp。

      1. Wings3D 是一个用 Erlang 编写的 3D 建模程序,因此我认为你也可以用 Gleam 来做,因为它的运行时是一样的。不过我认为它可能不是最适合这项工作的工具。

    3. 在主页上

      >Black lives matter. Trans rights are human rights. No nazi bullsh*t.

      No thanks. This is DOA.

  8. > 让我们来看一个做一些非小事的 Rust 程序:……(从非常短的程序源代码中,对一些深层次的技术细节进行了大量非常具体的解释)

    这个程序到底_做了什么?

    所有这些非常微妙的分析,都是关于 rust 语言的特定属性,针对的是这个单独的程序,并没有对程序应该做什么进行实际的总结或描述!

    它只是在符合特定条件的文件被修改时打印出一行,仅此而已。

    这个程序所做的事情描述起来微不足道,实现起来也应该相应地简单。

    对于那些渴望复杂性的人来说,这是一个令人兴奋的挑战,他们可以应对和解决!但这种复杂性、这种挑战是自找的,几乎总是偶然的和不必要的工作

    1. > 正如其他评论所说,在Rust语言中,程序员需要关心许多与当前问题领域无关的细节。

      正如其他评论所说,在其他语言中,你也会遇到同样的问题,只是那些语言不会帮你处理而已。并非所有文件名都可以打印,但大多数语言都不会帮你处理这个问题。几乎所有的语言都有某种方法可以让函数失败,不返回你想要的值–在 Rust 中,这种方法是通过返回类型来实现的,返回类型显示它可能返回不同的值,而在其他语言中,这种方法是通过 “异常 ”这个新颖的概念来实现的,你必须担心每个函数是否都是 “异常安全 ”的,这实际上并不简单,即使它乍看起来很简单。等等。

      1. > 不是所有的文件名都可以打印,但大多数语言都不会帮你处理这个问题

        实际上每个文件名都是可打印的,问题只是如何以 “可打印 ”的形式呈现文件名,而这是文件名和目标输出的一个函数

        这并不是一个有趣的问题,每种语言和/或其 stdlib 都应该为这类事情提供合理的默认值。这些细节正是像 OP 中的程序不需要关心的事情,除非他们明确选择加入这种复杂性。

        > 在其他语言中,你也会遇到同样的问题,只是那些语言不会帮你处理而已。

        好的语言会让你处理这些问题,但它们不会强制你去处理它们。而 rust 会直接、不可避免地将所有这些问题提交给程序员,并说:"嘿,你需要处理这个问题。

        管理像所有权或内存管理之类的底层细节,并不具有某种内在的美德或价值。

        1. > 实际上每个文件名都是可打印的

          不对。Unix 上的文件名可以是任意的字节序列,即使在 Windows 上,它们也可以是当前本地不存在的字符。

          > 这不是一个有趣的问题,每种语言和/或其 stdlib 都应该为这类事情提供合理的默认值。

          在你遇到这个问题之前,它并不是一个有趣的问题。操作系统应该制定更好的标准,但鉴于它们不能或不愿制定标准,每个应用程序都需要处理这种情况。

          1. > 在 Unix 上,文件名可以是任意的字节序列,甚至在 Windows 上,文件名也可以是在当前语言中不存在的字符。

            在这种情况下,要打印它们的东西有责任使它们可打印,对吗?

            我猜我们对 “可打印 ”的定义不一样,这使得我们的讨论根植于语义学,而语义学并不有趣。

            1. > 在这种情况下,想要打印它们的东西有责任让它们可以打印,对吗?

              这就使得将这一责任浮现给你的应用程序代码(就像 Rust 所做的那样),要比默默地将它们变成空字符串(就像其他一些语言所做的那样)要好,不是吗?

              1. 如果我尝试print(filename),但程序无法编译,因为filename是 "esoteric_string_type&' a` 而print期望的是`abstract Trait type SpecificPrintable` 或其他什么,那么与允许程序编译并打印出可以工作的东西相比,这不是一个可取的结果–至少在一般情况下是这样。

                我的意思是,你可以创建一个文件名为 `0x01 0x04 0xff 0x0a` 的文件,但它不像 `ls` 那样处理这种愚蠢的事情。

    2. 但实现_是_简单的,尤其是对于这样一种高性能语言。它只占不到一页纸的篇幅。你还能指望什么呢?

      1. 简单不是用 SLoC 来衡量的,而是用认知复杂度来衡量的

        文章本身明确指出,(简短的)“铁锈 ”程序需要了解大量非繁琐的语言特征,以及它们所有组合交互的行为结果–其中大约没有一个与该程序正在解决的问题相关!

        我所期望的是,当一种语言被用来表达给定问题的解决方案时,它所增加的认知复杂性(开销)应尽可能少

    3. > 这个程序所做的事情描述起来微不足道,因此它的实现也应该相应地简单。

      为什么您认为简单的解释会带来简单的实现?作为一个例子,请参阅 XKCD 1425[0] ,我在此将其副本包括在内:

      > Cueball: When a user takes a photo, the app should check whether they're in a national park…

      > Ponytail: Sure, easy GIS lookup. 给我几个小时。

      > Cueball: ……并检查照片上是否有鸟。

      > 马尾辫: I'll need a research team and five years.

      > 在 CS 中,很难解释简单和几乎不可能之间的区别。

      [0] https://xkcd.com/1425/

  9. 这感觉像是一种相当不友好的看法。作者很方便地遗漏了理解 JavaScript 版本所需的所有东西,包括 async/await、promises、模块、字符串插值、lambda 语法,更不用说运行这个版本的运行时了,等等。

    你也不必从一个同时调用 20 个概念的程序开始,每一个生疏的概念都可以有自己的 hello world,一次介绍一个概念。坦率地说,其中有几个只是 CS 的基本概念。

    1. > 作者很方便地遗漏了理解 JavaScript 版本所需的所有东西,包括 async/await、promises、模块、字符串插值、lambda 语法,更不用说运行这个版本的运行时等等。

      还有迭代器。

      1. 还有 “for of ”与 “for in”(后者通常不应使用)

    2. 我不认为你可以编写一个不使用所有这些概念的 Rust 程序,至少在非常基础的教程材料之外。在你编写自己的程序时,你很快就会遇到提到这些概念的编译器错误。

      > 作者很方便地遗漏了理解 JavaScript 版本所需的所有内容,包括 async/await、承诺、模块、字符串插值、[…] 更不用说运行这个程序的运行时等等。

      Rust 版本中也没有这些内容。

    3. 我是说这两种语言(和程序)都有模块、字符串插值、lambda语法和编译器/解释器。他们唯一遗漏的就是 Promises。而且你是在数组上迭代,所以不需要了解迭代器。你可以写很长时间的 Python 而不需要了解 __iter__。在 Rust 示例程序中,迭代器是暴露的。我想,如果 Rust 版本只使用 for 语法,你可以说你不需要了解迭代器。

      1. Javascript 有异常(文章甚至提到了异常,但似乎认为异常是直观的),而 Rust 没有。而且,Javascript 的 “一级函数 ”语法客观上并不比 Rust 的 lambda 语法简单,文章似乎也是这么认为的。

    4. 在我看来,作者是在阐述显性知识与隐性知识的区别。在 Rust 版本中,编译器会 “骂你”,礼貌地指出代码无法运行的地方,而 js 版本……只是运行,但可能无法运行?

    5. 并非如此,他明确指出,在 Javascript 中编写有用的简单程序更容易,不需要额外的东西,而在 Rust 中,你需要了解很多东西才能编写出同等的简单程序。

  10. 在编写了适量的 Rust 和 TypeScript 后,它们似乎有所不同,但我不会说其中一种语言的项目比另一种语言的项目简单得多。Rust 本身感觉比 TypeScript 复杂一些,但考虑得更周全。TypeScript 有更多的杂乱无章和遗留脚本。Rust 的工具更好(也更快)。对于编写网页前端代码来说,TypeScript 能更快地完成工作,而我遇到的大多数其他需求,在可以重新开始的情况下,使用 Rust 或 Python 更容易解决。

  11. 我觉得 Rust 的语义非常连贯一致。与其他语言相比,完成不同任务所需的 “糖 ”似乎比这篇文章所关注的要少。

    一般来说,所有接口都符合 mem 模块中的模式。如果你想清楚地了解其他一切的结构,最好从这里开始: https://doc.rust-lang.org/std/mem/

  12. > Bjarne Strousup有一句名言:“在C++内部,有一种更小巧、更简洁的语言正在努力脱颖而出”。在 Rust 中,也有一种更小巧、更简洁的语言正在努力脱颖而出:它有明确的愿景、目标和重点。这种语言是连贯的,因为它的功能是一致的。这篇文章就是关于这种语言的。

    问题是,使用这种语言的 “核心 ”永远不会奏效。你要么需要一个功能,要么需要一个库来触及你试图忽略的糟糕部分。

    1. 另外,至少对于 C++ 来说,不同的人对核心的理解是不一致的。

    2. 不过,如果 95% 的代码库都只使用核心,而不使用核心的部分被归入已知的 “龙 ”领域,并由几位经验丰富的工程师进行审查,那就容易多了。

  13. 我认为 Rust 是支持大语言模型(LLM)的最大赢家。当我遇到编译错误时,如果没有大语言模型(LLM),要反向思考如何重写以及为什么要这样写是非常痛苦的。

  14. 编辑:我把这条评论保留下来,这样回复才有意义,但我完全没理解这里的意思。这就是我在午休时间写黑客新闻评论的后果!

    我觉得很难认真对待这个问题,因为 JS 代码段有一个明显的语法错误和两个明显的 bug,这表明作者并没有认真思考他们想要表达的观点。

    我理解他们想表达的观点,那就是 rust 迫使你明确地而不是隐含地处理问题的复杂性。只是他们很容易就忽略了 JavaScript 版本要求程序员了解 async await 的工作原理、迭代器(他们使用不当)、字符串插值等。仅仅使用 typescript 类型注解就已经让 js 版本拥有了 rust 的几乎所有显式性。

    1. > 我理解他们想表达的意思,那就是 rust 迫使你显式地而不是隐式地处理问题的复杂性。

      这不是我想表达的观点。

      1. 我又读了一遍,明白了你的意思。很抱歉我这么快就发表了评论,我当时在用手机,还没来得及消化内容就把评论打出来了。

  15. 我曾经教过一些没有编程经验的人,我可以告诉你,对于 Python 这种典型的 “初学者 ”语言来说,你必须一次性学习的概念列表基本上和 Python 一样长。

    因此,作者的论点给人一种智力上不诚实的感觉。尤其明显的是与 JavaScript 的比较。后者有大量的概念需要处理,包括一些真正奇怪的概念,比如原型。

    Rust 之所以难学,我认为有两个原因:

    1)借用检查器处于一个令人不舒服的位置,它笨到会拒绝完全有效的代码,但又聪明到很难理解它是如何工作的。

    2)正如作者所指出的,有很多杠杆可用于底层控制、精确分配等。

    关于第二点,作者描述了他希望看到的一种语言:绿色线程、编译器决定分配、更少选择和简单的线程安全。

    这种语言已经存在(除去代数类型)。它叫 Go。它非常好,设计精良,适合初学者。有些人不喜欢它的美观,但这并不足以成为再次发明它的理由,只是它的语法受到了 Rust 的启发。

    1. Go 不是线程安全的。它甚至允许自己的运行时出现数据竞赛。Go 1.25 修正了一个在运行时中存在了两年之久的 nil 检查错误。

      一般来说,在涉及通道、互斥(或不互斥……)、WaitGroups、atomics 等时,Go 可以让你编译任何东西。由于没有静态检查,编译通常会失败。

      1. 说得好。我记得在 Go 的早期,人们对编译器未来静态判断程序的能力比现在乐观得多。遗憾的是,这门语言从未达到这一目标。

        可悲的是,考虑到谷歌的新文化和所有已经离开的人,它似乎不太可能到达那个境界。

    2. > 对于 Python 这门典型的 “初学者 ”语言来说,你必须同时学习的概念列表基本上是一样长的

      在我看来,Python 非常棒。但部署 Python 比学习 Rust 更费劲,而且工具也需要不断学习: 我听说现在所有严肃的 Python 团队都在使用 UV。最好学学它,否则就会被甩在后面!

      同时,vanilla Rust 拥有你所需要的一切。我很庆幸自己明白了沉没成本谬误,并在经过几个月的学习后脱离了 Python。

      1. 我刚刚在 twitch 上看到一个经验丰富的程序员挣扎了一个小时,试图用 UV 安装一个 Python 编写的程序!这确实是个大问题,Python 程序的发布仍然是一团糟。

      2. 这对 UV(UV 非常棒)和 2025 年之前的 Python 工具绝对不公平(又慢又脆,即使安装了也是如此–你好,Debian 的衍生产品)。UV 是我们期待已久的 Python 货物,预计你在很长一段时间内都不需要学习其他东西了。

    3. > 智力上不诚实

      你在这里声称作者的论证是故意误导。这是一个非常强烈的指控,而且你在评论中也没有支持它。

      当你继续列举 rust 所不具备的困难时,你也立刻破坏了自己的论点。

    4. Rust 更大的问题在于编译器太 “贱 ”了,它对每次添加一行的增量式开发充满敌意。

      尤其是未使用的变量会导致编译错误,而不是警告。

      1. 什么?在 Rust 中,未使用的变量就是警告。你在想 Go 吗?

  16. 又是一篇关于 Rust 不讨他们喜欢的帖子,因为 Rust 有他们熟悉的语言所没有的东西。例如,作者说异常在某种程度上比结果类型 “更简单”,而 Rust 的缺点是不允许在不检查错误的情况下获取值。作者还抱怨了“…… ”操作符。我认为,不能因为你更熟悉异常风格的编程,就认为它更简单。

    1. 我认为你误解了作者的写作方向–Jynn 绝对不是一个不理解 Rust 中的事物为何如此的 Rust 新手。

      1. 是的–作为一个还没有接触过 Rust 的人,我从这篇文章中感受到了 “是的,它很复杂,但这是出于好的原因,因为所有这些复杂的东西都能很好地协同工作,让编写软件变得更好……如果不能做到这一点,你就会遇到 Pythons 模式匹配之类的事情,这太糟糕了”。

    2. 我读这篇文章的时候并没有觉得它对 Rust 有什么特别的批评,它只是证明了 Rust 有很多概念,你需要以耦合的方式了解这些概念,才能用这门语言完成大多数功能性的事情。这只是意味着它是一种稍难学习的语言。

      我自己在刚开始用 Rust 做东西时也遇到过这种情况。当你对 Rust 中的 X、Y 和 Z 没有牢固的掌握时,你会发现编译器的错误,然后查找它们的含义。然后,为了实现你的愿望,你会钻进 mut 和 clone 的兔子洞……通常只是克隆,因为这样做更简单,即使性能较差,后来才明白 (a)rc 等等。

      在另一种语言中,即使不正确,你也能运行一些东西,这比让编译器从一开始就对你大喊大叫(慢慢地)更让人满意……这是一个更慢、更痛苦的反馈循环。它并不_糟糕_,只是有所不同。这并不意味着它不是真实的,也不意味着我或 TFA 在抨击 Rust。

  17. 在我看来,更小的 Rust 在精神上更像 Zig。就像 C++ 之于 C。

    1. 没有默认安全性的 Rust,也就是没有借用检查器的 Rust,不是 Rust。一个更小的 Rust 要么删除不安全的构造(然后你会得到更像新 Java/Scala/Kotlin 的东西,除了别名 XOR 突变性),要么保留指针/引用的区别(Zig 没有)。

      Zig 更像是小号的 C++,而不是小号的 Rust。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: