F# 在没有反射的情况下向下转换数组

F# Downcasting arrays without reflection

我正在开发一个用户可以在运行时向我发送各种对象的程序,而我事先不知道它们的类型(在编译时)。当对象可以向下转换为 any 元素类型的 (F#) 数组时,我想对基础数组执行一些常用操作。例如。 Array.LengthArray.sub...

我可以从用户那里得到的对象是 box [| 1; 2; 3 |]box [| "a"; "b"; "c" |] 或任何 'a[],但我在编译时不知道 'a时间。

以下无效:

let arrCount (oarr: obj) : int =
    match oarr with
    | :? array<_> as a -> a.Length
    | :? (obj[]) as a -> a.Length
 // | :? (int[]) as a -> a.Length     // not an option for me here
 // | :? (string[]) as a -> a.Length  // not an option for me here
 // | ... 
    | _ -> failwith "Cannot recognize an array"

例如,arrCount (box [| 1; 2; 3 |])arrCount (box [| "a"; "b"; "c" |]) 都失败了。

到目前为止我找到的唯一解决方案是使用反射,例如:

type ArrayOps =
     static member count<'a> (arr: 'a[]) : int = arr.Length
     static member sub<'a> (arr: 'a[]) start len : 'a[] = Array.sub arr start len
     // ...

let tryCount (oarr: obj) =
    let ty = oarr.GetType()
    if ty.HasElementType  && ty.BaseType = typeof<System.Array> then
        let ety = ty.GetElementType()
        let meth = typeof<ArrayOps>.GetMethod("count").MakeGenericMethod([| ety |])
        let count = meth.Invoke(null, [| oarr |]) :?> int
        Some count
    else 
        None

我的问题:有没有办法在 box [| some elements of some unknown type |] 形式的参数上使用 Array.countArray.sub 等函数而不使用反射?

奇怪的是,这在 C# 和 VB 等语言中不是问题,而您可能需要为 F# 做一些额外的工作。通常不能这样做,因为 F# 没有协变类型。

为该功能投票here

但是我们确实有灵活的类型,它给我们提供了相当有限的逆变,我们可以使用它。

let anyLength (arr : #obj[]) = 
    arr |> Array.length

而且,

let iarr = [| 1; 2; 3|]
let sarr = [|"a"; "b" |]

anyLength iarr // 3
anyLength sarr // 2

由于 F# 是静态类型安全的,它试图阻止您这样做,这就是为什么它不是微不足道的。将其转换为 array<_> 将不起作用,因为从 F# 的角度来看,array<obj> 不等于 array<int> 等,这意味着您必须检查每个协变类型。

但是,您可以利用 array 也是 System.Array 这一事实,并对其使用 BCL 方法。这不会给你 Array.length 等,因为它们需要类型安全,但你基本上可以通过一点点工作来完成任何操作。

如果您确实有一组有限的已知类型,obj 可以是 array,我建议您使用这些已知类型创建一个 DU 并创建一个简单的转换器在 array<int>array<string> 等上进行匹配,以便您恢复类型安全。

没有任何类型安全,你可以这样做:

let arrCount (x: obj) =
    match x with
    | null -> nullArg "x cannot be null"
    | :? System.Array as arr -> arr.GetLength(0)  // must give the rank
    | _ -> -1  // or failwith

用法:

> arrCount (box [|1;2;3|]);;
val it : int = 3

> arrCount (box [|"one"|]);;
val it : int = 1

这个关于 SO 的其他答案很好地解释了为什么允许这样的强制转换会使 .NET 类型系统不健全,以及为什么在 F# 中不允许这样:


编辑:第二种选择

如果您不介意装箱整个数组,您可以通过转换整个数组来扩展上述解决方案,一旦您知道它是一个数组。但是,第一种方法(System.Array)具有 O(1) 性能,而这种方法必然是 O(n):

open System.Collections

let makeBoxedArray (x: obj) =
    match x with
    | null -> nullArg "x cannot be null"
    | :? System.Array as arr -> 
        arr :> IEnumerable 
        |> Seq.cast<obj>
        |> Seq.toArray

    | _ -> failwith "Not an array"

用法:

> makeBoxedArray (box [|1;2;3|]);;  // it accepts untyped arrays
val it : obj [] = [|1; 2; 3|]

> makeBoxedArray [|"one"|];;  // or typed arrays
val it : obj [] = [|"one"|]

> makeBoxedArray [|"one"|] |> Array.length;;  // and you can do array-ish operations
val it : int = 1

> makeBoxedArray (box [|1;2;3;4;5;6|]) |> (fun a -> Array.sub a 3 2);;
val it : obj [] = [|4; 5|]