Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I convert IEnumerable<T> to string, recursively?

Tags:

c#

generics

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.

like image 460
Jessica Knight Avatar asked Apr 07 '13 03:04

Jessica Knight


2 Answers

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.

like image 143
Petar Ivanov Avatar answered Nov 03 '22 07:11

Petar Ivanov


(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, }, }
like image 2
Julián Urbano Avatar answered Nov 03 '22 06:11

Julián Urbano