我有一个FileReader
类,其职责是使用StreamReader
读取和处理文本文件。为了便于单元测试,我想为这个类提供一个类型参数,以便我可以将StreamReader
交换为实际上不与文件系统交互的FakeReader
(并且可能抛出异常例如OutOfMemory
,所以我可以在FileReader
)中测试错误处理。
理想情况下,我想定义FileReader
这样的事情(为了清晰起见而无关紧要):
type FileReader<'Reader> =
member this.Read file =
use sr = new 'Reader(file)
while not sr.EndOfStream do
printfn "%s" <| sr.ReadLine()
并简单地定义FakeReader
以获得一个带有文件名,EndOfStream
属性getter,ReadLine()
方法和(空)Dispose()
方法的构造函数。但是,F#有几个关于此类型定义的抱怨,包括"Calls to object constructors on type parameters cannot be given arguments."
由于StreamReader
没有默认构造函数,因此这种方法似乎是不行的。
到目前为止,我实现这一目标的唯一方法是从FakeReader
继承StreamReader
:
type FakeReader() =
inherit StreamReader("") with
override this.ReadLine() = "go away"
member this.EndOfStream = false
member this.Dispose() = ()
并使用工厂方法,根据需要返回新的FakeReader
或新的StreamReader
。
type ReaderType = Fake | SR
let readerFactory (file : string, readerType) =
match readerType with
| Fake -> new FakeReader() :> StreamReader
| SR -> new StreamReader(file)
type FileReader(readertype) =
member this.Read file =
use sr = readerFactory(file, readertype)
while not sr.EndOfStream do
printfn "%s" <| sr.ReadLine()
这似乎不那么优雅。有没有办法用类型参数做到这一点?谢谢大家。
答案 0 :(得分:4)
使用创建阅读器对象的函数(如MizardX所示)是您问题的直接答案。但是,我可能会考虑使用与TextReader
不同的抽象。正如Ankur在评论中提到的,您可以使用更具功能性的方法。
如果您只是使用TextReader
从输入中读取文本行,则可以使用seq<string>
类型。 FileReader
类型实际上可能只是一个seq<string>
的函数(尽管这可能过于简单化了......这取决于)。
这使它更具“功能性” - 在函数式编程中,你经常使用函数转换数据结构,这正是这个例子的作用:
open System.IO
/// Creates a reader that reads data from a file
let readFile (file:string) = seq {
use rdr = new StreamReader(file)
let line = ref ""
while (line := rdr.ReadLine(); !line <> null) do
yield !line }
/// Your function that processes the input (provided as a sequence)
let processInput input =
for s in input do
printfn "%s" s
readFile "input.txt" |> processInput
要测试processInput
功能,您可以创建新的seq<string>
值。这比实现新的TextReader
类要容易得多:
let testInput = seq {
yield "First line"
yield "Second line"
raise <| new System.OutOfMemoryException() }
testInput |> processInput
答案 1 :(得分:3)
您可以传入一个构造并返回所需类型对象的函数。
type FileReader(f : string -> TextReader) =
member this.Read file =
use sr = f file
while sr.Peek() <> -1 do
printfn "%s" <| sr.ReadLine()
type FakeReader() =
inherit StringReader("")
override this.ReadLine() = "go away"
override this.Peek() = 0
let reader1 = new FileReader(fun fn -> new StreamReader(fn) :> _)
let reader2 = new FileReader(fun fn -> new FakeReader() :> _)
强制转换是必要的,因为我删除了泛型类型参数,但可以推断实际类型。