7

I want a function that I can call as an alternative to .ToString(), that will show the contents of collections.

I've tried this:

public static string dump(Object o) {
    if (o == null) return "null";
    return o.ToString();
}

public static string dump<K, V>(KeyValuePair<K, V> kv) {
    return dump(kv.Key) + "=>" + dump(kv.Value);
}

public static string dump<T>(IEnumerable<T> list) {
    StringBuilder result = new StringBuilder("{");
    foreach(T t in list) {
        result.Append(dump(t));
        result.Append(", ");
    }
    result.Append("}");
    return result.ToString();
}

but the second overload never gets called. For example:

List<string> list = new List<string>();
list.Add("polo");
Dictionary<int, List<string>> dict;
dict.Add(1, list);
Console.WriteLine(dump(dict));

I'm expecting this output:

{1=>{"polo", }, }

What actually happens is this: dict is correctly interpreted as an IEnumerable<KeyValuePair<int, List<string>>>, so the 3rd overload is called.

the 3rd overload calls dump on a KeyValuePair>. This should(?) invoke the second overload, but it doesn't -- it calls the first overload instead.

So we get this output:

{[1=>System.Collections.Generic.List`1[System.String]], }

which is built from KeyValuePair's .ToString() method.

Why isn't the second overload called? It seems to me that the runtime should have all the information it needs to identify a KeyValuePair with full generic arguments and call that one.

Jessica Knight
  • 735
  • 6
  • 9
  • Not a duplicate, but of possible interest: http://stackoverflow.com/questions/6032908/is-there-a-library-that-provides-a-formatted-dump-function-like-linqpad – TrueWill Apr 07 '13 at 03:28
  • Probably has to do with `KeyValuePair` being a struct instead of a class. – Julián Urbano Apr 07 '13 at 03:28
  • Isn't the problem rather that the third overload isn't called for the `List`? You do get the output from the second overload, but when it dumps the pair it uses the first instead of third overload. Can you try writing out just `dump(list)`? Also: Have you debugged to verify exactly what decisions are made? Step through the code, and get wiser! =) – Tomas Aschan Apr 07 '13 at 03:30
  • 1
    `dump(list)` works just fine. `dump(dict.First())` correctly goes to the second `dump`, but when dumping the value, it goes to the first one – Julián Urbano Apr 07 '13 at 03:33
  • There is no recursion here. – Matthew Watson Apr 07 '13 at 08:19

3 Answers3

5

Generics is a compile time concept, not run time. In other words the type parametes are resolved at compile time.

In your foreach you call dump(t) and t is of type T. But there is nothing known about T at this point other than that it is an Object. That's why the first overload is called.

Petar Ivanov
  • 91,536
  • 11
  • 82
  • 95
  • Right, that makes sense. Or more specifically: generics are a run-time concept, but overloads are a compile-time concept. Right? – Jessica Knight Apr 10 '13 at 22:32
2

(updated) As mentioned in other answers, the problem is that the compiler does not know that type V is actually a List<string>, so it just goes to dump(object).

A possible workaround might be to check types at run time. Type.IsGenericType will tell you if the type of a variable has generics or not, and Type.GetGenericArguments will give you the actual type of those generics.

So you can write a single dump method receiving an object and ignoring any generics info. Note that I use the System.Collections.IEnumerable interface rather than System.Collections.Generics.IEnumerable<T>.

public static string dump(Object o)
{
    Type type = o.GetType();

    // if it's a generic, check if it's a collection or keyvaluepair
    if (type.IsGenericType) {
        // a collection? iterate items
        if (o is System.Collections.IEnumerable) {
            StringBuilder result = new StringBuilder("{");
            foreach (var i in (o as System.Collections.IEnumerable)) {
                result.Append(dump(i));
                result.Append(", ");
            }
            result.Append("}");
            return result.ToString();

        // a keyvaluepair? show key => value
        } else if (type.GetGenericArguments().Length == 2 &&
                   type.FullName.StartsWith("System.Collections.Generic.KeyValuePair")) {
            StringBuilder result = new StringBuilder();
            result.Append(dump(type.GetProperty("Key").GetValue(o, null)));
            result.Append(" => ");
            result.Append(dump(type.GetProperty("Value").GetValue(o, null)));
            return result.ToString();
        }
    }
    // arbitrary generic or not generic
    return o.ToString();
}

That is: a) a collection is iterated, b) a keyvaluepair shows key => value, c) any other object just calls ToString. With this code

List<string> list = new List<string>();
list.Add("polo");
Dictionary<int, List<string>> dict = new Dictionary<int, List<string>>() ;
dict.Add(1, list);
Console.WriteLine(dump(list));
Console.WriteLine(dump(dict.First()));
Console.WriteLine(dump(dict));

you get the expected output:

{marco, }
1 => {marco, }
{1 => {marco, }, }
Julián Urbano
  • 8,378
  • 1
  • 30
  • 52
  • This definitely seems the correct/only approach -- runtime type checking. Thanks for the example, it's good to know the exact syntax for dealing with generics at runtime! – Jessica Knight Apr 10 '13 at 22:37
0

To call the second version in your foreach, you need to specify the template parameters K and V, otherwise it will always call the first version:

dump(t); // always calls first version
dump<K,V>(t); // will call the second

How you get the parameter types K and V is another question....

chue x
  • 18,573
  • 7
  • 56
  • 70
  • This will result in a compiler error as T is not (always) convertible to List (or whatever). – usr Apr 07 '13 at 15:34