Io Guide

基础知识

绝大多数常见的 IO 工作都是由 [IO::Path](https://docs.raku.org/type/ IO::Path ) 类型完成的。如果你想以某种形式或形状读取或写入文件,这就是你想要的类。它抽象出文件句柄(或“文件描述符”)的细节,因此你甚至不必考虑它们。

在幕后, IO::PathIO::Handle 一起使用 ; 如果你需要比 IO::Path 提供的更多控制,你可以直接使用的类。当与其他进程,例如通过工作 ProcProc::Async 类型,你还可以处理一个子类的 IO::HandleIO::Pipe

最后,你有 IO::CatHandle ,以及 IO::Spec 及其子类,你很少直接使用它们。这些类为你提供了高级功能,例如将多个文件作为一个句柄进行操作,或者进行低级路径操作。

除了所有这些类之外,Raku还提供了几个子程序,可以让你间接使用这些类。如果你喜欢函数式编程风格或Raku 单行程序,这些就派上用场了。

虽然 IO::Socket 及其子类也与输入和输出有关,但本指南并未涵盖它们。

路径导航

什么是 IO::Path?

要将路径表示为文件或目录,请使用 IO::Path 类型。获取该类型对象的最简单方法是通过在 Str 上调用 .IO 方法将字符串强制为 IO 类型:

say 'my-file.txt'.IO; # OUTPUT: «"my-file.txt".IO␤» 

看起来这里似乎缺少某些东西 - 没有涉及卷或绝对路径 - 但该信息实际上存在于该 IO 对象中。你可以通过使用 .perl 方法看到它:

say 'my-file.txt'.IO.perl;
# OUTPUT: «IO::Path.new("my-file.txt", :SPEC(IO::Spec::Unix), :CWD("/home/camelia"))␤» 

这两个额外的属性 - SPEC 和 - CWD 指定路径应该使用的操作系统语义类型以及路径的“当前工作目录”,即如果它是相对路径,则它相对于该目录。

这意味着无论你如何创建一个, IO::Path 对象在技术上始终引用绝对路径。这就是它的 .absolute.relative 方法返回 Str 对象的原因,它们是字符串化路径的正确方法。

但是,不要急于字符串化任何东西。将路径作为 IO::Path 对象传递。在路径上运行的所有例程都可以处理它们,因此不需要转换它们。

使用文件

写入文件

写入新内容

让我们制作一些文件并从中写入和读取数据!在 spurtslurp 子里程以块儿的形式读写数据。除非你正在处理难以完全存储在内存中的非常大的文件,否则这两个例程都适合你。

"my-file.txt".IO.spurt: "I ♥ Perl!";

上面的代码在当前目录中创建了一个名为 my-file.txt 的文件,然后将文本 I ♥ Perl! 写入其中。如果 Raku 是你的第一语言,请庆祝你的成就!尝试使用其他程序打开你创建的文件,以验证你写入的内容。如果你已经了解其他语言,你可能想知道本指南是否遗漏了处理编码或错误条件等问题。

但是,这就是你需要的所有代码。默认情况下,字符串将以 utf-8 编码方式进行编码,并通过 Failure 机制处理错误:这些是你可以使用常规条件处理的异常。在这种情况下,我们会让所有潜在的 Failures 事件在调用之后陷入沉没,因此它们包含的任何异常都将被抛出。

附加内容

如果你想为我们在上一节中创建的文件添加更多内容,你可以注意 spurt 文档中提到的 :append 参数。但是,为了更好地控制,让我们自己使用 IO::Handle 来处理:

my $fh = 'my-file.txt'.IO.open: :a;
$fh.print: "I count: ";
$fh.print: "$_ " for ^10;
$fh.close;

.open 方法调用打开我们的 IO::Path,并返回一个 IO::Handle。我们将 :a 作为参数传递,表示我们想要以追加模式打开文件。

在接下来的两行代码中,我们使用 IO::Handle 上常用的 .print 方法打印一行包含 11 个文本(I count 字符串和 10 个数字)。请注意,Failure 机制再一次负责我们的所有错误检查。如果 .open 失败,它将返回一个 Failure,当我们尝试在其上调用 .print 方法时,它将抛出。

最后,我们通过调用它上面的 .close 方法来关闭 IO::Handl。这样做很重要,特别是在大型程序或处理大量文件的程序中,因为许多系统对程序可以同时打开的文件数量有限制。如果你没有关闭句柄,最终你将达到该限制并且 .open 调用将失败。请注意,与其他语言不同,Raku 不使用引用计数,因此当离开定义它们的作用域时,文件句柄不会被关闭。只有当它们被垃圾回收并且未能关闭句柄时,它们才会被关闭,这可能会导致程序有机会在打开的句柄被垃圾回收之前达到文件限制。

从文件中读取

使用 IO::Path

我们在前面的章节中已经看到,在文件中写东西是 Raku 中的单行代码。从它们中读取,同样容易:

say 'my-file.txt'.IO.slurp;        # OUTPUT: «I ♥ Perl!␤» 
say 'my-file.txt'.IO.slurp: :bin;  # OUTPUT: «Buf[uint8]:0x<49 20 e2 99 a5 20 50 65 72 6c 21>␤» 

.slurp 方法读取文件的全部内容并将其作为单个 Str 对象返回,如果请求二进制模式,则通过指定 :bin 命名参数将其作为 Buf 对象返回。

由于 slurping 将整个文件加载到内存中,因此不适合处理大文件。

IO::Path 类型提供了另外两种便捷方法:.words.lines 惰性地读取小块文件并返回序列对象是(默认)不保存已经消耗的值。

这是一个示例,它在文本文件中查找提及 Perl 并将其打印出来的行。尽管文件本身太大而无法容纳到可用的 RAM 中,但程序运行时不会出现任何问题,因为内容是以小块的形式处理的:

.say for '500-PetaByte-File.txt'.IO.lines.grep: *.contains: 'Perl';

这是另一个打印文件中前 100 个单词的示例,而不是完全加载它:

.say for '500-PetaByte-File.txt'.IO.words: 100

请注意,我们通过传递 limit 参数 .words 而不是使用列表索引操作来完成此操作。原因是在引擎盖下仍然使用文件句柄,并且在完全使用返回的 Seq 之前,句柄将保持打开状态。如果没有引用 Seq,最终句柄将在垃圾收集运行期间关闭,但在大型程序中使用大量文件时,最好确保所有句柄立即关闭。所以,你应该始终确保SEQ从 IO::Path 的.words 和 .lines 方法是完全具体化; 而极限论证可以帮助你。

使用 IO::Handle

当然,你可以使用 IO::Handle 类型从文件中读取,这样可以更好地控制你正在执行的操作:

given 'some-file.txt'.IO.open {
    say .readchars: 8;  # OUTPUT: «I ♥ Perl␤» 
    .seek: 1, SeekFromCurrent;
    say .readchars: 15;  # OUTPUT: «I ♥ Programming␤» 
    .close
}

IO::Handle 给你 .read.readchars.get.getc.words.lines.slurp.comb.split.Supply 方法从中读取数据。有很多选项; 当你完成它时,需要关闭句柄。

与某些语言不同,当离开所定义的作用域时,句柄不会自动关闭。相反,它将保持打开,直到垃圾回收为止。为了使关闭业务更容易,一些方法允许你指定 :close 参数,你还可以使用 will leave trait 或由 Trait::IO 模块提供的 does auto-close trait。

错误的做事方法

本节介绍如何不执行 Raku IO。

不管 $*SPEC

你可能听说过 $*SPEC 并看过一些代码或书籍显示其用于拆分和连接路径片段的用法。它提供的一些例程名称甚至可能看起来与你在其他语言中使用的名称相似。

但是,除非你编写自己的 IO 框架,否则几乎不需要直接使用 $*SPEC$*SPEC 提供低级别的东西,它的使用不仅会使你的代码难以阅读,还可能会引入安全问题(例如 null 字符)!

IO::Path 类型是 Raku 世界的主力。它满足所有路径操作需求,并提供快捷例程,让你避免处理文件句柄。使用它代替 $*SPEC

提示:你可以使用 / 连接路径部分并将其提供给 IO::Path 例程; 无论操作系统如何,他们仍将与他们一起做 The Right Thing™。

# WRONG!! TOO MUCH WORK! 
my $fh = open $*SPEC.catpath: '', 'foo/bar', $file;
my $data = $fh.slurp;
$fh.close;
# RIGHT! Use IO::Path to do all the dirty work 
my $data = 'foo/bar'.IO.add($file).slurp;

但是,将它用于 IO::Path 无法提供的东西是很好的。例如,.devnull 方法:

{
    temp $*OUT = open :w, $*SPEC.devnull;
    say "In space no one can hear you scream!";
}
say "Hello"

字符串化 IO::Path

不要使用该 .Str 方法对 IO::Path 对象进行字符串化,除非你只是想将它们显示在某处以供参考或使用某些内容。该 .Str 方法返回 IO::Path 实例化的任何基本路径字符串。它不考虑 $.CWD 属性的值。例如,此代码已损坏:

# WRONG!! .Str DOES NOT USE $.CWD! 
my $path = 'foo'.IO;
chdir 'bar';
run <tar -cvvf archive.tar>, ~$path;

chdir 调用更改了当前目录的值,但我们创建的 $path 是相对于该更改之前的目录。

但是,该 IO::Path 对象确实知道它相对于哪个目录。我们只需要使用 .absolute.relative 对字符串进行字符串化。两个例程都返回一个 Str 对象; 它们的不同之处只是在于结果是绝对路径还是相对路径。所以,我们可以像这样修复我们的代码:

# RIGHT!! .absolute does consider the value of $.CWD! 
my $path = 'foo'.IO;
chdir 'bar';
run <tar -cvvf archive.tar>, $path.absolute;
# Also good: 
run <tar -cvvf archive.tar>, $path.relative;

要留意的 $*CWD

虽然通常不在视图范围内,但 IO::Path 默认情况下,每个对象都使用 $*CWD 的当前值来设置其 $.CWD 属性。这意味着有两件事需要注意。

temp $*CWD

这段代码是错误的:

# WRONG!! 
my $*CWD = "foo".IO;

my $*CWD 使 $*CWD 为未定义的。然后 .IO coercer 继续并将其创建的路径的$.CWD 属性设置为 undefined 的字符串化版本$*CWD; 一个空字符串。

执行此操作的正确方法是使用 temp 而不是 my。它会将更改的效果本地化 $*CWD,就像 my 那样,但它不会使其未定义,因此 .IO coercer 仍将获得正确的旧值:

temp $*CWD = "foo".IO;

更好的是,如果要在本地化中执行某些代码 $*CWD,请使用 indir 例程来实现此目的。

IO 

comments powered by Disqus