【问题标题】:Do Haskell files close automatically after readFile?读取文件后 Haskell 文件会自动关闭吗?
【发布时间】:2012-12-01 20:30:39
【问题描述】:

我想使用 Haskell 函数

readFile :: FilePath -> IO String

将文件的内容读入字符串。在the documentation 中,我读到“文件被延迟读取,按需读取,就像 getContents 一样。”

我不确定我是否完全理解这一点。例如,假设我写

s <- readFile "t.txt"

当这个动作被执行时:

  • 文件已打开。
  • s 中的字符实际上是在需要评估某些表达式时(但不会更早)从文件中读取的(例如,如果我评估 length sall 文件的内容将被读取并且文件将被关闭)。
  • 读取完最后一个字符后,与对readFile 的调用相关联的文件句柄就会关闭(自动)。

我的第三个陈述正确吗?那么,我可以只调用readFile 而不自己关闭文件句柄吗?只要我没有消耗(访问)整个结果字符串,句柄会保持打开状态吗?

编辑

这里有一些关于我的疑虑的更多信息。假设我有以下内容:

foo :: String -> IO String
foo filename = do
                  s <- readFile "t.txt"
                  putStrLn "File has been read."
                  return s

putStrLn 被执行时,我会(直觉地)期待

  1. s 包含文件t.txt 的全部内容,
  2. 用于读取文件的句柄已关闭。

如果不是这样:

  • s 在执行putStrLn 时包含什么?
  • putStrLn执行时文件句柄处于什么状态?
  • 如果putStrLn执行时s不包含文件的全部内容,什么时候会真正读取到这个内容,什么时候会关闭文件?

【问题讨论】:

  • 即使您愿意,您也无法自行关闭手柄。 hGetContents 将手柄置于半关闭状态,参见docs
  • 这是否意味着文件大部分时间都是关闭的(从操作系统的角度来看),而 Haskell 只是在需要字符时临时打开并读入内存缓冲区? (从问题中读取文档,看起来像半封闭 = 出于锁定目的而打开。)
  • @Cat Plus Plus 感谢您的链接!如果我理解正确:当我到达文件末尾时,句柄将自动关闭。但是,如果我在评估整个字符串之前关闭句柄,结果将被截断。
  • 我也不明白我的sn-p中s的类型是什么。如果是字符串,怎么能懒惰地读取呢?懒得看,不应该有一个以某种方式包含IO的类型吗?
  • @Giorgio:是的,不是吗?由于这个原因,延迟 IO 有点争议。另见What's so bad about Lazy I/O?

标签: haskell io lazy-evaluation


【解决方案1】:

我的第三个说法正确吗?

不完全是,文件没有关闭“一旦最后一个字符被读取”,至少通常不会,它会在读取期间处于半关闭状态徘徊片刻,IO- manager/runtime 将在下次执行此类操作时将其关闭。如果您正在快速打开和读取文件,如果操作系统限制不是太高,那么该延迟可能会导致您用完文件句柄。

但是,对于大多数用例(以我有限的经验)来说,关闭文件句柄是足够及时的。 [有些人不同意,认为惰性 IO 在所有情况下都极其危险。它肯定有陷阱,但 IMO 的危险往往被夸大了。]

那么,我可以直接调用readFile 而不自己关闭文件句柄吗?

是的,当您使用readFile时,当文件内容被完全读取或发现文件句柄不再被引用时,文件句柄会自动关闭。

只要我没有消耗(访问)整个结果字符串,句柄会保持打开状态吗?

不完全是,readFile 将文件句柄置于半关闭状态,如hGetContents 的文档中所述:

计算hGetContents hdl返回与hdl管理的通道或文件的未读部分对应的字符列表,它被置于中间状态,半关闭。在这种状态下,hdl 被有效关闭,但项目会按需从hdl 读取并累积在hGetContents hdl. 返回的特殊列表中


foo :: String -> IO String
foo filename = do
              s <- readFile "t.txt"
              putStrLn "File has been read."
              return s

啊,那是另一端惰性 IO 的陷阱之一。此处文件在其内容被读取之前关闭。当foo 返回时,文件句柄不再被引用,然后关闭。 foos 结果的消费者然后会发现s 是一个空字符串,因为当hGetContents 尝试实际从文件中读取时,句柄已经关闭。

