F#: Как правильно перечислить несколько файлов?
У меня есть куча файлов размером несколько мегабайт, которые очень просты:
- Они имеют размер, кратный 8
- Они содержат только двойные числа с прямым порядком байтов, поэтому их можно читать с помощью
BinaryReader
"sReadDouble()
метод
При лексикографической сортировке они содержат все значения в той последовательности, в которой они должны быть.
Я не могу хранить все в памяти как float list
или же float array
так что мне нужно float seq
который просматривает необходимые файлы при фактическом доступе. Часть, которая проходит через последовательность, фактически делает это в императивном стиле, используя GetEnumerator()
потому что я не хочу никаких утечек ресурсов и хочу закрыть все файлы правильно.
Мой первый функциональный подход был:
let readFile file =
let rec readReader (maybeReader : BinaryReader option) =
match maybeReader with
| None ->
let openFile() =
printfn "Opening the file"
new BinaryReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
|> Some
|> readReader
seq { yield! openFile() }
| Some reader when reader.BaseStream.Position >= reader.BaseStream.Length ->
printfn "Closing the file"
reader.Dispose()
Seq.empty
| Some reader ->
reader.BaseStream.Position |> printfn "Reading from position %d"
let bytesToRead = Math.Min(1048576L, reader.BaseStream.Length - reader.BaseStream.Position) |> int
let bytes = reader.ReadBytes bytesToRead
let doubles = Array.zeroCreate<float> (bytesToRead / 8)
Buffer.BlockCopy(bytes, 0, doubles, 0, bytesToRead)
seq {
yield! doubles
yield! readReader maybeReader
}
readReader None
А потом, когда у меня есть string list
содержащий все файлы, я могу сказать что-то вроде:
let values = files |> Seq.collect readFile
use ve = values.GetEnumerator()
// Do stuff that only gets partial data from one file
Тем не менее, это только закрывает файлы, когда читатель достигает своего конца (что ясно при взгляде на функцию). В качестве второго подхода я реализовал файл, перечисляющий обязательно:
type FileEnumerator(file : string) =
let reader = new BinaryReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
let mutable _current : float = Double.NaN
do file |> printfn "Enumerator active for %s"
interface IDisposable with
member this.Dispose() =
reader.Dispose()
file |> printfn "Enumerator disposed for %s"
interface IEnumerator with
member this.Current = _current :> obj
member this.Reset() = reader.BaseStream.Position <- 0L
member this.MoveNext() =
let stream = reader.BaseStream
if stream.Position >= stream.Length then false
else
_current <- reader.ReadDouble()
true
interface IEnumerator<float> with
member this.Current = _current
type FileEnumerable(file : string) =
interface IEnumerable with
member this.GetEnumerator() = new FileEnumerator(file) :> IEnumerator
interface IEnumerable<float> with
member this.GetEnumerator() = new FileEnumerator(file) :> IEnumerator<float>
let readFile' file = new FileEnumerable(file) :> float seq
теперь, когда я говорю
let values = files |> Seq.collect readFile'
use ve = values.GetEnumerator()
// do stuff with the enumerator
Правильно распорядитель перечислителя подходит моему перечислителю.
Хотя это реальное решение для того, чего я хочу достичь (я мог бы сделать это быстрее, читая его по блокам, как первый функциональный подход, но для краткости я не делал этого здесь), мне интересно, есть ли действительно функциональный подход для избежания этого изменяемое состояние в перечислителе.
1 ответ
Я не совсем понимаю, что вы имеете в виду, когда говорите, что использование GetEnumerator() предотвратит утечку ресурсов и позволит правильно закрыть все файлы. Ниже будет моя попытка сделать это (игнорирование части блочного копирования в демонстрационных целях), и я думаю, что в результате файлы будут правильно закрыты.
let eof (br : BinaryReader) =
br.BaseStream.Position = br.BaseStream.Length
let readFileAsFloats filePath =
seq{
use file = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)
use reader = new BinaryReader(file)
while (not (eof reader)) do
yield reader.ReadDouble()
}
let readFilesAsFloats filePaths =
filePaths |> Seq.collect readFileAsFloats
let floats = readFilesAsFloats ["D:\\floatFile1.txt"; "D:\\floatFile2.txt"]
Это то, что вы имели в виду?