我可以将"T[] 参数传递给需要 obj[] 的函数而不使用"Array.map box"吗?



简短版本:

我需要在无法修改的代码中调用一个函数。该函数需要obj[],我想'T[]传递它。我可以使用Array.map box,但我试图避免创建中间数组。有没有一种直接的方法可以将'T[]转换为obj[],而无需传递Array.map box或任何其他会创建中间数组的代码?

长版本:

我正在尝试编写需要与FSharpx.Collections中的PersistentVector类互操作的代码。(具体来说,我正在尝试在 F# 中实现 RRB 树)。PersistentVector基本上是一个分支因子为32的B树。树中的每个节点都包含以下两个内容之一:其他节点(如果节点不是叶节点)或存储在树中的项(如果节点是叶节点)。现在,在 F# 中表示此数据结构的最自然方法是使用像type Node<'T> = TreeNode of Node[] | LeafNode of 'T[]这样的可区分联合。但出于我假设的性能原因,FSharpx.Collections.PersistentVector 代码改为定义其 Node 类,如下所示:

type Node(thread,array:obj[]) =
let thread = thread
new() = Node(ref null,Array.create Literals.blockSize null)
with
static member InCurrentThread() = Node(ref Thread.CurrentThread,Array.create Literals.blockSize null)
member this.Array = array
member this.Thread = thread
member this.SetThread t = thread := t

线程代码与我当前的问题无关(它用于瞬态向量,允许某些性能改进),因此让我们将其删除,以便创建最简单的问题摘要。删除与线程相关的代码后,我们有一个如下所示的Node定义:

type Node(array:obj[]) =
new() = Node([||])
with member this.Array = array

我希望我的 RRB 树实现能够与现有的 PersistentVector 类顺利互操作,因为所有有效 PersistentVector 树的集合是所有有效 RRB 树集合的严格子集。作为该实现的一部分,我有一个继承自NodeRRBNode类(因此还必须在其构造函数中采用obj[]参数),并且我经常需要创建NodeRRBNode的新实例。例如,我对RRBTree.ofArray的实现基本上如下所示:

let ofArray<'T> (arr:'T[]) =
let leaves = arr |> Array.chunkBySize 32 |> Array.map Node
// More code here to build a tree above those leaf nodes

或者更确切地说,我想这样定义它,但我不能。上面的代码在Array.map Node调用中给了我一个类型不匹配错误。Node构造函数采用obj[],错误消息报告"类型'T[]与类型obj[]不兼容"。

我试图解决这个问题的一种方法是使用boxunbox. https://stackoverflow.com/a/7339153/2314532 让我相信,通过box管道管道处理任何类型的数组,然后是unbox将导致将该数组转换为obj[]。是的,这基本上是 .Net 类型系统的一个错误功能,它损害了类型安全性(在编译时传递的强制转换可能会在运行时失败)——但由于我需要与 PersistentVector 中的Node类进行互操作,所以我无论如何都没有类型安全的好处(因为Node使用了obj而不是可区分的联合)。因此,对于我代码的这一部分,我实际上想告诉 F# 编译器"请不要在这里保护我,我知道我在做什么,我已经编写了大量的单元测试"。但是我尝试使用box >> unbox方法在运行时失败了:

let intArray = [|1;2;3;4;5|]
let intNode = Node(intArray) // Doesn't compile: Type mismatch. Expecting obj[] but got int[]
let objArray : obj[] = intArray |> box |> unbox // Compiles, but fails at runtime: InvalidCastException
let objNode = Node(objArray)

(我明确了objArray的类型,以使阅读这个最小示例尽可能简单,但我不需要编写它:F# 从对下一行Node(objArray)的调用正确推断其所需的类型。我实际代码的等效部分没有显式类型注释,但仍然推断出obj[]数组类型,并且通过|> box |> unboxobj[]转换的相同int[]导致我的实际代码中出现InvalidCastException

另一种可能有效的方法是在我的Node创建管道中插入对Array.map box的调用:

let ofArray<'T> (arr:'T[]) =
let leaves = arr |> Array.chunkBySize 32 |> Array.map (Array.map box >> Node)
// More code here to build a tree above those leaf nodes

这执行了我想要的(创建一个Node实例的数组,这些实例将成为树中的叶子),但它在此过程中创建了一个额外的中间数组。我想让分块数组直接成为 Node 数组,否则我将烧毁 O(N) 内存并产生不必要的 GC 压力。我考虑过在管道中的某个时候使用Seq.cast,但我担心使用Seq.cast的性能影响。将已知大小的数组(此处为 32)转换为 seqs 意味着需要数组(创建Node实例)的其他代码必须首先调用Array.ofSeq,并且Array.ofSeq是使用ResizeArray实现的,因为它在一般情况下不能指望 seqs 的大小。对已经是数组的 seqs 进行了优化,但即使是该版本的Array.ofSeq也会创建一个新数组作为其返回值(这恰恰是一般情况下的正确行为,但这正是我在这里试图避免的)。

有没有办法让我将'T[]数组投射到obj[]故意放弃类型安全性,而不创建我一直努力避免的中间数组?还是我必须用 C# 编写这一段代码,以便我可以执行 F# 编译器试图保护我免受的不安全操作?

有两种可能的结果,具体取决于'T是值还是引用类型。

引用类型

如果'T是引用类型,那么您的boxunbox技巧将正常工作:

let strArray = [|"a";"b";"c";"d";"e"|]
let objArray : obj[] = strArray |> box |> unbox
val strArray : string [] = [|"a"; "b"; "c"; "d"; "e"|]
val objArray : obj [] = [|"a"; "b"; "c"; "d"; "e"|]

值类型

如果'T是值类型,那么,正如您所注意到的,转换将在运行时失败。

根本无法使转换成功,因为数组中的值类型尚未装箱。 没有办法绕过类型系统并直接转换为obj[]。 您必须为每个元素显式执行此操作。

let intArray = [|1; 2; 3; 4; 5|]
let objArray : obj[] = intArray |> Array.map (box)

处理两者

您可以编写一个泛型转换函数来检查类型是引用类型还是值类型,然后执行相应的转换:

let convertToObjArray<'T> (arr : 'T[]) =
if typeof<'T>.IsValueType then
arr |> Array.map (box)
else
arr |> box |> unbox

用法:

convertToObjArray strArray
val it : obj [] = [|"a"; "b"; "c"; "d"; "e"|]
convertToObjArray intArray
val it : obj [] = [|1; 2; 3; 4; 5|]

最新更新