我将readFile 的行为与

bracket (openFile file ReadMode) hClose hGetContents

那里。 readFile 仅在 s 不再被引用后才关闭文件句柄,因此它在此处按预期正常运行。

putStrLn 被执行时,我会(直觉地)期待

  1. s 包含文件t.txt 的全部内容,
  2. 用于读取文件的句柄已关闭。

不,s 不包含任何内容,但可能会从文件句柄中获取一些字符。文件句柄是半封闭的,但不是封闭的。当文件内容被完全读取或s超出范围时,它将关闭。

如果不是这样:

  • s 在执行putStrLn 时包含什么?
  • putStrLn执行时文件句柄处于什么状态?
  • 如果putStrLn执行时s不包含文件的全部内容,什么时候会真正读取到这个内容,什么时候会关闭文件?

前两个问题已经回答,第三个问题的答案是“文件内容被消费时会被读取”,当全部内容被读取或不再被引用时会关闭。

这与上面的bracket 调用不同-bracket 保证最终操作,这里hClose 即使其他操作抛出异常也会运行,因此通常建议使用它。但是,hClosebracket 返回时运行,然后hGetContents 无法从现在真正关闭的文件句柄中获取任何内容。但是readFile如果发生异常,不一定会关闭文件句柄。

这是惰性 IO 的危险或怪癖之一,文件在需要其内容之前不会被读取,如果你错误地使用惰性 IO,那就太晚了,你不会得到任何内容。

这是一个很多人(甚至大多数人)一次又一次陷入的陷阱,但是在被它咬伤之后,人们很快就会知道什么时候 IO 需要非懒惰,并在这些情况下非懒惰地去做。

替代方案(迭代器、枚举器、管道、管道……)避免了这些陷阱[除非实施者犯了错误],但在惰性 IO 完全没问题的情况下使用起来就不太好用了。另一方面,他们会更好地处理不需要懒惰的情况。

【讨论】:

  • "hdl管理的通道或文件的未读部分对应的字符列表":这是否意味着将返回的列表立即读入主存,然后将文件句柄放入半封闭状态?
  • 没有。这就是惰性读取的重点,它只会读取您消耗的内容(嗯,不完全是,它以块的形式读取,一次请求 bufferSize 字节,缓冲区的大小默认为 4 或 8K iirc) ,并且如果您不保留对已使用部分的引用,则可以对它们进行垃圾收集,以便您可以读取文件并在常量内存中计算其中的逗号 [愚蠢的示例]。
  • "不完全是,readFile 将文件句柄置于半关闭状态":好的,但是从操作系统的角度来看,句柄应该保持打开状态,直到读取完所有字符,或者如果在以后某个时间点消耗更多字符,文件是否会重新打开?
  • “消费者将得到一个空字符串作为文件内容。”:非常令人惊讶!如果s包含了懒惰阅读内容的所有信息,我怎么知道什么时候减少s来不及了?
  • “当 foo 返回时,文件句柄不再被引用,然后关闭。”:我希望在执行 readFile 之后不再引用句柄,并且此时文件已关闭,其内容已刷新到列表ss 不只是一个列表吗?我看不到它的类型中的IO)。我看不出退出函数foo 有什么特别之处,它会导致文件句柄被关闭(或计划尽快关闭)。
【解决方案2】:

当 putStrLn 被执行时,我会(直觉地)期望s 包含文件 t.txt 的全部内容,

您需要考虑一下您在此处使用惰性 IO 的事实。从文件中读取只会创建一个未经评估的字符串计算,如果以后需要,则会读取该文件。

通过使用惰性 IO,您可以将 IO 延迟到需要该值为止。

一旦读取了文件的最后一个字符,或者删除了对打开文件的所有引用(例如您的 s 值),垃圾收集器将关闭您打开的文件。

【讨论】:

  • 澄清一下,它不是读取触发关闭的最后一个字符,而是尝试读取检测到 EOF 条件并关闭文件的最后一个字符。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-12-13
  • 1970-01-01
  • 1970-01-01
  • 2014-12-23
  • 2012-06-16
  • 1970-01-01
  • 2010-11-03
相关资源
最近更新 更多