15
votes

I'm trying to recursively print out all an objects properties and sub-type properties etc. My object model is as follows...

type suggestedFooWidget = {
    value: float ; 
    hasIncreasedSinceLastPeriod: bool ;
}

type firmIdentifier = {
    firmId: int ;
    firmName: string ;
}
type authorIdentifier = {
    authorId: int ;
    authorName: string ;
    firm: firmIdentifier ;
}

type denormalizedSuggestedFooWidgets = {
    id: int ; 
    ticker: string ;
    direction: string ;
    author: authorIdentifier ;
    totalAbsoluteWidget: suggestedFooWidget ;
    totalSectorWidget: suggestedFooWidget ;
    totalExchangeWidget: suggestedFooWidget ;
    todaysAbsoluteWidget: suggestedFooWidget ;
    msdAbsoluteWidget: suggestedFooWidget ;
    msdSectorWidget: suggestedFooWidget ;
    msdExchangeWidget: suggestedFooWidget ;
}

And my recursion is based on the following pattern matching...

let rec printObj (o : obj) (sb : StringBuilder) (depth : int) 
    let props = o.GetType().GetProperties()
    let enumer = props.GetEnumerator()
    while enumer.MoveNext() do
        let currObj = (enumer.Current : obj)
        ignore <|
             match currObj with
             | :? string as s -> sb.Append(s.ToString())
             | :? bool as c -> sb.Append(c.ToString())
             | :? int as i -> sb.Append(i.ToString())
             | :? float as i -> sb.Append(i.ToString())
             | _ ->  printObj currObj sb (depth + 1)
    sb

In the debugger I see that currObj is of type string, int, float, etc but it always jumps to the defualt case at the bottom. Any idea why this is happening?

5

5 Answers

18
votes

As others has pointed out, you need to invoke the GetValue member to get the value of the property - the iteration that you implemented iterates over PropertyInfo objects, which are "descriptors of the property" - not actual values. However, I don't quite understand why are you using GetEnumerator and while loop explicitly when the same thing can be written using for loop.

Also, you don't need to ignore the value returned by the sb.Append call - you can simply return it as the overall result (because it is the StringBuilder). This will actually make the code more efficient (because it enables tail-call optimizataion). As a last point, you don't need ToString in sb.Append(..), because the Append method is overloaded and works for all standard types.

So after a few simplification, you can get something like this (it's not really using the depth parameter, but I guess you want to use it for something later on):

let rec printObj (o : obj) (sb : StringBuilder) (depth : int) =
  let props = o.GetType().GetProperties() 
  for propInfo in props do
    let propValue = propInfo.GetValue(o, null)
    match propValue with 
    | :? string as s -> sb.Append(s) 
    | :? bool as c -> sb.Append(c) 
    | :? int as i -> sb.Append(i) 
    | :? float as i -> sb.Append(i) 
    | _ ->  printObj currObj sb (depth + 1) 
6
votes

Here is how I got it to work...

 let getMethod = prop.GetGetMethod()
 let value = getMethod.Invoke(o, Array.empty)
     ignore <|
         match value with
         | :? float as f -> sb.Append(f.ToString() + ", ") |> ignore
                            ...
2
votes

In your example, enumer.Current is an object containing a PropertyInfo. This means that currObj is always a PropertyInfo object, and will always correspond to the last case in your match statement.

Since you're interested in the type of the value of the property, you'll need to call the GetValue() method of the PropertyInfo to get to the actual value of the property (as in ChaosPandion's answer).

Since an Enumerator returns its values as objects, you'll also need to cast the enum.current to a PropertyInfo before you can access GetValue.

Try replacing

let currObj = (enumer.Current : obj)

with

let currObj = unbox<PropertyInfo>(enumer.Current).GetValue (o, null)

With this change, I can get your code to work (in FSI):

>  let test = {authorId = 42; authorName = "Adams"; firm = {firmId = 1; firmName = "GloboCorp inc."} };;
> string <| printObj test (new StringBuilder()) 1;;
val it : string = "42Adams1GloboCorp inc."
0
votes

Are you sure the program is not behaving as expected? The debugger spans are not always reliable.

0
votes

You want something like this instead.

let rec printObj (o : obj) (sb : StringBuilder) (depth : int) 
    let props = o.GetType().GetProperties() :> IEnumerable<PropertyInfo>
    let enumer = props.GetEnumerator()
    while enumer.MoveNext() do
        let currObj = (enumer.Current.GetValue (o, null)) :> obj
        ignore <|
             match currObj with
             | :? string as s -> sb.Append(s.ToString())
             | :? bool as c -> sb.Append(c.ToString())
             | :? int as i -> sb.Append(i.ToString())
             | :? float as i -> sb.Append(i.ToString())
             | _ ->  printObj currObj sb (depth + 1)
    sb

This is coming from the MSDN docs on the Array class:

In the .NET Framework version 2.0, the Array class implements the System.Collections.Generic.IList, System.Collections.Generic.ICollection, and System.Collections.Generic.IEnumerable generic interfaces. The implementations are provided to arrays at run time, and therefore are not visible to the documentation build tools. As a result, the generic interfaces do not appear in the declaration syntax for the Array class, and there are no reference topics for interface members that are accessible only by casting an array to the generic interface type (explicit interface implementations). The key thing to be aware of when you cast an array to one of these interfaces is that members which add, insert, or remove elements throw NotSupportedException.