NodeJS与Rust在读取文件功能上比较

使用NodeJS与Rust读取文件,文件是读取位于项目根目录中名为 hello.txt 的文件:该文件包含 Hello world! 文本,我们将其内容记录到控制台。

NodeJS
在开始 Node 示例之前,我们将为 Nodes 内置模块安装类型声明。只有这样,TypeScript 才能知道我们读取文件所需的 fs 等模块。这些声明可以在一个名为 @types/node) 的包中找到:
$ npm install --save-dev @types/node

太好了!说完这些,我们就可以创建我们的示例了。

要读取文件并记录其内容,我可能会编写这样一个程序:

import { readFile } from 'fs';

readFile('hello.txt', 'utf8', (err, data) => {
  if (err) {
    console.log(`Couldn't read: ${err.message}`);
    process.exit(1);
  } else {
    console.log(`Content is: ${data}`);
  }
});

  • 你需要导入 readFile 函数,在调用该函数时传递文件路径('hello.txt'),可选择传递编码('utf8'),并传递一个回调函数,在文件读取完毕或出现错误时调用该回调函数。
  • 按照惯例,回调的第一个参数是错误对象(err)--如果出现错误;第二个参数是文件内容(data)--如果没有出现错误。我们用 if/else 语句检查这两种情况。
  • 请注意,如果出现错误,我们会记录下错误信息,即人类可读的错误描述。不是每个 Er 对象都会出现这种情况,但所有与文件读取相关的典型错误(如 ENOENT:无此类文件或目录,如果找不到文件,则打开'hello.txt')都会出现这种情况。
  • 我还调用 process.exit(1);来停止程序的执行,并将进程标记为失败。这是一种很好的停止程序的方式,如果该程序被其他脚本使用或在持续集成中使用,这种方式也很有用。
  • 如果没有错误发生,我们只需记录内容:console.log(`内容为:${data}`);。


总之......让我们重写这个示例,以便更容易与我们的 Rust 程序进行比较。看看新代码吧:

import { openSync, readSync, fstatSync, Stats } from 'fs';

let fileDescriptor: number;
try {
  fileDescriptor = openSync('hello.txt', 'r');
} catch (err) {
  console.log(`Couldn't open: ${err.message}`);
  process.exit(1);
}

let stat: Stats;
try {
  stat = fstatSync(fileDescriptor);
} catch (err) {
  console.log(`Couldn't get stat: ${err.message}`);
  process.exit(1);
}

const buffer = Buffer.alloc(stat.size);

try {
  readSync(fileDescriptor, buffer, 0, stat.size, null);
} catch (err) {
  console.log(`Couldn't read: ${err.message}`);
  process.exit(1);
}

let data: string;
try {
  data = buffer.toString();
} catch (err) {
  console.log(`Couldn't convert buffer to string: ${err.message}`);
  process.exit(1);
}

console.log(`Content is: ${data}`);

  • 首先,你会注意到一些 *Sync 函数的使用,而不是我们之前使用的异步风格。
  • 目前,Rust 只在标准库中提供了读写文件的同步 API。虽然异步 API 很快就会被标准化,但据我所知,目前还没有计划在标准库中添加用于文件操作(如读取或写入)的异步 API。
  • 你会注意到的第二件事是,我们现在需要打开文件(使用 openSync),然后才能读取文件内容(使用 readSync)。这比我们抽象出来的 readFile 函数要低级得多。但正如你所知道的......低级函数一般也更强大。
  • 如果需要多步或分片读取文件内容,最好是一次性打开文件并执行所有读取步骤,而不是每次读取操作都打开文件。
  • 请注意,openSync 返回的文件描述符是对文件的引用。标志 "r "告诉 openSync,我们只想稍后读取文件。
  • 下一步,我们调用 fstatSync 并传递文件描述符,以获取文件的实际大小。

测试:
$ npm -s start
Content is: Hello world!

Rust
让我们来看看整个 Rust 程序:

use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::str::from_utf8;

fn main() {
    let mut file = match File::open("hello.txt") {
        Err(err) => panic!(
"Couldn't open: {}", err.description()),
        Ok(value) => value,
    };

    let stat = match file.metadata() {
        Err(err) => panic!(
"Couldn't get stat: {}", err.description()),
        Ok(value) => value,
    };

    let mut buffer = vec![0; stat.len() as usize];

    match file.read(&mut buffer) {
        Err(err) => panic!(
"Couldn't read: {}", err.description()),
        Ok(_) => (),
    };

    let data = match from_utf8(&buffer) {
        Err(err) => panic!(
"Couldn't convert buffer to string: {}", err.description()),
        Ok(value) => value,
    };

    println!(
"Content is: {}", data);
}

我认为您可以阅读代码并掌握它的功能。它应该与我们的 Node 示例非常相似。它能运行吗?
$ cargo -q run
Content is: Hello world!

