提问人:kam 提问时间:12/4/2020 最后编辑:kam 更新时间:12/5/2020 访问量:137
F# seq 行为
F# seq behavior
问:
我对 F# 中序列表达式的内部工作有点困惑。
通常,如果我们使用 seq 制作一个顺序文件读取器,而没有故意缓存数据
seq {
let mutable current = file.Read()
while current <> -1 do
yield current
}
如果我们尝试进行一些重复或回溯,我们最终会得到一些奇怪的行为,我的想法是,由于 Read() 是一个调用一些可变值的函数,如果我们重新迭代,我们不能期望输出是正确的。但是,即使在边界读取上,这也表现得很好?
let Read path =
seq {
use fp = System.IO.File.OpenRead path
let buf = [| for _ in 0 .. 1024 -> 0uy |]
let mutable pos = 1
let mutable current = 0
while pos <> 0 do
if current = 0 then
pos <- fp.Read(buf, 0, 1024)
if pos > 0 && current < pos then
yield buf.[current]
current <- (current + 1) % 1024
}
let content = Read "some path"
我们显然使用相同的缓冲区来增强性能,但假设我们读取 1025 字节,如果我们在仍然获得正确输出后尝试读取位置< 1025 的任何字节,它将触发对缓冲区的更新。这怎么可能,有什么区别?
答:
你的问题有点不清楚,所以我试着猜测。
当你创建一个 时,你实际上是在创建一个状态机,它只会在它需要的时候运行。当您从它请求第一个元素时,它将从顶部开始并运行,直到您的第一条指令。然后,当您请求另一个值时,它将从该点运行到下一个点,依此类推。seq { }
yield
yield
请记住,a 会产生一个 ,这就像一个“执行计划”。每次开始迭代序列(例如,通过调用 ),都会在后台调用序列,这会导致创建一个新序列。它是实际提供价值的。你可以用更经典的术语来理解它,即有一个可以迭代的数组(可迭代或可枚举)和该数组上的许多指针,每个指针都位于数组中的不同点(许多迭代器或枚举器)。seq { }
IEnumerable<'T>
Seq.head
GetEnumerator
IEnumerator<'T>
IEnumerator
在第一个代码中,很可能是块的外部。这意味着您正在读取的文件已烘焙到执行计划中;无论您开始迭代序列多少次,您都将始终从同一个文件中读取。这显然会导致不可预测的行为。file
seq
但是,在第二个代码中,该文件将作为块定义的一部分打开。这意味着,每次迭代序列时,你都会获得一个新的文件句柄,或者实质上,每个枚举器都会获得一个新的文件句柄。此代码起作用的原因是,您不能反转枚举器或多次迭代它,至少不能使用单个线程。seq
(现在,如果您要手动获取枚举器并将其推进到多个线程上,您可能会很快遇到问题。但这是一个不同的话题。
评论
seq
FileStream
已缓冲。来自文档:FileStream 缓冲输入和输出以获得更好的性能。如果有一个未缓冲的流,则可以将其包装在BufferedStream
中,它将被缓冲。但是你能解释一下你在这里想做什么吗?如果我们尝试进行一些重复或回溯,我们最终会得到一些奇怪的行为,因为序列不能被随机倒带或访问。