如何在F#中一般处理选项值

时间:2014-07-19 14:01:31

标签: f# option idatareader

我正在编写一个适配器类来映射IEnumerable<'T>到IDataReader,完整的源代码在https://gist.github.com/jsnape/56f1fb4876974de94238以供参考,但我想问一下编写部分内容的最佳方法。即两个功能:

member this.GetValue(ordinal) =
    let value = match enumerator with
        | Some e -> getters.[ordinal](e.Current)
        | None -> raise (new ObjectDisposedException("EnumerableReader"))

    match value with
        | :? Option<string> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj
        | :? Option<int> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj
        | :? Option<decimal> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj
        | :? Option<obj> as x -> if x.IsNone then DBNull.Value :> obj else x.Value
        | _ -> value

此函数必须返回一个对象,但由于传递的值可以是任何F#选项类型,下游函数(如SqlBulkCopy)无法理解这一点我需要解压缩选项并将其转换为null / DBNull。

上面的代码有效,但我觉得它有点笨重,因为我必须为不同的类型添加新的特化(浮动等)。我确实尝试使用通配符| :?选项&lt; _&gt;作为x - &gt;在匹配中但编译器给了我一个'不太通用的警告,代码只匹配Option&lt; obj&gt;。

如何更恰好地写这个?我怀疑活动模式可能起作用,但我从未使用它们。

与其他功能类似:

member this.IsDBNull(ordinal) =
    match (this :> IDataReader).GetValue(ordinal) with
        | null -> true
        | :? DBNull -> true
        | :? Option<string> as x -> x.IsNone
        | :? Option<int> as x -> x.IsNone
        | :? Option<decimal> as x -> x.IsNone
        | :? Option<obj> as x -> x.IsNone
        | _ -> false

我不在乎它是什么类型的Option类型我只想检查IsNone

2 个答案:

答案 0 :(得分:4)

我认为你应该使用这样的反射技术:

open System

let f (x:obj) =
    let tOption = typeof<option<obj>>.GetGenericTypeDefinition()
    match x with
    | null -> printfn "null"; true
    | :? DBNull -> printfn "dbnull"; true
    | _ when x.GetType().IsGenericType && x.GetType().GetGenericTypeDefinition() = tOption ->
        match x.GetType().GenericTypeArguments with
        | [|t|] when t = typeof<int> -> printfn "option int"; true
        | [|t|] when t = typeof<obj> -> printfn "option obj"; true
        | _                          -> printfn "option 't" ; true

    | _ -> printfn "default"; false


let x = 4 :> obj
let x' = f x  //default

let y = Some 4 :> obj
let y' = f y  // option int

let z = Some 0.3 :> obj
let z' = f z  // option 't

<强>更新

事实上,如果您只是想检查所有选项类型的IsNone情况并且不想使用反射而不需要其他情况,则它们将属于null情况,因为None被编译为null。例如,使用上一个函数试试这个:

let y1 = (None: int option)  :> obj
let y1' = f y1  // null

let z1 = (None: float option)  :> obj
let z1' = f z1  // null

正在处理第一种情况(空案例)

对于GetValue成员,我看了你的要点,因为你定义了通用的'T已经在包含该成员的类型中,你可以写:

match value with
| :? Option<'T> as x -> if x.IsNone then DBNull.Value :> obj else x.Value :> obj

适用于所有选项类型。

答案 1 :(得分:2)

正如古斯塔沃的回答所暗示的那样,你应该使用反思。如果参数obj在编译时未知,则没有其他方法可以从option<'a>转换为'a类型。相反,您必须将参数检查为System.Type对象,然后决定下一步该做什么。

执行此操作的一般操作是设置一个可以将任何选项类型作为参数的函数,并返回类型不依赖于选项类型的参数的函数。然后可以在确定参数类型之后通过反射调用此函数。

要定义可以将任何选项类型作为参数的函数,辅助器接口很有用,因为我们可以在该接口中定义泛型方法:

type IGenericOptionHandler<'result> =
    abstract Handle<'a> : 'a option -> 'result

请注意,作为整体的接口在'result方法的 return 类型Handle上是通用的,但内部'a参数仅在方法本身的定义。

我们现在可以定义一个调用此接口的函数:

let handleGeneric
        (handle : IGenericOptionHandler<'result>)
        (x : obj)  // something that might be an option type
        (defaultValue : 'result) // used if x is not an option type
      : 'result =

    let t = x.GetType()
    if t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<_ option>
        then
            match t.GetGenericArguments() with
            | [|tArg|] ->
                handle
                  .GetType()
                  .GetMethod("Handle")
                  .MakeGenericMethod([|tArg|])
                  .Invoke(handle, [|x|])
                 :?> 'result
            | args -> failwith "Unexpected type arguments to option: %A" args
        else defaultValue

最后,我们可以使用对象表达式方便地调用它,例如以下内容将作为类似于上面的IsDBNull的通用选项类型检测器 - 您需要在DBNull参数中添加defaultValue的特殊情况才能完全复制它。 / p>

Option.handleGeneric
    { new IGenericOptionHandler<bool> with member this.Handle _ = true }
    (Some 5)
    false