现在,让我们一步步了解所有线路。即使是其中的一些导入模块也相当有趣!
use std::error::Error;

这太令人吃惊了......如果你看一下我们的示例,实际上我们从来没有使用过类似 Error 的东西。试着删除这一行并运行 $ cargo -q run。你会得到这样的错误信息

error[E0599]: no method named `description` found for type `std::io::Error` in the current scope
 --> src/main.rs:7:53
  |
7 |         Err(err) => panic!("Couldn't open: {}", err.description()),
  |                                                     ^^^^^^^^^^^
  |
  = help: items from traits can only be used if the trait is in scope
help: the following trait is implemented but not in scope, perhaps add a `use` for it:
  |
1 | use std::error::Error;
  |

我们的 err 是一个所谓 struct 的实例,它是一种数据结构。现在你可以把它想象成 JavaScript 中的一个对象,但稍后你会学到更多关于 struct 的知识。

默认情况下,我们的 err 没有名为 description 的方法。只有当我们添加 use std::error::Error; 时,它才可用。为什么?

如果你再次阅读错误信息:

items from traits can only be used if the trait is in scope

就会发现只有在traits 处于作用域中时,才能使用traits 中的项目。

好吧。不管traits是什么,看起来 std::error::Error 其实就是一个traits 。编译器甚至推荐在这种情况下使用它。如果你使用了该traits ,它就会被添加到当前作用域中。

那么什么是traits 呢?
引用 Rust by Example 的一段话:
trait 是为未知类型:Self定义的方法集合。

trait 可以指定方法签名(就像其他语言中的接口),但它也可以提供完全实现的方法,因此这些方法对该类型是可用的。

如果我需要将其与 Node 世界中的某些东西进行比较,我可能会想到操作prototype原型。想想这个例子:

import './get-second-item';

const two = [1, 2, 3].getSecondItem();
console.log(two); // logs `2`

在./get-second-item中有:

Array.prototype.getSecondItem = function getSecondItem() {
  return this[1];
};

这与 Rust trait 不同,但有助于我理解它们:

  • 如果我不导入"./get-second-item",就无法调用 getSecondItem。
  • 如果我不使用 std::error::Error,我就无法调用 description。

不过,虽然在 JavaScript 中操作原型是一种糟糕的做法,但在 Rust 中使用 traits 却非常习以为常。多亏了诸如作用域(scoping)之类的特性,我们才没有继承操纵原型的问题。

下面继续原来Rust代码:
use std::fs::File;
std::fs 的行为很像 Node 世界中的 fs。它包含用于访问文件系统的核心结构体和特质。在本例中,我们只使用 File 这个结构体,并在其实例中使用 open 方法打开文件(类似于 Node 的 openSync)。

use std::io::Read;
Read 也是一种trait。它允许我们在文件实例上调用 read。

use std::str::from_utf8;
std::str::from_utf8 只是一个函数,我们需要用它将缓冲区(实际上是字节片段)转换为字符串片段 (&str)。

接下来是我们的主函数。这是我们程序的入口:

fn main() {
    // ...
}


我们在main主程序中做的第一件事就是打开一个文件:

let mut file = match File::open("hello.txt") {
    Err(err) => panic!(
"Couldn't open: {}", err.description()),
    Ok(value) => value,
};

哇这里有很多东西要看:
我们用 let 声明了一个名为 file 的变量。file 被标记为 mut,代表可变性。你可能已经知道可变性和不可变性的概念了--这基本上意味着我们可以改变变量的值(它是可变的),也可以不改变(它是不可变的)。

Rust 中的每个变量默认都是不可变的。当我们稍后读取文件时,这将在内部改变文件的读取位置,因此它需要是可变的。

接下来是匹配 File::open("hello.txt") {}:
让我先说一下:Rust 没有 try/catch 关键字,因为你不能抛出异常!错误的可能性通过类型来表达。

  • File::open 实际上返回 Result<File> 类型。
  • Result 类型代表成功(Ok)或失败(Err)。

现在,您可以将其视为 JavaScript Promise 的同步变体,它要么实现(类似于 Ok),要么拒绝(类似于 Err)。

理解这段代码的最后一部分是用于模式匹配的 match 关键字。你可以将 match 视为一个超级强大的 switch/case(Rust 中根本没有这个功能,因为它使用的是更强大的 match)。

它为何如此强大?它强制你覆盖每一种情况。不可能忘记任何一种情况。

如果Result结果是 Ok 或 Err,则需要同时处理两种情况。因此,我们要处理这两种情况。每种情况都可以给我们提供一个变量。

  • Err 可以包含一个 Err,
  • 而 Ok 可以包含实际结果(值)。

