如何使用通用转换器将 FileHelpers.engine 与 F# 选项类型一起使用?

How do you use FileHelpers.engine with F# option types using generic converters?

展示了将 CsvHelper 与通用 F# 选项类型一起使用的好方法。您如何使用 FileHelpers 做同样的事情?

我不知道如何使通用 OptionConverter 工作。考虑以下 test.csv

X,Y,Z
hi,1,hi
bye,,

以下作品:

#r "nuget: FileHelpers"

open System
open FileHelpers

Environment.CurrentDirectory <- __SOURCE_DIRECTORY__

type OptionFloatConverter() =
    inherit ConverterBase()
    override this.FieldToString(x) = 
        match x :?> Option<float> with
        | None -> ""
        | Some x -> base.FieldToString(x) 
    override __.StringToField(x) =
        match x with 
        | "" -> None :> obj
        | s -> s |> float  |> Some :> obj

type OptionStringConverter() =
    inherit ConverterBase()
    override this.FieldToString(x) = 
        match x :?> Option<string> with
        | None -> ""
        | Some x -> base.FieldToString(x) 
    override __.StringToField(x) =
        match x with 
        | "" -> None :> obj
        | s -> s |> Some :> obj



[<CLIMutable>]
[<DelimitedRecord(",")>]
[<IgnoreFirst(1)>]
type TestExplicit =
    { X : string;
      [<FieldConverter(typeof<OptionFloatConverter>)>]
      Y : float Option
      [<FieldConverter(typeof<OptionStringConverter>)>]
      Z : string Option
      }

let generic = FileHelperEngine<TestGeneric>()

explicit.ReadFile "test.csv"

(*
val it : TestExplicit [] = [|{ X = "hi"
                               Y = Some 1.0
                               Z = Some "hi" }; { X = "bye"
                                                  Y = None
                                                  Z = None }|]
*)

如何使用这个通用版本?

type OptionConverter<'T>() =
    inherit ConverterBase()
    override this.FieldToString(x) = 
        match x :?> Option<'T> with
        | None -> ""
        | Some x -> base.FieldToString(x) 
    override __.StringToField(x) =
        match x with 
        | "" -> None :> obj
        | s -> Convert.ChangeType(s,typeof<'T>) |> Some :> obj

[<CLIMutable>]
[<DelimitedRecord(",")>]
[<IgnoreFirst(1)>]
type TestGeneric =
    { X : string;
      [<FieldConverter(typeof<OptionConverter<float>>)>]
      Y : float Option
      [<FieldConverter(typeof<OptionConverter<string>>)>]
      Z : string Option
      }
let explicit = FileHelperEngine<TestExplicit>()
generic.ReadFile "test.csv"

(*
> generic.ReadFile  "test.csv";;
FileHelpers.ConvertException: Line: 2. Field: Y@. The converter for the field: Y@ returns an object of Type: FSharpOption`1  and the field is of type: FSharpOption`1
 ---> System.InvalidCastException: Unable to cast object of type 'Microsoft.FSharp.Core.FSharpOption`1[System.Object]' to type 'Microsoft.FSharp.Core.FSharpOption`1[System.Double]'.
   at lambda_method4(Closure , Object , Object[] )
   at FileHelpers.RecordOperations.StringToRecord(Object record, LineInfo line, Object[] values)
   --- End of inner exception stack trace ---
   at FileHelpers.RecordOperations.StringToRecord(Object record, LineInfo line, Object[] values)
   at FileHelpers.FileHelperEngine`1.ReadStreamAsList(TextReader reader, Int32 maxRecords, DataTable dt)
   at FileHelpers.FileHelperEngine`1.ReadStream(TextReader reader, Int32 maxRecords)
   at FileHelpers.FileHelperEngine`1.ReadFile(String fileName, Int32 maxRecords)
   at FileHelpers.FileHelperEngine`1.ReadFile(String fileName)
   at <StartupCode$FSI_0004>.$FSI_0004.main@()
Stopped due to error
*)

问题出在您的 StringToField 方法中。 Convert.ChangeType 返回的静态类型是 obj,因此结果选项的类型是 Option<obj>,而不是 Option<float>。要解决此问题,请在调用 Some:

之前向下转换转换后的值
override __.StringToField(x) =
    match x with 
    | "" -> None :> obj
    | s ->
        let value = Convert.ChangeType(s,typeof<'T>) :?> 'T
        Some value :> _