How to cast an object to a list of generic type in

2019-01-07 21:34发布

问题:

In the following snippet my intention is to convert a System.Object (which could be an FSharpList) to a list of whatever generic type it is holding.

    match o with
    | :? list<_>              -> addChildList(o :?> list<_>)
    | _                       -> addChild(o)

Unfortunately only list<obj> is ever matched as a list. I would like list<Foo> to also be matched as a list.

For some context, I am trying to traverse an object structure by reflection in order to build a TreeView of the class and its children. Consider the following class:

type Entity = {
    Transform   : Matrix
    Components  : obj list
    Children    : Entity list
}

I would like to build a tree that shows me all the classes that is contained in the entity. Through reflection, I can obtain all the properties of an object and also their values (The value is important, since I want to display the different elements in a list with the Name property of the element if it has one):

        let o = propertyInfo.GetValue(obj, null)

This value could be a list of some type, but the value return is just a System.Object I run into problems when trying to convert this object to a list. I am forced to do the following:

        match o with
        | :? list<obj>              -> addChildList(o :?> list<obj>)
        | :? list<Entity>           -> addChildList(o :?> list<Entity>)
        | _                         -> addChild(o)

Here I have to specify exactly the type that I am trying to convert to.
I would really like to write this:

        match o with
        | :? list<_>              -> addChildList(o :?> list<_>)
        | _                       -> addChild(o)

Unfortunately this only ever matches on list< obj >

回答1:

Unfortunately, there's no easy way to do what you want. Type tests can only be used with specific types, and even if the type test passed, the conversion operator :?> also only works to cast expressions to specific types so the right hand side of your match wouldn't do what you want anyway. You can partially work around this issue using an active pattern:

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns

let ( |GenericType|_| ) =
  (* methodinfo for typedefof<_> *)
  let tdo = 
    let (Call(None,t,[])) = <@ typedefof<_> @>
    t.GetGenericMethodDefinition()
  (* match type t against generic def g *)
  let rec tymatch t (g:Type) =
    if t = typeof<obj> then None
    elif g.IsInterface then
      let ints = if t.IsInterface then [|t|] else t.GetInterfaces()
      ints |> Seq.tryPick (fun t -> if (t.GetGenericTypeDefinition() = g) then Some(t.GetGenericArguments()) else None)
    elif t.IsGenericType && t.GetGenericTypeDefinition() = g then
      Some(t.GetGenericArguments())
    else
      tymatch (t.BaseType) g
  fun (e:Expr<Type>) (t:Type) ->
    match e with
    | Call(None,mi,[]) ->
        if (mi.GetGenericMethodDefinition() = tdo) then
          let [|ty|] = mi.GetGenericArguments()
          if ty.IsGenericType then
            let tydef = ty.GetGenericTypeDefinition()
            tymatch t tydef
          else None
        else
          None
    | _ -> None

This active pattern can be used as follows:

match o.GetType() with
| GenericType <@ typedefof<list<_>> @> [|t|] -> addChildListUntyped(t,o)
| _                                          -> addChild(o)

where you've created a variation of addChildList which takes a type t and an object o (with runtime type list<t>) instead of taking a generic list.

This is a bit clunky, but I can't think of a cleaner solution.



回答2:

It turns out that either list<'a> or array<'a> can be matched as seq<obj>

    match o with
    | :? seq<obj> -> addChildCollection(o :?> seq<obj>)
    | _           -> addChild(o)

I don't really care that it is a list. As long as I can iterate over it.