在 Ok 的情况下,我们只需返回值,并将其保存到文件中。(是的,我们可以在模式匹配中返回值,并将其直接保存到变量中。这里没有返回关键字,所以暂时把 Ok(value) => value 想象成 JavaScript 中自动调用的 (value) => value)。

  • 在 Err 的情况下,我们调用 panic!
  • 请记住!标记的是一个宏,宏是在编译时转换成其他代码的一些代码。
  • panic! 将记录我们的错误信息并退出程序。

所以 panic!("Couldn't open:{}",err.description()) 的工作原理非常类似于 Node 示例中的 console.log(`Couldn't get stat: ${err.message}`); process.exit(1)。

现在进入下一段代码:

let stat = match file.metadata() {
    Err(err) => panic!("Couldn't get stat: {}", err.description()),
    Ok(value) => value,
};


file.metadata 返回的结果类型类似 File::open,因此我们再次使用模式匹配。如果 file.metadata 调用成功,我们就会获得文件的元数据,就像 JavaScript 中的 fstatSync(file)。

  • stat 没有被标记为可改变突变的,因为我们不会更改它的值,而只是读取它们。
  • (stat.len()会给出文件的大小,就像 JavaScript 示例中的 stat.size)。

let mut buffer = vec![0; stat.len() as usize];

vec! 是一个宏:

  • 可以使用类似数组的语法更方便地创建 Vec。
  • 另一种方法是使用 Vec::new()

类似数组,是因为 Rust 实际上也有数组,而且它们看起来很像 JavaScript 数组(例如 [1,2])。但它们的行为并不像 JavaScript 数组。
  • JavaScript 数组更类似于 Rust 的 Vec。

在这方面,Vec 和数组可以与 String 和 &str 进行比较:
  • Vec 和 String 可以有动态大小,其行为类似于 JavaScript 数组和字符串,
  • 而 Rust 的数组和 &str 有固定大小。


因此,我们创建一个 Vec,并在 vec![x; N] 中使用 ; 来重复表达式。
这就意味着我们的 Vec 将填充 0 的 stat.len()次。
(您也可以像在 JavaScript 中一样,使用 , 创建数组或向量,如 [1、2、3])。
我们需要将 stat.len()(u64 类型)转换为 usize,因为 N 需要是 usize。这可以使用 as 关键字来完成,其工作原理与 TypeScript 类似。

最后,我们将缓冲区标记为突变,因为当我们读取文件时,缓冲区的值会发生变化。这将在下一步中完成:

match file.read(&mut buffer) {
    Err(err) => panic!("Couldn't read: {}", err.description()),
    Ok(_) => (),
};

我们用 &mut 将缓冲区 buffer 传递给 file.read。这意味着缓冲区buffer 是作为可变引用(& 标记引用)传递给 file.read。这是允许 file.read 更改缓冲区所必需的。
(在一般情况下,将缓冲区buffer 标记为可变是不够的,我们需要在每种情况下都允许其他函数或方法这样做)。

  • 只要 file.read 运行,file.read 就会借用缓冲区。
  • 如果它退出,我们的主函数就会再次成为缓冲区的所有者。
  • 这样做可以确保每次只有一个函数拥有一段内存,防止数据竞赛。这使得 Rust 非常安全。

file.read 返回值

  • file.read 没有我们感兴趣的返回值,所以我们在 Ok 的情况下什么也不做:只是 Ok(_) => ().你可以把这看作一个 noop:Ok(_) 中的 _ 只是一个占位符,
  • 最后的 () 是所谓的单元类型,用来标记一个无意义的值。(每个不返回有意义值的函数都会隐式返回(),就像 JavaScript 函数默认返回 undefined 一样)。

let data = match from_utf8(&buffer) {
    Err(err) => panic!("Couldn't convert buffer to string: {}", err.description()),
    Ok(value) => value,
};

println!(
"Content is: {}", data);

这里没有什么花哨的东西。我们只需用 from_utf8 将缓冲区转换为 &str。请注意,我们将 &buffer 传递给 from_utf8,这意味着 from_utf8 获得了 buffer 的引用(因为我们使用了 &)。因此,& 是对资源的引用,而 &mut 是对资源的可变引用。from_utf8 不需要更改缓冲区的值,所以引用不需要可变。

最后,我们只需打印出文件内容。

不错。希望你能照着示例做。说完了吗?嗯......我们可以完成了。不过,我们把 Node 示例中的高级 readFile 函数移到了一些低级函数上,以便与 Rust 进行比较,现在我们可以反其道而行之,在 Rust 中也使用一些更高级的函数。这是可能的,因为我们是一次性读取完整的文件,而不是分几步读取。

use std::error::Error;
use std::fs::read_to_string;

fn main() {
    match read_to_string("hello.txt") {
        Err(err) => panic!(
"Couldn't read: {}", err.description()),
        Ok(data) => println!(
"Content is: {}", data),
    };
}

这个示例应该很好解释。你只需将文件路径传给 read_to_string,它就会返回一个结果。在 Ok 例子中,我们打印出文件的内容。