基础知识§

绝大多数常见的 IO 工作由 IO::Path 类型完成。如果您想以某种形式或形状从文件读取或写入文件,这就是您想要的类。它抽象了文件句柄(或“文件描述符”)的细节,因此您大多数时候甚至不必考虑它们。

在幕后,IO::PathIO::Handle 协同工作,这是一个类,如果您需要比 IO::Path 提供的更多控制,可以使用它。在处理其他进程时,例如通过 ProcProc::Async 类型,您还将处理 IO::Handle子类IO::Pipe

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

除了所有这些类之外,Raku 还提供了一些子例程,让您可以间接使用这些类。如果您喜欢函数式编程风格或在 Raku 单行代码中,这些子例程非常有用。

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

导航路径§

什么是 IO::Path?§

要将路径表示为文件或目录,请使用 IO::Path 类型。获取此类型对象的简便方法是通过对 Str 调用 .IO 方法来强制转换它。

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

这里似乎缺少了一些东西——没有涉及卷或绝对路径——但这些信息实际上存在于对象中。您可以使用 .raku 方法查看它。

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

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

这意味着无论您如何创建 IO::Path 对象,它在技术上始终引用绝对路径。这就是为什么它的 .absolute.relative 方法返回 Str 对象,它们是将路径转换为字符串的正确方法。

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

路径部分§

给定一个本地文件名,获取其组件非常容易。例如,我们有一个文件“financial.data”,它位于某个目录“/usr/local/data”中。使用 Raku 分析其路径。

my $fname = "financial.data";
# Stringify the full path name 
my $f = $fname.IO.absolute;
say $f;
#   OUTPUT: «/usr/local/data/financial.data␤» 
# Stringify the path's parts: 
say $f.IO.dirname;                       # OUTPUT: «/usr/local/data␤» 
say $f.IO.basename;                      # OUTPUT: «financial.data␤» 
# And the basename's parts: 
# Use a method for the extension: 
say $f.IO.extension;                     # OUTPUT: «data␤» 
# Remove the extension by redefining it: 
say ($f.IO.extension("")).IO.basename;   # OUTPUT: «financial␤» 

使用文件§

写入文件§

写入新内容§

让我们创建一些文件,并从这些文件中写入和读取数据!spurtslurp 例程分别以一个块写入和读取数据。除非您正在处理非常大的文件,这些文件难以同时完全存储在内存中,否则这两个例程适合您。

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

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

但是,这就是您需要的全部代码。字符串将默认以 utf-8 编码进行编码,并且错误通过 Failure 机制处理:这些是您可以使用常规条件语句处理的异常。在本例中,我们让所有潜在的 Failure 在调用后被吞没,因此它们包含的任何 Exceptions 都将被抛出。

追加内容§

如果您想在上一节中创建的文件中添加更多内容,您可以注意 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 作为参数,以指示我们希望以追加模式打开文件进行写入。

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

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

从文件读取§

使用 IO::Path§

我们在前面的部分中看到,在 Raku 中,将内容写入文件只是一行代码。从文件中读取同样容易。

say 'my-file.txt'.IO.slurp;        # OUTPUT: «I ♥ Raku!␤» 
say 'my-file.txt'.IO.slurp: :bin;  # OUTPUT: «Buf[uint8]:0x<49 20 E2 99 A5 20 52 61 6B 75 21>␤» 

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

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

IO::Path 类型提供了另外两种方便的方法:.words.lines,它们以较小的块懒惰地读取文件,并返回 Seq 对象,这些对象(默认情况下)不会保留已消耗的值。

以下是一个示例,它在文本文件中查找提到 Raku 的行并将其打印出来。尽管文件本身太大,无法放入可用的 RAM 中,但程序不会有任何运行问题,因为内容是分块处理的。

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

以下是一个示例,它打印文件中的前 100 个单词,而无需完全加载它。

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

请注意,我们通过向 .words 传递一个限制参数来做到这一点,而不是使用 列表索引操作。这样做的原因是,在幕后仍然有一个文件句柄在使用,并且在您完全使用完返回的 Seq 之前,该句柄将保持打开状态。如果没有任何东西引用 Seq,最终该句柄将在垃圾回收运行期间关闭,但在处理大量文件的大型程序中,最好确保所有句柄立即关闭。因此,您应该始终确保来自 IO::Path.words.lines 方法的 Seq完全具体化;限制参数可以帮助您做到这一点。

使用 IO::Handle§

您可以使用 IO::Handle 类型从文件读取;这使您可以更精细地控制该过程。

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

IO::Handle 为您提供了 .read.readchars.get.getc.words.lines.slurp.comb.split.Supply 方法来从中读取数据。有很多选项;需要注意的是,您需要在完成使用后关闭句柄。

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

错误的做法§

本节介绍了如何不进行Raku IO操作。

不要使用$*SPEC§

您可能听说过$*SPEC,并且看到了一些代码或书籍展示了它在拆分和连接路径片段中的用法。它提供的一些例程名称甚至可能看起来很熟悉,就像您在其他语言中使用过的那些一样。

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

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

提示:您可以使用/连接路径部分,并将它们提供给IO::Path的例程;它们仍然会对它们执行正确操作™,无论操作系统是什么。

# 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属性的值。例如,以下代码是错误的

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

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

但是,IO::Path对象确实知道它相对于哪个目录。我们只需要使用.absolute.relative来将对象转换为字符串。这两个例程都返回一个Str对象;它们的区别仅在于结果是绝对路径还是相对路径。因此,我们可以这样修复我们的代码

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

注意$*CWD§

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

临时使用$*CWD§

以下代码是错误的

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

my $*CWD使$*CWD变为未定义。然后,.IO强制转换器继续将它创建的路径的$.CWD属性设置为未定义的$*CWD的字符串化版本;一个空字符串。

执行此操作的正确方法是使用temp而不是my。它会将对$*CWD的更改的影响局部化,就像my一样,但它不会使其变为未定义,因此.IO强制转换器仍然会获得正确的旧值

temp $*CWD = "foo".IO;

更好的是,如果您想在局部化的$*CWD中执行一些代码,请为此目的使用indir例程