F#:没有默认构造函数的类作为类型参数?

时间:2011-07-19 05:43:33

标签: f#

我有一个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()

这似乎不那么优雅。有没有办法用类型参数做到这一点?谢谢大家。

2 个答案:

答案 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() :> _)

强制转换是必要的,因为我删除了泛型类型参数,但可以推断实际类型。