0

I have a very simple recursively defined function that prints out the contents of any list, defined as such;

    static string ShowList<T>(IEnumerable<T> iterable)
    {
        return "[" + string.Join(", ", iterable.Select(e => ShowList(e))) + "]";
    }

    static string ShowList(string str)
    {
        return $"\"${str}\"";
    }

    static string ShowList<T>(T elem)
    {
        return elem.ToString();
    }

As you can see, the lowest override is a catch all which applies to any argument. The highest overload only applies to arguments which are enumerables. Ideally I want it to check against and run the most specific overloads first, and then if they all fail, use the general case. But what I find is that it always goes straight for the general case immediately, and just prints out Systems.Generic.Containers.List1[]. Is it possible to give certain overloads more precedence than others, so that the compiler will automatically try those before others?

Maurdekye
  • 3,597
  • 5
  • 26
  • 43
  • I don't think your function is recursive by definition – Jonesopolis Nov 17 '16 at 18:45
  • @Jonesopolis see the recursive call on line 3 – Maurdekye Nov 17 '16 at 18:46
  • I guess if T itself were IEnumerable, okay – Jonesopolis Nov 17 '16 at 18:47
  • 1
    @Maurdekye No, it's not recursive. One overload calls another overload. No overload can ever call itself, so the function is not in fact recursive. – Servy Nov 17 '16 at 18:48
  • If it prints that then it's not a string and can't go to the other overload anyway since that one expects a string, not a list – Sami Kuhmonen Nov 17 '16 at 18:48
  • @Jonesopolis Method overload resolution is done at compile time, so whether `T` is actually an `IEnumerable` is irrelevant. – Servy Nov 17 '16 at 18:48
  • @Servy it's recursive if the input is a list of lists, like `List>` – Maurdekye Nov 17 '16 at 18:49
  • No, even in that case it will still go to your 3rd listed method and not call itself again. – Igor Nov 17 '16 at 18:50
  • @Maurdekye No, it's *not*, because even if the input is a list of lists, your usage of `ShowList` in the first overload will *always* bind to the third overload. It can never end up calling the first overload, so the method is not recursive. – Servy Nov 17 '16 at 18:50
  • What I'm asking is how to prevent that and make it check against the most specific cases first. – Maurdekye Nov 17 '16 at 18:50
  • This is good to know. I guess you'd have to test if `T` itself is IEnumerable, then call your function again with T as an IEnumerable. – Jonesopolis Nov 17 '16 at 18:51
  • @Maurdekye It already does that. What you see is the result. `T` is not `IEnumerable`, and it's not a string, so the only valid overload is the third overload. – Servy Nov 17 '16 at 18:51
  • If I pass it a `List>`, it doesn't apply it to the first overload, it applies the whole thing to the third. What I want it to do is apply it to the first, where then each element (a `List`) is applied to the first again, and each element from there (`string`) is applied to the second overload. – Maurdekye Nov 17 '16 at 18:52
  • My problem here is that multiple overloads can apply to a single value, and I want to define the order in which those overloads are tested. Right now the compiler seems to prioritize them randomly, and I need to be able to prioritize them in a specific order. – Maurdekye Nov 17 '16 at 18:56
  • 1
    @Maurdekye Multiple overloads *cannot* apply to a single value. From the call site in question there is only ever one valid overload, as I explained in my previous comment, so that overload is used. Also, in cases where there *are* multiple applicable overloads (which never happens in your code) there *are* in fact strict rules for the "betterness" of an overload, you're free to look at the language specs if you want to see what they are. They are not chosen "randomly". – Servy Nov 17 '16 at 18:59
  • That is exactly what I mean when I say that multiple overloads can apply to a single value; of course they don't all apply at once, that doesn't make sense. I mean the same thing you do; that multiple overloads are applicable to a single given value. And if there are strict rules, do you mind posting them? Because I couldn't find anything relevant by searching; that's why I'm making a stackoverflow post. I don't know what the specific terminology to use in my search should be, so I came up with nothing. – Maurdekye Nov 17 '16 at 19:01
  • @Servy Also, I'm pretty sure that a type constriction of `T elem` is applicable to both `int elem` and `List elem`. – Maurdekye Nov 17 '16 at 19:06
  • A constriction of `IEnumerable iterable` should apply to `List` as well. – Maurdekye Nov 17 '16 at 19:16
  • So having two overloads with both of those type constrictions should mean that both are applicable to a value of type `List`, but only one of them would be called if a value of that type is passed. – Maurdekye Nov 17 '16 at 19:16
  • @Maurdekye In your code, when you write `ShowList(e)` `e` is of type `T`. `T` is not implicitly convertible to `IEnumerable`, so the first overload is not valid. If that were the only overload, the method wouldn't even compile. Likewise, `T` is not implicitly convertible to `string`, so it is *never* valid for that call to be bound to the second overload. If it were the only overload of the method it *also* wouldn't compile. The third overload can accept a parameter of any type, for which `T` is a valid example of, so it's a possible overload, so it wins. – Servy Nov 17 '16 at 20:05
  • @Maurdekye If you want to see the C# rules for betterness, feel free to look up the method overload resolution section of the C# specs. It's too much to fit into a comment, and anyway, as I've said, it is never taken into consideration in your code, so it is irrelevant to your situation. – Servy Nov 17 '16 at 20:06

2 Answers2

0

It looks to me that because e(in the delegate for iterable.Select) is of type T, therefore the compiler is going to use the type T overload. I would suggest, that since all it does is return the ToString() method, eliminate it and change the call in the delegate to ShowList(e.ToString()). If the ToString() is overloaded you'll most likely get meaningful data, otherwise you'll the object type data.

As for sending the List<List<T>> you'll need a different overload that one won't accept it.

tinstaafl
  • 6,908
  • 2
  • 15
  • 22
0

The generic resolution does seem to be a little odd. I would think that if an object implements the generic IEnumerable<T>, that this match would be more specific than just matching the generic <T>, but apparently if you have a List<T> the compiler shows preference to matching <T> rather than IEnumerable<T>. I found someone else who was having a similar problem here, and their solution was to overload with List<T>, but that doesn't seem like a good solution, because then you would be overloading everything that implements IEnumerable<T>. I found a simple work around using the is operator

    static string ShowList<T>(IEnumerable<T> iterable)
    {
        return "[" + string.Join(", ", iterable.Select(e => ShowList(e))) + "]";
    }

    static string ShowList<T>(T elem)
    {
        if (elem is IEnumerable<object> iterable)
        {
            //This will force the call to the overload for IEnumerable<T>
            return ShowList(iterable);
        }
        else
        {
            //This call will implicitly call ToString, so it covers the generic
            //object case that needs to call ToString and the case where it is
            //already a string, so the string overload is not needed.
            return $"\"{elem}\"";
        }
    }

Here is code for testing these methods intentionally mixing List with arrays to show that it will work for different items that implement IEnumerable<T>

var list = new List<string[]>(){new string[2]{"hello", "world"}, new string[2]{"foo", "bar"}};
Console.WriteLine(ShowList(list)); //outputs: [["hello", "world"], ["foo", "bar"]]
TJ Rockefeller
  • 3,178
  • 17
  • 43